kamil
kamil

Reputation: 3522

Explanation needed, applyAsync with nested allOf treats CompletionStage as finished

In my app I have 3 future calls, that are done in parallel and when a response for one of them is received, I have another 3 requests that all should finish before proceeding with code execution, precisely the DeferredResult from spring.

After a while, I realized that page is sometimes rendered before the latter 3 requests are done. Original source code (ommited logic for simplicity):

public DeferredResult<String> someControllerMethod() {

    DeferredResult<String> result = new DeferredResult();

    CompletableFuture.allOf(
        future1(),
        future2(),
        future3()
    )
    .whenComplete((aVoid, throwable) -> result.setResult("something"));

    return result;
}

public CompletableFuture<?> future3() {
    return someService.asyncCall()
        .thenApplyAsync(response -> {
            ....
            return CompletableFuture.allOf(
                future4(),
                future5(),
                future6()
            );
        }
    );
}

With thenApplyAsync sometimes DeferredResult is completed before actual future, while changing to thenComposeAsync seems solve the issue. Could someone explain me why? Or it's a bug in my code somewhere and it should not behave this way?

Upvotes: 0

Views: 855

Answers (1)

Holger
Holger

Reputation: 298233

thenApply[Async] accepts a function that evaluates to an arbitrary value. Once the value has been returned, the future will be completed with that value. When the function, like in your code, returns another future, this doesn’t add an additional meaning to it, the future will be the result value, whether completed or not, just like any other object.

In fact, your

public CompletableFuture<Void> future3() {
    return someService.asyncCall()
        .thenApplyAsync(response -> {
            ....
            return CompletableFuture.allOf(
                future4(),
                future5(),
                future6()
            );
        }
    );
}

method does not even compile, as the result is CompletableFuture<CompletableFuture<Void>>, a future whose result value is another future. The only way not to spot the error, is to use a broader type, e.g. CompletableFuture<Object> or CompletableFuture<?>, as the return type of future3().

In contrast, thenCompose[Async] expects a function that evaluates to another future, to exactly the outcome you expect. That’s the fundamental different between “apply” and “compose”. If you keep the specific CompletableFuture<Void> return type for future3(), the compiler already guides you to use “compose”, as only that will be accepted.

public CompletableFuture<Void> future3() {
    return someService.asyncCall()
        .thenComposeAsync(response -> {
            ....
            return CompletableFuture.allOf(
                future4(),
                future5(),
                future6()
            );
        }
    );
}

Upvotes: 2

Related Questions