Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Allow auto-cancelation inside an async function at every await point #23

Closed
domenic opened this issue Jun 20, 2016 · 14 comments
Closed

Comments

@domenic
Copy link
Member

domenic commented Jun 20, 2016

I am extracting this from #19, which in general is about an entirely different proposal unrelated to this repo (and thus closed), but inspired an idea which could be useful for this repo.

The idea is to have syntax sugar for automatically inserting cancelToken.cancelIfRequested() after every await point inside an async function. Doing this implicitly has gotten pushback in the past, but doing it explicitly seems to make sense to me. The strawman is then that we would allow

async function pollForValue(bus, targetValue, cancelToken) {
  await.cancelToken = cancelToken;

  while (true) {
    const answer = await bus.read();

    if (answer === targetValue) {
      return;
    }

    await delay(1000, cancelToken);
  }
}

as sugar for

async function pollForValue(bus, targetValue, cancelToken) {
  while (true) {
    const answer = await bus.read();
    cancelToken.cancelIfRequested();

    if (answer === targetValue) {
      return;
    }

    await delay(1000, cancelToken);
    cancelToken.cancelIfRequested();
  }
}

(using a slightly-tweaked version of the motivating example for cancelIfRequested, see #16).

This may be similar to what @inikulin was asking for in #18.

/cc @wycats since I believe this might address some of his concerns.

@bergus
Copy link

bergus commented Jun 20, 2016

Two questions:

  • Did you forget the Promise.race() again or do you really only want to cancel the async function after the read or delay have settled?
  • What does .cancelIfRequested() return? A cancelled promise? Or does it not return but complete with an abrupt cancellation? (Do these things cross function boundaries now?)

@domenic
Copy link
Member Author

domenic commented Jun 20, 2016

Did you forget the Promise.race() again or do you really only want to cancel the async function after the read or delay have settled?

Promise.race seems irrelevant.

You can only cancel after the read or delay is settled anyway. Before that there's no way for code outside the function run since the async function itself is running.

What does .cancelIfRequested() return? A cancelled promise? Or does it not return but complete with an abrupt cancellation? (Do these things cross function boundaries now?)

Please read the spec... more like the latter.

@bergus
Copy link

bergus commented Jun 20, 2016

You can only cancel after the read or delay is settled anyway. Before that there's no way for code outside the function run since the async function itself is running.

I'm not sure what you mean. Let me explain with an example:

async function demo(cancelToken) {
  await.cancelToken = cancelToken;
  try {
    await delay(2000);
  } finally {
    console.log("done");
  }
}
console.log("start")
demo(new CancelToken(cancel => {
  delay(1000).then(() => {
    console.log("cancelling");
    cancel();
  });
}))

Will "done" be logged after one or two seconds? Clearly there is code running concurrently to the async function and the two-second delay.
I am assuming on purpose here that cancelToken is not passed to the delay(2000) call and that promise doesn't get cancelled.

@domenic
Copy link
Member Author

domenic commented Jun 20, 2016

Two seconds. Because here is what the VM does:

  • demo is called
  • await.cancelToken is set (irrelevant)
  • try is entered
  • delay() is called
  • await happens

NOW execution of the function is suspended for 2 seconds, and we can execute other code (such as console.log("cancelling"); cancel();. We cannot do so before this point, so it's pointless to insert an implicit cancelToken.cancelIfRequested() before the await delay(2000);.

And yes, cancel() will be executed at the one second mark. But the async function is suspended for 2 seconds, so there's no way for it to know about that. If you want to cancel during the suspension, you have to turn over the cancel token to the operation that causes the suspension, by doing await delay(2000, cancelToken).

(This is why it is so hard to find a good use case for cancelIfRequested() or await.cancelToken, by the way: it is almost always better to just pass the cancel token along to the child operation.)

@bergus
Copy link

bergus commented Jun 21, 2016

But the async function is suspended for 2 seconds, so there's no way for it to know about that.

Sure there is - that's what we registered the cancelToken for through await.cancelToken. We could desugar await x to

cancelToken.cancelIfRequested()
await Promise.race(x, cancelToken.promise.then(c => { cancel throw c; }));

which would cause "done" to be logged after only 1s in the above example.

Isn't that what we want, to break and run our finally clauses as early as possible after the token is cancelled?

@domenic
Copy link
Member Author

domenic commented Jun 21, 2016

That's an interesting proposal as well, thank you. Although Promise.race only takes one argument, and ignores canceled promises anyway, I think I understand what you're getting at.

@bergus
Copy link

bergus commented Jun 21, 2016

Ah, really should've studied https://domenic.github.io/cancelable-promise/#sec-promise.race better, I was under the mistaken assumption that cancellation worked like rejection there.

@bergus
Copy link

bergus commented Aug 11, 2016

Just a quick update, I've changed await.cancelOn (or whatever we name it) to a nullary/unary operator in my draft. I believe that's more attractive and signifies the special behaviour (influencing awaits around the whole function) better than a "property" with an assignment-like behaviour. And it's probably also easier to specify.

The best we could get from a clarity perspective might be a scope-like behaviour:

await.cancellation from cancelToken in {
    await ;
}

(but we would need to consider what to do with nesting, maybe even allow token composition through that)

@domenic
Copy link
Member Author

domenic commented Aug 11, 2016

The reason it's going to be specified as await.cancelToken = is because it fits with the existing meta-property concept used elsewhere. There is no dotted unary operator concept so we won't be introducing one.

@bergus
Copy link

bergus commented Aug 11, 2016

You sound like it's decided already?
The only meta properties I know about are new.target and function.first (in generators, I might misremember the name), and those act purely as "getters" - or plain keywords/nullary operators, rather. It seems that assignment to them introduces a new concept anyway, doesn't it?

@domenic
Copy link
Member Author

domenic commented Aug 11, 2016

Yeah, it was decided at the last TC39 meeting.

@bergus
Copy link

bergus commented Aug 11, 2016

Ah, I had interpreted the meeting notes as only pursuing this path further, not to settle on the exact syntax. Anyway, I should be glad my idea was adopted :-)

@bergus
Copy link

bergus commented Aug 11, 2016

Wow, thanks! I guess I should open seperate issues for any problems I see with that draft or whishes I have to extend it?

@domenic
Copy link
Member Author

domenic commented Aug 11, 2016

Yeah, that would be best.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants