Suppose I have 3 services. First I call serviceA
, it returns a CompletableFuture
. After that with the result I call serviceB
and serviceC
paralelly ( thenCompose()
). After I have all result I would like to combine all 3 results and return it to some caller. In the caller I would like to wait overall X millseconds to the whole process so that:
serviceA
call is in progress: throw some exception (so it is mandatory) serviceB
and serviceC
calls are in progress: return some default value (they are optional). This is the reason I try to use the getNow(fallback)
method of the CompletableFuture
Please check below my code snippets, if I use long delays in serviceB
and serviceC
calls, I always ends up with a TimeoutException
. How can I do this?
public CompletableFuture<Result> getFuture() {
CompletableFuture<A> resultA = serviceA.call();
CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
return CompletableFuture.allOf(resultB, resultC)
.thenApply(ignoredVoid -> combine(
resultA.join(),
resultB.getNow(fallbackB),
resultC.getNow(fallbackC));
}
public Result extractFuture(CompletableFuture<Result> future) {
Result result;
try {
result = future.get(timeOut, MILLISECONDS);
} catch (ExecutionException ex) {
...
} catch (InterruptedException | TimeoutException ex) {
// I always ends up here...
}
return result;
}
The future returned by .allOf(resultB, resultC)
is only completed when both, resultB
and resultC
are completed, therefore, the dependent function ignoredVoid -> combine(resultA.join(), resultB.getNow(fallbackB), resultC.getNow(fallbackC)
will only get evaluated if resultB
and resultC
are completed and providing a fallback has no effect at all.
It is generally impossible to react on a get()
call within these function. Which should be obvious considering that there can be an arbitrary number of get()
calls on the future at different times with different timeouts, but the function passed to thenApply
is only evaluated once.
The only way to handle a consumer specified timeout within getFuture()
is to change it to return a function which receives the timeout:
interface FutureFunc<R> {
R get(long time, TimeUnit u) throws ExecutionException;
}
public FutureFunc<Result> getFuture() {
CompletableFuture<A> resultA = serviceA.call();
CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
CompletableFuture<Result> optimistic = CompletableFuture.allOf(resultB, resultC)
.thenApply(ignoredVoid -> combine(resultA.join(), resultB.join(), resultC.join()));
return (t,u) -> {
try {
return optimistic.get(t, u);
} catch (InterruptedException | TimeoutException ex) {
return combine(resultA.join(), resultB.getNow(fallbackB),
resultC.getNow(fallbackC));
}
};
}
public Result extractFuture(FutureFunc<Result> future) {
Result result;
try {
result = future.get(timeOut, MILLISECONDS);
} catch (ExecutionException ex) {
...
}
return result;
}
Now, different calls with different timeouts can be made, with possibly different outcome as long as B or C have not completed yet. Not that there is some ambiguity regarding the combine
method which may also take some time.
You could change the function to a
return (t,u) -> {
try {
if(resultB.isDone() && resultC.isDone()) return optimistic.get();
return optimistic.get(t, u);
} catch (InterruptedException | TimeoutException ex) {
return combine(resultA.join(), resultB.getNow(fallbackB),
resultC.getNow(fallbackC));
}
};
to wait for the completion of a possibly already running combine
. In either case, there is no guaranty that the result is delivered within the specified time, as even if the fallback values for B and C are used, there will be an execution of combine
that may take an arbitrary amount of time.
If you want cancellation like behavior, ie that all result queries return the same result, even if it has been calculated using the fallback values due to the first query, you can use instead
public FutureFunc<Result> getFuture() {
CompletableFuture<A> resultA = serviceA.call();
CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
CompletableFuture<Void> bAndC = CompletableFuture.allOf(resultB, resultC);
CompletableFuture<Result> result = bAndC
.thenApply(ignoredVoid -> combine(resultA.join(), resultB.join(),
resultC.join()));
return (t,u) -> {
try {
bAndC.get(t, u);
} catch (InterruptedException|TimeoutException ex) {
resultB.complete(fallbackB);
resultC.complete(fallbackC);
}
try {
return result.get();
} catch (InterruptedException ex) {
throw new ExecutionException(ex);
}
};
}
With this, all queries on a single FutureFunc
will consistently return the same result, even if it is based on fallback values due to the first timeout. This variant also consistently excludes the execution of combine
from the timeout.
Of course, if different timeouts are not intended at all, you could refactor getFuture()
to get the desired timeout in advance, eg as parameter. That would simplify the implementation significantly and it could return a future again:
public CompletableFuture<Result> getFuture(long timeOut, TimeUnit u) {
CompletableFuture<A> resultA = serviceA.call();
CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor();
e.schedule(() -> resultB.complete(fallbackB), timeOut, u);
e.schedule(() -> resultC.complete(fallbackC), timeOut, u);
CompletableFuture<Void> bAndC = CompletableFuture.allOf(resultB, resultC);
bAndC.thenRun(e::shutdown);
return bAndC.thenApply(ignoredVoid ->
combine(resultA.join(), resultB.join(), resultC.join()));
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.