10

I have the following code (resulting from my previous question) that schedules a task on a remote server, and then polls for completion using ScheduledExecutorService#scheduleAtFixedRate. Once the task is complete, it downloads the result. I want to return a Future to the caller so they can decide when and how long to block, and give them the option to cancel the task.

My problem is that if the client cancels the Future returned by the download method, whenComplete block doesn't execute. If I remove thenApply it does. It's obvious I'm misunderstanding something about Future composition... What should I change?

public Future<Object> download(Something something) {
    String jobId = schedule(something);
    CompletableFuture<String> job = pollForCompletion(jobId);
    return job.thenApply(this::downloadResult);
}

private CompletableFuture<String> pollForCompletion(String jobId) {
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    CompletableFuture<String> completionFuture = new CompletableFuture<>();

    ScheduledFuture<?> checkFuture = executor.scheduleAtFixedRate(() -> {           
            if (pollRemoteServer(jobId).equals("COMPLETE")) {
                completionFuture.complete(jobId);
            }
    }, 0, 10, TimeUnit.SECONDS);
    completionFuture                
            .whenComplete((result, thrown) -> {
                System.out.println("XXXXXXXXXXX"); //Never happens unless thenApply is removed
                checkFuture.cancel(true);
                executor.shutdown();
            });
    return completionFuture;
}

On the same note, if I do:

return completionFuture.whenComplete(...)

instead of

completionFuture.whenComplete(...);
return completionFuture;

whenComplete also never executes. This seems very counterintuitive to me. Shouldn't logically the Future returned by whenComplete be the one I should hold on to?

EDIT:

I changed my code to explicitly back-propagate the cancellation. It's abhorrent and unreadable, but it works and I couldn't find a better way:

public Future<Object> download(Something something) throws ChartDataGenException, Exception {
        String jobId = schedule(something);
        CompletableFuture<String> job = pollForCompletion(jobId);
        CompletableFuture<Object> resulting = job.thenApply(this::download);
        resulting.whenComplete((result, thrown) -> {
            if (resulting.isCancelled()) { //the check is not necessary, but communicates the intent better
                job.cancel(true);
            }
        });
        return resulting;
}

EDIT 2:

I've discovered tascalate-concurrent, a wonderful library providing a sane implementation of CompletionStage, with support for dependent promises (via the DependentPromise class) that can transparently back-propagate cancellations. Seems perfect for this use-case.

This should be enough:

DependentPromise
          .from(pollForCompletion(jobId))
          .thenApply(this::download, true); //true means the cancellation should back-propagate

Didn't test this approach, mind you.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • It doesn't even enter the `whenComplete` block. I put a breakpoint and a `System.out.print` inside, and neither does the breakpoint get hit nor does the line get printed. Both happen if I remove the `thenApply` bit. – kaqqao Oct 26 '16 at 09:16
  • According to my understanding it should be the reverse of what you report. `completionFuture.whenComplete()` is a pure function and shouldn't change anything in the way `completionFuture` itself operates. If you don't return the result of `whenComplete`, it should become unreachable and subject to GC. – Marko Topolnik Oct 26 '16 at 09:34
  • Absolutely agreed. But this is what I'm seeing... If return the result of `whenComplete`, and `cancel` it immediately, I don't get XXXXX in the console. On the other hand, if I return the original `completionFuture` and `cancel` _that_, I do. – kaqqao Oct 26 '16 at 09:37
  • 2
    This is my new understanding: 1. it is correct to pass the stage before applying `whenComplete` because you must cancel _this_ stage to make it trigger the action in the downstream `whenComplete` stage. If you cancel the `whenComplete` stage, it simply doesn't execute. 2. You must also hold on to the `whenComplete` stage to ensure its reachability. Confusing... – Marko Topolnik Oct 26 '16 at 09:57
  • 1
    I have tried to reproduce your problem based on your code (adding the missing parts), and I don't have your issue: `whenComplete()` is always executed (after the scheduled tasks completes the future), and cancelling by the client has absolutely no effect – cancellation is not backpropagated. Also, if I implement backpropagation of the cancellation, `whenComplete()` is still executed when the client cancels the future. Could you turn this code into an [mcve]? – Didier L Oct 26 '16 at 10:32
  • 1
    @Didier L: I guess, the fact that cancellation is not backpropagated is exactly what the OP has to realize. – Holger Oct 26 '16 at 10:54
  • 1
    @Holger Probably the next step indeed, but that will not explain why `whenComplete` is not executed – Didier L Oct 26 '16 at 11:01
  • @Holger Backpropagation is exactly what I misunderstood, as you say. I thought the entire pipeline is cancelled implicitly (sort of like closing the wrapper stream closes all underlying ones). – kaqqao Oct 26 '16 at 11:31
  • Actually you can draw a parallel to a stream, but keep in mind that the wrapper is "upstream" from the wrapped object. If you close the wrapped stream, it doesn't propagate to the wrapper, same as here. – Marko Topolnik Oct 26 '16 at 11:44
  • 3
    For backpropagation, you can also test for `resulting.isCancelled()` instead of relying on `instanceof`. – Didier L Oct 26 '16 at 11:51

1 Answers1

7

Your structure is as follows:

           ┌──────────────────┐
           │ completionFuture |
           └──────────────────┘
             ↓              ↓
  ┌──────────────┐      ┌───────────┐
  │ whenComplete |      │ thenApply |
  └──────────────┘      └───────────┘

So when you cancel the thenApply future, the original completionFuture object remains unaffected as it doesn’t depend on the thenApply stage. If, however, you don’t chain the thenApply stage, you’re returning the original completionFuture instance and canceling this stage causes the cancellation of all dependent stages, causing the whenComplete action to be executed immediately.

But when the thenApply stage is cancelled, the completionFuture still may get completed when the pollRemoteServer(jobId).equals("COMPLETE") condition is fulfilled, as that polling doesn’t stop. But we don’t know the relationship of jobId = schedule(something) and pollRemoteServer(jobId). If your application state changes in a way that this condition can never be fulfilled after canceling a download, this future will never complete…


Regarding your last question, which future is “the one I should hold on to?”, there is no requirement to have a linear chain of futures, in fact, while the convenience methods of CompletableFuture make it easy to create such a chain, more than often, it’s the least useful thing to do, as you could just write a block of code, if you have a linear dependency. Your model of chaining two independent stages is right, but cancellation doesn’t work through it, but it wouldn’t work through a linear chain either.

If you want to be able to cancel the source stage, you need a reference to it, but if you want to be able to get the result of a dependent stage, you’ll need a reference to that stage too.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • OP doesn't need the result of `whenComplete`, though. But my understanding is that this stage must be explicitly retained, otherwise it's unreachable and may or may not exist at the time of cancellation. – Marko Topolnik Oct 26 '16 at 10:55
  • @MarkoTopolnik I guess the original future that you call `whenComplete` on retains the reference to the chained one... Otherwise, it would be seriously screwy. – kaqqao Oct 26 '16 at 11:33
  • @kaqqao It's probably right due to the way one expects this to be implemented, but it's still unspecified behavior and unhealthy to rely on. – Marko Topolnik Oct 26 '16 at 11:42
  • @Holger Please see my edited question. Is this the right way to check for cancellation? I couldn't find a different way and this looks dreadful... It does behave as desired though: canceling the result of `download` cancels the `pollForCompletion`, shutting the executor as well. – kaqqao Oct 26 '16 at 11:49
  • @MarkoTopolnik you do not need to keep references to each `CompletableFuture` to have them executed. If you had to, it would break the API of `CompletionStage` which indicates that the task will be executed without such a requirement. Also, it would be a nightmare to use. Anyway, `CompletableFuture` holds hard references internally via its `Completion stack`. – Didier L Oct 26 '16 at 11:49
  • @DidierL There is no explicit wording to that effect. – Marko Topolnik Oct 26 '16 at 11:53
  • @MarkoTopolnik exactly, the wording in the javadoc is “_Returns a new CompletionStage that, when this stage completes normally, is executed […]_”. So it MUST be executed even if you do not keep a reference to it. If it was otherwise, there would be an explicit wording for it. – Didier L Oct 26 '16 at 12:01
  • 2
    @kaqqao: you don’t need to poll `resulting.isCancelled()` as `thrown` will be `CancellationException` then. But actually, you don’t need any check at all. You can simply use `resulting.whenComplete((result, thrown) -> job.cancel(true));` as by the time, the function is executed, either, `resulting` has been canceled or the `job` has been completed, in which case it will ignore the cancellation anyway. – Holger Oct 26 '16 at 12:25
  • 2
    @Marko Topolnik: every time I `submit` a job to an `ExecutorService` or register a listener to an event source, I assume that it will retain a reference to the object to fulfill the contract. Well, actually, it is indeed not guaranteed that the *object* is still alive, but that doesn’t matter as long as the *associated action* is performed, which is, what this is all about. We want the `BiConsumer`’s `accept` method to be executed; the `CompletableFuture` returned by `whenComplete` is irrelevant… – Holger Oct 26 '16 at 12:33