1

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:

  1. I have 2 CompletableFutures. I want to take the first one that returns.
  2. When an exception occurs in the first future, I want to fallback to the second.
  3. When an exception occurs in the second future, I want to generate an exceptional response.
  4. 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:

  1. If firstFuture returns first, that is the overall result.
  2. If secondFuture returns first, that is the overall result.
  3. If firstFuture encounters an exception (by doOptimisticWork) then we fallback to secondFuture.
  4. If secondFuture encounters an exception, we directly generate the exceptional response.
  5. If firstFuture times out, then already we returned secondFuture so this is actually the same case as #2.
  6. If secondFuture times out, then already we returned firstFuture so this is actually the same case as #1.

This does not work for the following case:

  1. Both firstFuture and secondFuture 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?

Mike
  • 11
  • 2
  • I have a doubt. If second CF is the fallback scenario, then is not like you need both or any result at once, you need to evaluate the result from optimistic work, and if that failed evaluate the result from fallback work. I mean this because you're using `CompletableFuture#anyOf` which defies this idea. Or there's something I'm not getting right. – Luiggi Mendoza Aug 10 '21 at 18:12
  • I will update the description with more information. I agree, I am not 100% sure that `anyOf` is the right thing to use. The overarching idea is that I want very fast execution, and I have 2 methods (`doOptimisticWork` and `doFallbackWork`) to execute in parallel. So for the sake of speed, I want to take the _first_ that returns. So the `anyOf` (and really the reason for wrapping two CF into a composite one) is in order to take the first that returns. – Mike Aug 10 '21 at 18:18
  • That's what I don't understand. You say you want to get any of those results, but then you state that if it happens to be `doOptimisticWork` the first method to finish, then you need to evaluate if is a proper result or an exception. What happens if `doFallbackWork` finishes first with an exception and no timeout was given? Would you wait to see the result of `doOptimisticWork` to see if there was an exception or would you rethrow the exception from `doFallbackWork`? – Luiggi Mendoza Aug 10 '21 at 18:22
  • If doOptimisticWork returns _successfully_ then I want to take that. If it returns _exceptionally_, then I want to fallback to `doFallbackWork`. If `doFallbackWork` returns exceptionally, then I want to accept that as the final answer. In this case, we can simply ignore the result of `doOptimisticWork`. – Mike Aug 10 '21 at 18:35
  • Does this answer your question? [CompletableFuture: Waiting for first one normally return?](https://stackoverflow.com/questions/33913193/completablefuture-waiting-for-first-one-normally-return) – Tim Moore Aug 11 '21 at 06:19
  • @TimMoore not exactly. In that case they are trying to suppress exceptionally cases. In my case, I want to define explicit fallback scenarios for each CF. – Mike Aug 11 '21 at 17:25

0 Answers0