What I am trying to do:
Background: Latency is critical. I have 2 methods that return basically the same information, but will execute differently. One will be on average faster, but may by chance sometimes be slower. The second is on average slower, but more reliable.
So the requirements are:
- I have 2 CompletableFutures. I want to take the first one that returns.
- When an exception occurs in the first future, I want to fallback to the second.
- When an exception occurs in the second future, I want to generate an exceptional response.
- Because of #2 and #3, if an exception occurs in both, I want to generate an exceptional response.
Existing Code Attempt:
public Foo getFoo() {
CompletableFuture<Foo> firstFuture = CompletableFuture
.supplyAsync(this::doOptimisticWork, this.executor)
.timeout(this.timeout, TimeUnit.MILLISECONDS));
CompletableFuture<Foo> secondFuture = CompletableFuture
.supplyAsync(this::doFallbackWork, this.executor)
.timeout(this.timeout, TimeUnit.MILLISECONDS))
.exceptionally(this::getExceptionalResponse);
CompletableFuture<Foo> compositeFuture = CompletableFuture
.anyOf(firstFuture, secondFuture)
.exceptionally(throwable -> secondFuture.get())
.thenApply(Foo.class::cast);
return compositeFuture.join;
}
This works exactly as expected for the following cases:
- If
firstFuture
returns first, that is the overall result. - If
secondFuture
returns first, that is the overall result. - If
firstFuture
encounters an exception (bydoOptimisticWork
) then we fallback tosecondFuture
. - If
secondFuture
encounters an exception, we directly generate the exceptional response. - If
firstFuture
times out, then already we returnedsecondFuture
so this is actually the same case as #2. - If
secondFuture
times out, then already we returnedfirstFuture
so this is actually the same case as #1.
This does not work for the following case:
- Both
firstFuture
andsecondFuture
timeout. In this case,
I expect that the compositeFuture
completes exceptionally (some random chance on which future triggers this, since they both have the same timeout). This triggers the exceptionally clause in teh completableFuture
, which then waits on firstFuture
to complete. Then the firstFuture
should encounter a timeout exception, which triggers it to fall back to its own (secondFuture) exceptionally clause, where we generate an exceptional response.
I added a bunch of println to check the state of each CompletableFuture at each step. In actuality what is happening is that the compositeFuture
completes exceptionally (as expected), and then we wait on the secondFuture
to complete. The secondFuture
then seems to ignore its own timeout, and simply waits indefinitely for its supplier to complete. This is a problem since now the whole sequence is waiting on this as the compositeFuture
join method is blocked.
I am suspicious about the close timing of both of firstFuture
and secondFuture
, but am not able to reproduce any conclusive behavior by injecting different timeouts. Interestingly I can ensure with unit tests that this works as expected when the do<>Work
methods throw exceptions, the only problem seems to be with the CF configured timeouts.
Question:
How can we nest 2 CompleteableFutures with their own timeouts into a composite CompletableFuture, and get the overall sequence to honor the timeout?