-
Notifications
You must be signed in to change notification settings - Fork 19
No standard way to cancel promises / async operations #10
Comments
Just figured I would point out that I vehemently dislike the idea of promises being cancelable. Async operations should be cancelable, of course, but promises should only represent the observation of an event being completed, not some reverse hook to control it before it has finished. This topic has been debated at length, and any mechanism which allows for a promise to be used to signal a cancelation "back up the chain" will be opposed by many of us for all the same reasons that have been brought up. As for async functions being cancelable, I think cancelation tokens is how that should work. I built CAF to model an approach to that. |
I think that the focus here isn't whether or not ECMAScript or Node.js should provide cancellation or promise cancellation but rather whether or not we should allow for hooks that enable that. I'm very hesitant to even debate promise cancellation or cancel tokens - but I'm definitely in favor of adding hooks that enables tools to establish user-land solutions. |
For example, if I understand the request of @itaysabato correctly (not sure I do) - it would enable CAF to work with async functions. |
Adding hooks on async functions is fine. I was objecting to adding hooks to promises. :) Related: #11 |
Hi @getify, If I understand correctly, we both agree that:
What you disapprove of is:
I tend to agree with both points, but:
In any case, as @benjamingr said, these hooks may allow for "more things being cancellable" than you like, but by subsumption you can implement your flavor of cancellability without explicitly passing around tokens. There could be a few libraries, each implementing a different API for cancelling async operations and the market will decide for itself what is best. I want to stress this point again: just because we say we are "canceling a promise" doesn't mean that the promise itself is cancelled - which is semantically impossible - what we are really doing is "cancelling the async operation this promise observes (if possible) and ignoring its resolution anyway". |
I'm not sure if the critical point is being missed here, regarding "promise cancelation". What is vital to NOT allow is for there to be two observers of a promise, in ostensibly different parts of the application, and one of them gets to decide to "cancel" the original operation, thereby affecting the other observer without them knowing or agreeing. This is called action-at-a-distance and is almost universally considered poor design. If I have a promise, no one else should be able to affect my ability to observe it, except the originator of the operation. Observers are just observers, not manipulators. Moreover, the most critical part of a promise's design is that you be able to trust that it represents a reliable and immutable (once resolved) result of an operation. If you can't trust that because the promise itself exposes state mutators (like cancelation), then the promise is almost entirely useless. The majority reason for promises being useful is their trustability. |
I'd really rather not focus on whether or not we should do promise cancellation, what the right solution is or how to do it to be honest. I'd like to see what hooks Node.js or V8 can expose in order to allow users to better prototype solutions - ideally a hook would allow both CAF and bluebird like cancellation of async functions so these solutions can be prototyped in Node.js |
CAF is able to "cancel" an asynchronous operation because it actually uses generators, and thus is able to simply abort immediately with Of course, I'd love for regular async functions to be cancelable in that same way, but I can't envision any path where that happens, if for no other reason than that it would be backwards-incompatible for JS to ever start allowing that. |
I'm sorry to be such a pest on this topic -- I will leave it alone after I post this message -- but the reason I'm being so persistent about objecting to any hook that makes a promise cancelable is, I consider that such bad practice that I think it's irresponsible for any platform to even consider it. IMO, it's in the same boat as extending/overriding native prototypes. We've all known for more than a decade that this is terrible practice. No amount of, "but hey I really would like to do ____" overcomes that it's a terrible practice. The overall spirit of what you're exploring here -- what hooks can we provide to let users explore potential fixes/extensions -- is important and I'm thankful and supportive of it... but not all things that you CAN expose, SHOULD be exposed. Imagine someone asked, "Hey, could you provide a hook so I could modify the |
I am not shying away from that.
So basically - my goal in the summit at the end of the month isn't to provide solutions. I intend to:
"Cancellation" is one thing I can present, I wasn't planning to focus on it or ask for it but it looks like people care deeply - so I intend to try and explore what hooks we can expose. We do not intend to break ECMAScript semantics in either case or provide promise cancellation in Node.js. I want to bring hooks that will enable userland solutions to prototype things.
Your participation here is welcome, you are welcome to comment and discuss this for as long as you're willing and I promise to do my best to read everything carefully and bring it into consideration in the meeting. In fact, I would be very interested in what pains you have when users debug apps written with CAF and what we can do to give them a better user debugging story. |
Codefied in a way that does not imply a solution is preferable to another in https://github.com/nodejs/promise-use-cases/blob/master/use-cases/extras/2/extras-2.md and https://github.com/nodejs/promise-use-cases/blob/master/use-cases/extras/1/extras-1.md |
Cool! BTW as I was reading https://github.com/nodejs/promise-use-cases/blob/master/use-cases/extras/1/extras-1.md I thought a much much simpler API could also be very helpful in this respect: What if you could simply pass a promise returned by an async function to an API and get back a "controller" for that function? Then we can do something like: const p = someAsyncFunction()
const controller = magicApi(p)
return {cancel: () => controller.return(), p: Promise.resolve(p)} |
@itaysabato if you could write or add a document looking at different approaches hooks can provide in order to deal with the use case that would be appreciated. I am not looking at solutions yet but rather exposing capabilities. |
@benjamingr sure, do you mean discussing several options for hooks/APIs that could be used to solve the use case? |
@itaysabato yes - just to present them to V8 or Node |
@getify You said:
I'd be very interested to read this debate. I have used bluebird's ability to do exactly what you are opposing and found it a much clearer and more composable interface for cancellation than tokens. But I know many people more experienced that me don't like it. Could you point me in the right direction to read more on this please? |
Here's two very long threads where many discussions were had about cancelable promises (there were others but can't find them ATM):
Also, the proposal for cancelable promises repo had several issues in it that may be of interest. Finally, I'd suggest checking TC39 meeting notes for several meetings they had about it, where eventually there was enough push back that the cancelable-promise issue was dropped. TL;DR (those threads are really long)- My main objection to cancelable promises is that it creates action at a distance, which is generally held to be bad system design. If an async operation produces a promise, and five different parts of the application get a copy/reference to that promise, now there are five independent observers of the resolution of that operation. Let's say one of them decides it wants to "cancel" the original operation. Should they be able to unilaterally decide from that vantage point that none of the other 4 observers should get to observe the resolution? If so, that's action-at-a-distance. Put a different way, if I hand you a promise so you can observe my operation, am I really ok that I also handed you the ability to cancel my operation out from underneath me? A promise, at its core concept, is supposed to be an immutable and trustable time-independent representation of a future resolution/completion. Holding a reference to a promise and being able to unilaterally alter its state (by canceling it) is violating this principle, and fundamentally making promises less trustable. Two last points:
|
This is something that came up quite often also during the Node Collaborator Summit. Here's my (brief) take on this: It doesn't make sense to think of cancellation in terms of promises. Promises are placeholders for values, and you don't cancel values. Instead cancellation is something that makes sense for operations. |
@getify I would like to address the issue of multiple consumers, and how it is solved in Bluebird (and therefore not really an issue). First of all, the case of:
is very rare IMO. In the vast majority of cases, a promise has a single consumer - usually the initiator of the async operation which passes its own caller the same promise or a child thereof.
No. In Bluebird cancellation semantics, if multiple consumers depend on the same promise, a single consumer cannot cancel it - if only a subset of consumers tries to cancel (via a child promises) it will only be "unregistered", as you say. I agree that, semantically, "unregistration" and cancellation are two distinct abilities and that promises were not designed to act as controllers. Nevertheless, the way I see it, functionality trumps semantics and since in ~99% of practical use cases the initiator of the async operation, the consumer of the promise, the canceller of the operation and the "unregisterer" of the promise are all the same logical entity, the simplest most elegant solution that provides the best functionality is using promises as handles for cancellation. Since mechanisms as mentioned above can be put in place to protect against the edge cases of multiple consumers, etc. it is really only that feeling that "we're doing something wrong" that is holding us back. Semantics can, and should, be extended and changed over time, and that's fine - to me it doesn't feel "wrong". |
In short and summing up the discussion from there: you're all right and there is no incompatibility.
The problem is with the term "cancellation". Bluebird doesn't do promise "cancellation" it does promise "disinterest". In bluebird, you don't cancel promises, you express disinterest in them. When you "cancel" a promise all it does it communicate upstream that we are not interested in the value being passed. Whether or not any action was actually done about it somewhere (like cancelling an HTTP request) is transparent to the users. We are talking about two different things - cancellation of functions (which are actions) vs. expressing disinterest in promises (unsubscribing from them). (By the way - Kyle, promise "disinterest" is a little like your Anyway, I don't think this is the right place to discuss promise disinterest or async function cancellation - especially given how contentious this topic has been historically. What I'd love though is some discussion about how we can provide hooks for people to experiment with. I'd love it if there were 5 libraries out there that prototyped cancellation with async functions (of functions, promises or cats for that matter) so we can gather more data to present to the TC and to our own TSC when discussing cancellation. |
@itaysabato If multiple consumers share the same Promise, bluebird has no way to tell which consumer cancelled it (or that there are multiple consumers at all). As a result, one consumer can cancel the Promise for the other consumer. Bluebird's refcounting semantics are only sound if each individual Promise is used by a single consumer, and each consumer calls |
@not-an-aardvark you are correct. Like I said it is very rare to have the same promise ref spread in different parts of the code. It is very easy though to spread children of that promise instead of the original ref in order to avoid this. The point is that this is a mere technicality that can be solved in various ways. Like @benjamingr said it would be best if we had tools to experiment with cancellation over native async operations (i.e. without replacing native promises). |
That sounds like a good follow-up action item. I saw the Solutions Exploration from the comments above and I'm wondering what other proposals do we have about the hooks API that will allow for further user-land experiments. For instance are the moments when hooks are executed in the promise lifetime sufficient? |
@getify Thanks for the links. I will read up on it before weighing in further in the debate. For what it's worth, in my particular use case, I'm after "real" cancellation, not "unthen-ing". I want to abort the operation and get told when/if the abort is complete. i.e. cancel but maintain observation. @benjamingr I have some thoughts about the hooks required. I got most of the way through implementing abort-style cancellation for bluebird last year but gave up as I realised that the inexorable rise of async/await was going to render it obsolete pretty soon. But that work gave me a good idea of the mechanics required. Where would be best to put these thoughts? In this issue or elsewhere? |
We have three people here who have experimented with this (well, four if you include me) - @getify (with caf) @overlookmotel (with abortable) and @itaysabato (with the exploration proposal). If would be great if we could get you in a call/room/discussion to specify what hooks would be sufficient for exploration of these solutions. |
That would be very helpful definitely. I guess @bmeurer would like to join as well. |
I'd be happy to do a call/discussion, but would want to write it all up first anyway to make sure I have my thoughts all in order. One thing that's apparent is that we also all may have different opinions on what "cancellation" means - so that could cause crossed wires unless we've each specified that first. |
I think the purpose is explicitly not to solve the language-level problem but to instead enable hooks that would allow more userland experimentation with async/await. In fact, I think the fact you Itai and Kyle have a very different opinion on how cancellation should work is pretty great. |
In that spirit, just some sketches of possibility:
|
@getify These are good ideas and we can actually start implementing and experimenting with this kind of functionality right now by leveraging TypeScript's tslib module. For instance, as a workaround for making bluebird cancellations work with async functions, I've created this module that replaces the implementation of TypeScript's default |
And also no reasonable way to implement it in userland.
For instance, one can override native promises with Bluebird promises which expose an optional cancellation API, but this API does not inter-operate with modern async constructs such as async functions, generators and iterators.
IMO, this (not to mention the forgoing of native promises altogether for cancellation's sake) is a price too high to pay in the long run.
Suggestion
Without forcing the community to agree on a "right" way to implement cancellations, hooks can be provided to allow developers a way to standardize cancellations in their respective applications and in the way they see fit.
This reference implementation demonstrates a possible implementation of a "Bluebird-style" cancellation API on top of native promises via a promise interception hooks API that I drafted therein.
Both the implementation and the suggested interception API should be considered as a proof of concept that may be incomplete and possibly buggy.
Additional use cases
These kinds of interception hooks could prove useful for implementing additional use cases, e.g. for debugging tools etc.
I will add these additional use cases as separate issues that will reference this one.
The text was updated successfully, but these errors were encountered: