promises-aplus / cancellation-spec

Discussion and drafts of a possible promise cancellation spec.
24 stars 5 forks source link

Draft E: `cancel` as a message #15

Closed yahuio closed 3 years ago

yahuio commented 9 years ago

Revisit Promise Concept

The Promise pattern is about Consumer and Producer. ConsumerA request ItemA from the ProducerA, ProducerA creates a Promise pA with TaskA (suppose to produce ItemA) to return to Consumer A immediately, regardless whether ItemA is available now or later. So Promise is the kind of the proxy to access A, providing to Producer utility of then. However, when P(ItemA)'s then is being triggered with handler onFulfilled, P(ItemA) itself becomes a Producer to create a Promise for the new Consumer.

So what's "cancel" at conceptual level?

Thanks to @bergus, led me to this understanding of "cancel": Consumer cancels it's the request (demand of ItemA) from the Producer. Which is only a message to the Producer, so that Producer can response to that, such as:

Therefore, Promise itself cannot be cancelled, cancel is just a specific message Consumer wants to tell the Producer. It's up to the Producer to decide what to do with it whether to cancel the task or not.

As stated in the concept section, then is a Producer that creates another Promise. So it's up to then to decide wether to cancel the task when it receives a cancel request.

Cancel handling for the Producer then

In my understanding, the task of then is, run the handler upon fulfilment/rejection.

So, when it's being cancelled (while it's unresolved yet of coz'), it should cancel the task on them. However, since a Promise may have multiple Consumers request through multiple triggers of then, it should ONLY cancel the task if there're no more Consumers.

Therefore, Promise should provides the ref counting utility for Producer to get to know how many Consumers are pending.

In Summary

  1. Consumer cancel a promise is just a message to the Producer
  2. Producer then suspends cancels it's task, run the handler upon fulfillment/rejection, upon cancellation message received, ONLY if a. Promise of then is still PENDING b. there's no more Consumer
  3. Ref counting utility needed

    API Proposal

    Constructor

var promise = new Promise( function resolver(resolve, reject,notify) {
   ...
}, function canceller(cancel, message, hasPendingThen) {

// boolean whether there are still any pending consumer
if (!isDemanded) {
    cancel(); // when producer decides to cancel the promise, calling this method turns the promise into `Cancelled` state
 }
});

Promise Instance cancel Method

Cancel a promise with message and consumerId. e.g. The consumerId would be the id of the promise created from then.

promise.cancel(message, [consumerId]);

Promise Instance isCancelled Method

Returns true if promise is in Cancelled state

promise.isCancelled();

Promise Instance getId Method

Returns a unique id of a promise

p.getId();

Sample Usage

Cancel then produced Promise, Producer then cancel it's task

var p1 = new Promise(function(resolve, reject, notify) {
    setTimeout(function() {
        resolve(1);  //resolve p1
    },1);
}, function(cancel, message, hasPendingThen) {
    // called upon p3.cancel below
    // hasPendingThen returns true, as all pending then are cancelled already through bubbling
    // here it does nothing, so promise is still resolvable after timeout
});

var p2 = p1.then(function() {
   // called upon p1 resolved 
});

var p3 = p2.then(function() {
    // never called as suspended by p3.cancel below
});

p3.cancel(); //p3, p2, is being suspended, triggered p1's onCancel handler

p2.isCancelled(); // true, p2 becomes cancelled
p3.isCancelled(); // true, p3 becomes cancelled
p1.isCancelled(); // false, p1 stays pending

Cancelling then produced Promise that has more than one Consumer

var p1 = new Promise( function resolver(resolve, reject, notify) {
}, function onCancel(cancel, message, hasPendingThen) {
     //not triggered
});

var p2 = p1.then(function() {});
var p3 = p1.then(function() {});

p2.cancel(); // cancel p2 suspends p2 only but message not propagated to p1 since p1 still has a request (i.e. p3) pending

Cancel delay produced Promise results in rejection

