Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursively cancel an allOf CompletableFuture

If I have

CompletableFuture<Something> future1 = service.request(param1);
CompletableFuture<Something> future2 = service.request(param2);
CompletableFuture<Void> many = CompletableFuture.allOf(future1, future2);

what will happen when I do many.cancel()? Will future1 and future2 be cancelled as well? If not, what would be the cleanest way to achieve this? I'm reluctant to hold on to future1 and future2, just to be able to cancel them when I want to cancel many.

Some background on why I want this: when receiving a piece of data, I need to request matching, potentially future data to perform a computation. If a newer piece of data arrives, I want to cancel the completion of the earlier computation, because the result will immediately be superceded by the new computation.

like image 997
Michel Avatar asked Apr 13 '17 10:04

Michel


Video Answer


1 Answers

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.

like image 151
Holger Avatar answered Nov 15 '22 23:11

Holger