Skip to content

Cancellation #64

Closed
kriskowal opened this Issue May 2, 2012 · 7 comments

5 participants

@kriskowal
Owner

Cancellation is tricky.

Mark Miller argues that we should not add cancellation at all. On the flip side, most people feel the need for it, including @domenic and @igorminar. If cancelation is a mistake, it is an oft-made mistake.

Consider a naïve cancelable promise.

function service() {
    // start work…
    var deferred = Q.defer(function cancel() {
        // …stop work
    });
    deferred.promise;
}

var promise = service();
clientA(promise);
clientB(promise);

There is a hazard in this case. Either clientA or clientB might call promise.cancel() and thus interfere with the other client’s progress. With your face close to the page, it looks like the problem is that we need a way to count how many services depend on the promise. From a higher perspective, cancelation is inherently hazard prone.

The philosophical reasoning behind banning cancelation outright is that a cancelable promise must necessarily have all of the power of a deferred, nullifying the (POLA) advantage of separating the promise part from the resolve part. Anyone with a cancelable can call cancel(), which is effectively equivalent to preemptively rejecting the promise (resolving the promise with a rejection).

So, Mark argues, if you want something to be cancelable, just return the whole deferred to make it clear that you’re granting both powers. “Similar things should either be very different or the same” — Mark Miller attributes this paraphrase to Alan Kay.

In this example, the promise can be cancelled by rejecting the deferred.

function delay(ms) {
    var handle = setTimeout(deferred.resolve, ms);
    var deferred = Q.defer();
    Q.catch(deferred.promise, function () {
        clearTimeout(handle);
    });
    deferred;
}

var deferred = service();
clientA(deferred);
clientB(deferred);

deferred.reject(new Error("Cancelled”));

This at least demonstrates the critical insight that, regardless of how cancelation looks, this is certainly how it should work. It does not inherently solve the problem of tracking how many parties remain interested in the result.

However, there is a story on graceful migration that this does not address. Expressing that you are no longer interested in a result is orthogonal to expressing that you are able to cancel work if no one is interested any longer in the result. I posit, it should be possible to go through either side of this extra effort independently. That is, a cancelable should have the same interface as a promise, albeit all the powers of a deferred, but also, all promises should have the interface of a cancelable, even if they do not provide that power. As such, it would be possible for a service provider to add cancelability to a promise before or after a service consumer adds support for proactively canceling promises that they no longer need.

Down this track of thought, promises would have a cancel() method which would be a NOOP. A cancelable would be a new type of object entirely, that has the interface of a promise and the powers of a deferred. A cancelable would have a functioning cancel() method. All of the promise-like methods of a cancelable would return new, normal promises, and cancelation would not implicitly propagate. Cancelables could be constructed from deferreds, like Q.cancelable(deferred).

The cancelable solution might not stick since it would obviate chaining. Each step of the promise chain would have to be replaced with an explicit mechanism for how to forward cancelation. We’d have to explore some code samples to see whether it would be worth dealing with, and whether we might find ways to make it easy to flag common policies.

For either approach, using a deferred or a cancelable, there is the independent problem of tracking how many parties are still interested in the result of a promise and actually canceling only when that drops to 0.

Ideally, we could involve the garbage collector since it does this work already. We could use a WeakRef, or any mechanism that notifies the application after an object has been garbage collected. We could use this mechanism to implicitly cancel any promise for which there are provably no observers. However, we do not live in a world with WeakRef, and such would only work if the promise library were to exist in a privileged context, possibly served to unprivileged contexts.

In the real world, we would have to do reference counting in application-space. One thought would be to add a fork() function to a cancelable. The cancelable could contain an internal counter of how many forks exist and each fork could decrement that number exactly once. When they drop to zero, the cancelable could commit to stopping work.

This should be an accurate summary of every discussion I’ve had on the topic as-yet. Please chime in.

@domenic
Collaborator
domenic commented Sep 25, 2012

We need to assimilate cancellable WinJS promises at work so I'll probably be working on this over the next week or two.

@domenic domenic was assigned Sep 25, 2012
@kriskowal
Owner

@domenic, @igorminar, I’ve updated the description to contain a breakdown of all the discussions we’ve had on the topic.

@ForbesLindesay
Collaborator

What does WinJS do? I thought they just made their functions take a cancellationToken argument and that it had nothing to do with the promises/tasks returned?

@kriskowal kriskowal added this to the 2.0.0 milestone Apr 7, 2014
@kriskowal kriskowal self-assigned this May 28, 2014
@leonerd
leonerd commented Jul 21, 2014

This sounds exactly like the same discussion happening with Perl's Future module, here

https://rt.cpan.org/Public/Bug/Display.html?id=96685

Essentially the conclusion seems to be "add a ->reuse method to mark that something else is using it, so require an extra ->cancel before it counts".

@spoike spoike referenced this issue in reflux/refluxjs Sep 5, 2014
Closed

Asynchronous dataflow example #57

@kriskowal
Owner

In short, Q will not implement cancellation on promises. There is an opportunity for another library to introduce an alternative primitive that requires explicit forking and has a single-producer to single-consumer binding relationship. Such a library would have the same interface as Q but the implementation would be very different, and would compromise the unidirectional communication guarantees that Q provides. Q can be used for cancellation if you set up an API to creatively pass a cancellation resolver (express capability to cancel) as an argument and use the promise internally to observe the impatience or disinterest of the consumer.

https://github.com/kriskowal/gtor/blob/master/cancelation.md

@kriskowal kriskowal closed this Sep 11, 2014
@tolmasky

I'm curious on thoughts about wanting to use q.race to run a number of intense computations simultaneously and give up on all the ones that don't finish first: q.race(expensive op 1, expensive op 2, etc). Here once you have your answer you don't want to be wasting resources any more. How could similar patterns be implemented in a compost or way without for example w.race just canceling the incomplete promises?

@kriskowal
Owner

@tolmasky There is an interface that is strongly analogous to promises that I tentatively call tasks. Tasks only differ from promises in that they are unicast (meaning single-consumer and explicitly forkable) and therefore cancelable. They have the same interface but differ in behavior. The implementation of race would behave as you describe for tasks. Similarly, all would cancel all outstanding tasks if a single task fails. A promise cannot make the same assumptions because it is broadcast, which confers other benefits, like easy memoization and safe distribution. I encourage you to look over my General Theory article.

https://github.com/kriskowal/gtor/blob/master/task.js
https://github.com/kriskowal/gtor/blob/master/README.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.