tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
375 stars 29 forks source link

Are `await.cancelToken` implicit races actually bullet-proof? #66

Closed itaysabato closed 7 years ago

itaysabato commented 7 years ago

Originally discussed in #55 here.

Specifically, is it possible that after setting await.cancelToken and awaiting on a promise, an async function may proceed although a cancellation has already been requested?

Consider, for instance, the following code.

const {token, cancel} = CancelToken.source();
cancelable(token);

async function cancelable(token) {
    await.cancelToken = token;
    await notCancelable();
    // Should never throw if executed:
    token.throwIfRequested();
}

// Helper for notCancelable variations:
function delay(ms = 5000) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

And assume one of the following notCancelable implementations.

  1. function notCancelable() {
    delay().then(cancel);
    return delay();
    }
  2. function notCancelable() {
    const promise = delay();
    promise.then(cancel);
    return promise;
    }
  3. function notCancelable() {
    const promise = delay();
    promise.then(() => delay(1).then(cancel));
    return promise;
    }

Is it possible that in some execution, the last line of cancelable will be executed and throw? If so, I think the spec should mandate that this additional line will be executed implicitly if await.cancelToken is assigned with a token.

itaysabato commented 7 years ago

I found another example which seems to be guaranteed to always break, assuming the race as implemented exactly as demonstrated here:

async function cancelMeB(cancelToken) {
  doSyncThing();

  await Promise.race([
    doAsyncUncancelableThing1(),
    cancelToken.promise.then(c => { throw c; })
  ]);

  await Promise.race([
    doAsyncUncancelableThing2(),
    cancelToken.promise.then(c => { throw c; })
  ]);
}

Given this kind of implementation, consider the following.

async function broken(token) {
    await.cancelToken = token;
    const promise = token.promise.then(() => ());
    await promise;
    //Wouldn't this line always be executed and would always throw?
    token.throwIfRequested();
}

It is very contrived but it seems that promise will always be resolved after the cancellation was requested but before the cancellation was propagated to the "awaiter". Thus, the last line will always execute and always throw.