Recursively cancel an allOf CompletableFuture

Before you make you life harder than necessary, you should become aware of what cancelling a CompletableFuture actually does. Most important, it does not stop the associated computation.

If a computation associated with a CompletableFuture is already running, but has not completed yet, cancelling a CompletableFuture turns it into the “cancelled” state, which may have an immediate effect on all dependent stages, but not on the computation, which will continue until complete, though its attempt to complete the cancelled future will not have any effect.

While other Future’s might be cancelled with interruption, which will stop the computation, if it checks for interruption, this doesn’t apply to CompletableFuture, see CompletableFuture.cancel(boolean):

Parameters:

mayInterruptIfRunning - this value has no effect in this implementation because interrupts are not used to control processing.

So when you cancel either, future1 or future2, successfully, the only immediate effect would be the cancellation of many, which you can also achieve by calling cancel on many itself. It would have a broader effect, if there were more dependent stages, but since you stated, that you don’t want to keep references to future1 or future2, this doesn’t seem to be the case.

The following code demonstrates the behavior:

CompletableFuture<String> supply = CompletableFuture.supplyAsync(() -> {
    LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
    System.out.println("supplying value");
    return "foo";
});
CompletableFuture<String> then = supply.thenApply(s -> {
    System.out.println("Evaluating next stage");
    return s;
});
CompletableFuture<?> last = then.handle((s,t) -> {
    System.out.println("last stage: value: "+s+", throwable: "+t);
    return "";
});
System.out.println("cancelling: "+supply.cancel(true));
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.DAYS);

This code reproducible prints:

last stage: value: null, throwable: java.util.concurrent.CompletionException: java.util.concurrent.CancellationException
canceling: true
supplying value

(the order might change)

regardless of whether you call supply.cancel(true) or then.cancel(true) or whether you pass true or false; it won’t stop the ongoing Supplier evaluation.

There will be a difference, if the associated computation hasn’t been started yet and it does check the cancellation state when starting, like with the actions produced by the convenience methods in CompletableFuture. This is a rare situation, as normally, your service.request(paramN) call is supposed to trigger the evaluation.

It’s a fundamental property of the CompletableFuture, as its name suggests, that it is completable, i.e. anyone could call complete on it, thus, the CompletableFuture can’t control whoever might eventually call complete on it in the future. So all, cancel can achieve, is to set it to the cancelled state, which implies ignoring subsequent completion attempts and propagating the cancellation downward to the dependent actions.


So the bottom line is that you might already be fine with just calling cancel on the many instance, because calling cancel on future1 and future2 is unlikely to have an effect that is worth the complication of your code.


The tree constructed by CompletableFuture.allOf doesn't hold any references to the given instances of CompletableFuture. Instead if just builds completion tree, which is is completed when all of the given CompletableFutures complete (from JavaDocs).

So probably you have to keep references to all CompletableFuture in order to cancel them sequentially when it is needed.