W3C home > Mailing lists > Public > public-script-coord@w3.org > January to March 2015

Re: Cancellation architectural observations

From: Tab Atkins Jr. <jackalmage@gmail.com>
Date: Mon, 2 Mar 2015 16:04:15 -0800
Message-ID: <CAAWBYDBLBw3c1O6F6C6=mqwu7ymcFp6DW_uBubpVFVk9yR4Xzg@mail.gmail.com>
To: Ron Buckton <rbuckton@chronicles.org>
Cc: Dean Tribble <tribble@e-dean.com>, "public-script-coord@w3.org" <public-script-coord@w3.org>, es-discuss <es-discuss@mozilla.org>
On Mon, Mar 2, 2015 at 3:39 PM, Ron Buckton <rbuckton@chronicles.org> wrote:
> 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?

Yes, you do want configPromise to cancel there.  It's the common case.
If you dont' want that to happen, produce all your chained promises
*first*; if you can't do that, chain a dummy promise off of it (to
increase the ref count) and then cancel it when you're done producing
chained promises.  That'll ensure configPromise stays alive until
you're done chaining things off of it.

>> 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
> ```

That works if you produced the source cancelables, and had the ability
to initialize them with the same cancel token.  If you didn't, and had
their individual cancel tokens passed in a side channel, it's much
more frustrating, as you have to explicitly track all of their tokens
together and then cancel all of them.

This doesn't seem complicated when you're just looking at simple
examples like this, but it's extra information that quickly gets out
of hand when you're working on real-world code.

> 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:

Yeah, you just don't hand the consumer the cancellation token, that's easy.

> 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:

Sorry, I didn't provide an example, so you misunderstand what I'm
referring to.  I mean in the case of something like:

```
let p = somePromiseFunc().then(x=>fetch(...));
```

If you want that fetch to be cancellable, you need to explicitly
create a token outside to pass in:

```
let ct = new CancelToken();
let p = somePromiseFunc().then(x=>fetch(..., ct));
```

Which, again, doesn't seem like any big burden when you're looking at
trivial isolated examples, but is that much more state you have to
carry around for every single fetch() you do.  Imagine you were
kicking off a variable number of fetches, for example - then you need
to create an array on the outside, and create your cancel tokens on
the inside and push them into the array to exfiltrate them.  This kind
of exfiltration is annoying when you're trying to extract the
resolve/reject functions from the Promise constructor, but that's
fairly rare; requiring it for common cases of fetch() usage makes it
obnoxious.

Whereas with a promise subclass, you do have to know that you want it
to be cancellable, but then you write:

```
let p = FetchPromise.resolve(somePromiseFunc()).then(x=>fetch(...));
```

You need to upgrade the vanilla promise into a cancellable promise,
but then chaining will work - if you cancel p, it'll cancel the
fetch() too, without having to keep track of anything further.

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

Agreed, and it's not ideal, but it's not an automatic tradeoff.  If
the safe behavior is significantly less usable, it can still be worth
it to default to unsafe, as long as switching to safe is fairly easy
(and it is).

~TJ
Received on Tuesday, 3 March 2015 00:05:03 UTC

This archive was generated by hypermail 2.3.1 : Tuesday, 3 March 2015 00:05:04 UTC