-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lib: add Timer/Immediate to promisifyed setTimer/setImmediate #29521
Conversation
The current implementation of the promisified setTimeout and setImmediate does not make the Timeout or Immediate available, so they cannot be canceled or unreffed. The docs and code follow the pattern established by child_process - lib/child_process.js#L150-L171 - doc/api/child_process.md#L217-L222
7118c0e
to
c0367dc
Compare
Frankly, if you think you might ever need to do a cancel, then Promises are just the wrong abstraction. If you await such a thing, what is the expected result? An error? Cancelable promises don't exist so the base expectations are not even clear. |
I wrote this PR because I wrote some code like this
If you can't cancel the timeout then deadline will always resolve and throw an error, even when doWork completes first. With this proposal you can write
|
to be fair it doesn't really matter if it rejects, race will already have resolved. |
An alternative for implementing cancellation might be implementing the AbortController API by accepting an AbortSignal in |
Going the An alternative approach could be to attach an // In this option, the AbortController would abort all Promises
// created by the sleep function.
const sleep = util.promisify(setTimeout)
sleep(10)
sleep(20)
sleep.abort() // cancel both sleep promises
// or
sleep.abortController.abort() // cancel both sleep promises Or... const sleep = util.promisify(setTimeout)
const abortable1 = sleep(10)
const abortable2 = sleep(20)
abortable1.abort()
abortable2.abort() In the second example, the Promise returned by sleep would essentially implement the Obviously the second option has the downside of extending the Promise API which would cause problems down the road if TC39 decides to add it's own abort or cancel method. Bottom line, however, is that the promisified function should provide a way of allowing the user to discover if the Promise is abortable as opposed to the user just passing in an |
Since we implement |
that could work also but ends up with the requirement that let fn;
try {
fn = util.promisify(otherFn, abortController)
// cancelable
} catch {
// not cancelable
} vs. const fn = util.promisify(otherFn)
if (typeof fn.abort === 'function') {
// cancelable
} else {
// cancelable
} Stylistically I prefer the latter over the former. |
It matters if the |
This discussion makes me question the point of having custom util.promisify in the core library, instead of adding promise native functions to the libraries, like fs/promises. Then users don't have to question if a util.promisify'ed function supports abort controllers or not. That said, users know if util.promisify supports an abort controller by reading the documentation, or thru static types.
child_process has a better use case for exposing the |
We could make Timeout objects "thenable", where an error in the callback bubbles to a const deadline = util.promisify(setTimeout)(TIMEOUT);
deadline.then(() => {
throw Error('Task took too long');
});
const task = doWork();
promise.race([task, deadline]); Then likely becomes const deadline = setTimeout(TIMEOUT, () => {
throw Error('Task took too long');
});
const task = doWork();
promise.race([task, deadline]).finally(() => {
clearTimeout(deadline);
}) |
This comment has been minimized.
This comment has been minimized.
+1 to that @Fishrock123 ... Making the timeout object a thenable makes a lot of sense. The on clearTimeout(), the thenable can reject if it hasn't already resolved. Makes for a nice elegant solution. Just thinking about async foo() {
const interval = setInterval(fn, 1000)
for await (const m of interval) {
/* ... */
clearInterval(interval) // would end the iterator...
}
) |
@everett1992 yes, I will correct that. Doing that also makes the async iterator story more streamlined, since both could just be directly on the timeout object. |
@nodejs/open-standards btw |
would be nice to see if its possible to say in sync with the dom too: whatwg/html#617 |
The dom has a bunch of problem which we frankly don't have (because someone had a good idea and returned a timer object rather than a number). I don't think we should follow them, it's very hard to trust a good api design to come out of that, considering what the dom wants compared to us. |
@Fishrock123 i'm not saying we have to, but i think we should at least explore it. I'm planning to work on the dom api some time in the nearish future, so it shouldn't be too hard to keep node in the loop. |
for what it's worth the DOM issue discusses adding a new function, not extending setTimeout. This feels like an opportunity to design a good api for dom and node. |
I have written my thoughts into a somewhat long post over on the whatwg thread: whatwg/html#617 (comment) |
I'm in favor of using thenables and an async iterators. |
Looks like the response on the other thread @Fishrock123 points to is that the whatwg approach is going to use AbortController no matter what. We can go with that approach also as an option, but it will be difficult to implement consistently, so I'm definitely still in favor of making the object returned by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for doing this and for taking a stab at it. I agree with the other comments that this is rather footfun-y :/
@jasnell there is also work at tc39 to standardize a cancellation api that would drop in with AbortController and could therefore interoperate with node. All I'm asking is that we slow down a little bit here :) I'm going to be working on async dom timers at some point in the coming months and as someone who also maintains node I'm inclined to at least try to match up the apis. |
@devnek ... no worries on being patient on it. I'm hesitant to do anything on cancelable promises until there's an official direction on it anyway. That said, would there be harm in experimenting with a couple of different approaches if those are clearly marked experimental? For instance, code such as the following could easily emit a process warning... const sleep = setTimeout(fn, 100);
sleep.catch( /* ... */ )
clearTimeout(sleep) |
@devsnek Fwiw, as noted in my post to whatwg, cancellation does not cover all use cases for persistents. |
I would be fine however with supporting |
@Fishrock123 just to be clear, we cannot:
That would break with async functions which always return a native promise, the proposal to let us do that was rejected 3 years ago in tc39 (compositional functions). |
I would strongly suggest not supporting AbortController outside of fetch; it's not a great name, and it doesn't imo fit with Promises in a general sense. |
@benjamingr I do not understand what you are referring to. I have a patch in progress which appears to work fine -- do you mean having the timeout callback be an async function? (That would be a separate issue...?) |
@ljharb You may want to leave that feedback on whatwg/html#617 or else whatwg will likely implement it for other apis too. |
They’ll likely do it regardless of the feedback they get. |
@Fishrock123 i think Benjamin is referring to something like this: async function x() {
return setTimeout(() => {}, 10);
}
x().then((timer) => {
clearTimeout(timer);
}); |
@ljharb we should strive to do the same thing as the web platform and standard suggest rather than roll our own abstractions IMO. If the web platform has settled on AbortController that is what we should do in my opinion. Otherwise we're going against the web platform.
There was a proposal ( https://github.com/jhusain/compositional-functions ) to make it possible to hook into await but it did not pass. We can expose an async iterator for setInterval just fine :] @jasnell - that would not work - util.promisify returns a function which means that the AbortController would have to be passed on the returned function rather than on the util.promisify call no? |
Also - I am -1 on using That said, deep down I still believe that cancellable promises like other languages and ecosystems do are the correct solution to this problem. I do not want to pursue that because that is not the direction TC39 is giving us so I recommend we stick to the standards. |
And just to clarify - I am +1 on an async iterator for |
@benjamingr the language still has an active proposal for general promise cancellation; I’m suggesting waiting for that, even if it takes awhile. |
As previously noted, making the promise cancellable only solves one of the issues. |
Link? |
Oh, that one - if I understand correctly that proposal is both inactive (stage 1 for years) and intends to be compatible (interop wise not api wise) with AbortController. The bigger issue with AbortController is that it depends on Pinging @rbuckton - are you still pursuing that proposal? |
Yes, I just haven't had much time to spend on it lately. |
@rbuckton do you know when you will be presenting it next? |
@benjamingr I'm focusing on advancing two of my other proposals at the moment. It likely won't be until next year that I can focus on the proposal again. Currently the proposal is focused on a "protocol" based approach using a Symbol-based API (similar to iteration), rather than an actual cancellation token implementation. The goal is that the ECMAScript spec would specify the protocol, which could be added to the existing AbortController/AbortSignal implementation, and that library authors and other runtimes (such as NodeJS) could also implement the protocol without needing to depend on AbortController (and thus EventTarget). However, since there would be no actual implementation in the ECMAScript spec, there need to be compelling use cases in ECMAScript spec for such a feature. The upside of a protocol-based approach is that a library author could accept cancellation signals by depending on the protocol rather than needing to feature test for You can see an example of this protocol implemented in userland in |
@rbuckton what do you think Node.js should do with its cancellation? We have the following options:
Given that the current proposal is stage 1 and work likely won't happen on it for another year at which point it has to go through a risky process - what should Node.js do? If we decide this is a priority for Node.js is there any way for our own TC39 representation (assuming they are interested) to help? (and to be clear that is fair, you owe us nothing and I am not complaining and am thankful for your work) |
Note: the choice we make here will likely have implications on other non-web apis that node ships too. |
Is there any progress? |
So, Rust's The answer is: have the timeout Promise wrap (be chained from) the Promise which could "cancel" it. If the upstream needs to cancel, it can just resolve the promise, which should be able to cancel the timer promise's internal timer. |
8ae28ff
to
2935f72
Compare
The current implementation of the promisified setTimeout and
setImmediate does not make the Timeout or Immediate available, so they
cannot be canceled or unreffed.
The docs and code follow the pattern established by child_process
There are two issues with this implementation
timeout
orimmediate
properties that exist or may exist on other promise implementations or future promise implementations. Looks like bluebird promises include a.timeout
method. http://bluebirdjs.com/docs/api/timeout.htmlclearTimeout()
orclearImmediate()
properties that clears the timer and rejects the promise. I can't currently think of a use case for calling unref, unref, or refresh on a promised timer so it may not be necessary to expose the whole timer instance.Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes