tpdi
tpdi

Reputation: 35171

Java CompletableFutures: is it ever a acceptable to not join/get?

Is it ever correct to not return or join on a CompletableFuture?

That is, is this code ever correct?

void noJoin() {
   CompletableFuture<Void> cfa = CompletableFuture
                                   .supplyAsync(() -> "a")
                                   .thenAccept(this::someSideEffect);

   CompletableFuture<Void> cfb = CompletableFuture
                                   .supplyAsync(() -> "b")
                                   .thenAccept(this::someSideEffect);
}

In the above code, we never know if the Futures complete successfully, or at all. (And I don't even know that the Futures created in noJoin are not eligible for garbage collection; but presumably the ForkJoinPool.commonPool() holds onto those references?)

In any case, if it truly doesn't matter to our program whether or not those Futures succeed, a better implementation of noJoin is just a no-op:

void noJoin() {}

If on the other hand, it does matter, we need to either join on or return both Futures, by composing them (either serially with thenCompose, or in parallel with thenCombine or allOf) and then join the composed Future or return it to our caller.

By joining, we block and throw an exception if either Future is exceptional; or better, by returning a Future, we remain asynchronous while ensuring that our caller gets a Future that holds any exceptional result:

Future<Void> returnBothAsync() {

       CompletableFuture<Void> cfa = CompletableFuture
                                       .supplyAsync(() -> "a")
                                       .thenAccept(this::someSideEffect);

       CompletableFuture<Void> cfb = CompletableFuture
                                       .supplyAsync(() -> "b")
                                       .thenAccept(this::someSideEffect);

       return CompletableFuture.allOf(cfa, cfb);
}

or

void joinBothBlocking() {

       CompletableFuture<Void> cfa = CompletableFuture
                                       .supplyAsync(() -> "a")
                                       .thenAccept(this::someSideEffect);

       CompletableFuture<Void> cfb = CompletableFuture
                                       .supplyAsync(() -> "b")
                                       .thenAccept(this::someSideEffect);

       CompletableFuture.allOf(cfa, cfb).get(50L, TimeUnit.MILLISECONDS);
}

I think this is true even if we arrange to handle all exceptions:

void noJoin() {
   CompletableFuture<Void> cfa = CompletableFuture
                                   .supplyAsync(() -> "a")
                                   .thenAccept(this::someSideEffect)
                                   .exceptionally(e -> {
                                         Logger.log(e);
                                         return DEFAULT;
                                   });

   CompletableFuture<Void> cfb = CompletableFuture
                                   .supplyAsync(() -> "b")
                                   .thenAccept(this::someSideEffect);
}

because even if the exceptional are handled / "can't happen", we still don't know if the Future ever completed at all.

Or am I wrong, and there are cases where code like that in noJoin is correct?

Upvotes: 4

Views: 2829

Answers (1)

Marcono1234
Marcono1234

Reputation: 6934

This is not a complete answer to your question. It probably depends on the exact use case to be able to tell how to handle CompletableFuture and their results.

If you choose to not wait for the results of the CompletableFutures, you will likely have to make sure that the used executor finishes all tasks. In your case the used executor is ForkJoinPool.commonPool() whose documentation says:

[...] However this pool and any ongoing processing are automatically terminated upon program System.exit(int). Any program that relies on asynchronous task processing to complete before program termination should invoke commonPool().awaitQuiescence, before exit.

Upvotes: 3

Related Questions