function delay(time) {
    var handle;
    return new Promise( function(resolve, reject, notify) {
         handle = setTimeout(function(){
               resolve();
         }, time);
    }, function(cancel, message, hasPendingThen) {
         clearTimeout(handle);
         cancel(); 
    });
}

var p1 = delay(100);
p1.cancel(); // fnA is triggered as delay is rejected
p1.isCancelled(); //true

Change Log

16/3/15

bergus commented 9 years ago

I do like the idea of message passing to the producer of a promise as much as you, especially if we talk about promises that use "progress" notifications to emit messages by themselves. However I think that cancellation should be something more specific. In your proposal, you call .cancel() but can have no expectations on what the promise does with it - it might call either success or failure callbacks, or even just neither of them; right now or later or never. This is quite problematic from the consumer's point of view, and imo from a promise library implementer's as well - who needs to deal with memory leaks on pending promises. Especially the resuming of suspended promises is problematic in that regard. You say

var p3 = p2.then(function() {
    // never called as suspended by p3.cancel below
});
p3.cancel();

but what would happen if someone called p3.then() now? Another thing that I don't understand is how your oncancel handlers knows about the multiplicity of subscribed consumers. How could it decide to "cancel the task only if there are no more consumers"? What about consumers who are late for the partypromise?

I think it would be nice to have some kind of .send(message) mechanism that can be used for things like suspension and resumption, or even for cancel messages (my own promise implementation does this in fact), but cancellation should have a more restricted meaning and clearly defined effects.

yahuio commented 9 years ago

This is quite problematic from the consumer's point of view, and imo from a promise library implementer's as well - who needs to deal with memory leaks on pending promises.

Right. I agree. I don't have any clean solution on this. Perhaps it's too ideal.

Another thing that I don't understand is how your oncancel handlers knows about the multiplicity of subscribed consumers. How could it decide to "cancel the task only if there are no more consumers"? What about consumers who are late for the partypromise?

Yes, it's not something manageable, unless the Producer keep track a reference of the Consumer to see identify which specific Consumer had cancelled the request. But it would lead to a memory leak issue as you said.

We, team in my company had an interesting discussion on it. We mirror the concept to a fast food shop scenario. Where the customers would be the Consumer at the end of the promise chain, requesting an order at the counter (Producer of the last node in the chain). Then request the food production node chain to create the food. Upon cancel request form the customer (Consumer) at the counter, the counter propagate the cancellation message to each node, and stops their task if it's not demanded by other customers. So in the real world, normally an order ID would be given to identify the customer.

I imply this in the promise implementation. All promise would be given an ID. If a child promise is created from parent promise through then. Then, when child promise get cancelled, the child promise will have to use it's ID to propagate the cancellation to the parent promise. parent promise then check if this ID is a valid chlid, if so, decrement a demand of it. And if it reaches 0, it will cancel it's task.

This isn't an ideal solution for sure that we know. In real use cases the IDs have to be managed properly to ensure cancellation will be carried. i.e. If an ID is lost, then the promise won't get cancelled as ref count will never decrement to 0.

Thoughts?

bergus commented 9 years ago

All promise would be given an ID.

All promises are objects, which already have an identity :-) Yeah, reference counting will probably be the way to go. This can be done in various ways without leaking memory, even if JS does not support weak references out of the box. The IDs you're talking of are a detail of one possible implementation.

yahuio commented 9 years ago

Updated.

trowski commented 9 years ago

I've implemented cancellable promises in PHP that might be of interest to this group. The implementation can be found here: https://github.com/icicleio/Icicle/tree/master/src/Promise. There's no reason that cancellation could not be implemented similarly in JS.

In the above implementation, the parent promise is only cancelled if all children (consumers) of the promise have also been cancelled. This is done by keeping a simple count of children and decrementing the count if a child is cancelled. Only if the count is 0 when a child is cancelled is the parent promise also cancelled. Cancelling a promise is similar to rejecting the promise, so subsequent calls to then() will call the rejection callback.

yahuio commented 3 years ago

Closing, not following anymore.