Re: Cancellation architectural observations

> Cancellations should "chain"
> ======================
> If you have a cancellable promise p1, and use .then() to produce a new
> promise p2, p2 should also be cancelable, and in the default case,
> should "chain up" to p1 and cause it to cancel as well.

The CTS/Token approach can do this directly without needing a chain, which is observable on the p2 via its onrejected handler.

> If you chain multiple promises off of p1, like p2a and p2b, then
> canceling either one of the p2X promises should do nothing, but
> cancelling *both* of them should cancel p1. In other words, p1 can
> ref-count its "child" promises that retain cancellation abilities, and
> cancel itself when everything consuming its result has been cancelled.

> This is important so you don't have to explicitly keep track of every
> single cancelable thing you're doing, if you're only using it to
> immediately chain onward again.  You can just care about the final
> result, and if you end up not needing it, you can cancel it and it
> walks back and tries to cancel everything your result depends on.

CTS partially accomplishes this through linked registrations, though assuming that if all chained promises are canceled that the root should be canceled could be a mistake:

```js
var rootCts = new CancellationTokenSource();
var configPromise = fetchConfig(rootCts.token);

var alphaCts = new CancellationTokenSource();
var alphaPromise = fetchConfigProperty(configPromise, "alpha", alphaCts.token);

var betaCts = new CancellationTokenSource();
var betaPromise = fetchConfigProperty(configPromise, "beta", betaCts.token);

alphaCts.cancel();
betaCts.cancel();

// do we want to cancel configPromise here? What if we want to enlist via .then later?
// if we are sure we want that behavior we can do this explicitly

var requests = new Set();
function fetchConfigProperty(configPromise, name, token) {
  requests.add(token);
  var registration = token.register(() => {
    requests.delete(token);
    if (requests.size <= 0) { rootCts.cancel(); }
  });
  return configPromise.then(
    config => {
      requests.delete(token);
      registration.unregister();
      return config[name];
    },
    reason => {
      requests.delete(token);
      registration.unregister();
      throw reason;
    });
}
```

> Combinators should combine cancellations
> =================================
>
> If you do `let fp3 = FetchPromise.all(fp1, fp2)`, then an fp3.cancel()
> should try to cancel fp1 and fp2, as noted above.  You want all the
> (cancellation-aware) combinators to be just as friendly as chaining
> directly, for usability.

If the fetch api supports tokens, you could instead do:

```js
var cts = new CancellationTokenSource();
var fp1 = fetch(..., cts.token);
var fp2 = fetch(..., cts.token);
let fp3 = Promise.all([fp1, fp2]);
cts.cancel(); // will cancel fp1 and fp2
```

> You need to be able to "clean" a cancellable promise
> ========================================
>
> If the promise is what carries the cancellation ability, you need to
> be able to observe its value without carrying the cancellability
> around, to prevent spreading power around in an unwanted way (and
> prevent upping the "chained promises" refcount).  This is doable by
> just wrapping it in a standard promise - `Promise.resolve(fetch(...))`
> will return a normal non-cancellable promise.

This behavior is an explicit benefit of CTS, as it does not conflate Promise with the cancellation signal, and is very explicit about the source of cancellation. If you want to expose a promise to a consumer without cancellation, and instead support cancellation internally, you would do the following:

```js
// [api.js]
var rootCts = new CancellationTokenSource();
export function getRecord(id) {
  // cancellation not exposed to consumer
  return fetch(id, rootCts.token);
}
export function shutdown() {
  rootCts.cancel(); // cancellation only exposed to consumer via explicit entry point
}

// [consumer.js]
import { getRecord, shutdown } from 'consumer';
var fp1 = getRecord(...); // cannot cancel this individual request
var fp2 = getRecord(...);
shutdown(); // cancels all requests through explicit entry point
```

> A cancellation token is basically an ocap, and that means you have to
> keep track of the ocaps explicitly and separately from the promise for
> the result.  This means more value-passing, and when you return
> another cancellable promise in the callback (like
> `fetch(...).then(x=>fetch(...))`), you have to explicitly smuggle that
> cancellation token out of the callback and hold onto both of them.
> Combinators become annoying, as you have to grab *all* of the cancel
> tokens used and hold them together, etc.

That's what linking registrations are for, and you should only need to add cancellation to methods where you can explicitly cancel. You wouldn't need tokens for every single promise. Also, the token doesn't come "out of the callback", but rather is passed in from the caller:

```
var cts = new CancellationTokenSource();
var f = token => fetch(..., token).then(x => fetch(..., token));
f(cts.token);
```

> Attaching cancellation to the promise just provides more usable
> behavior overall, without preventing safe behavior when you desire it.

Adding cancellation directly to a Promise means that supporting safe behavior is harder than the default. 

Ron

Received on Monday, 2 March 2015 23:40:11 UTC