-
Notifications
You must be signed in to change notification settings - Fork 30k
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
stream: fix sync callback leak #31765
Conversation
618a737
to
25dedba
Compare
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.
Unfortunately, this approach would make adding this feature extremely hard in readable-stream. I’m -1.
The only other way I can think of is to fallback to always using Should we go the documentation route then? i.e. explicitly explain that the callback must be called inside |
@mcollina Your concern is definitely valid, but do you have other suggestions, other than just documenting the current state? I do feel like this is a bug worth fixing… |
I think the solution in 4d7eddc is perfectly feasible and solves the problem at a cost of an additional I'm open to other fixes, but I'm strongly -1 in adding a feature that cannot be ported to readable-stream easily. |
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.
Also this solution incurs some significant performance regressions:
confidence improvement accuracy (*) (**) (***)
streams/writable-manywrites.js callback='no' writev='no' sync='no' n=2000000 *** -28.95 % ±1.50% ±1.99% ±2.59%
streams/writable-manywrites.js callback='no' writev='no' sync='yes' n=2000000 *** -30.11 % ±3.86% ±5.18% ±6.82%
streams/writable-manywrites.js callback='no' writev='yes' sync='no' n=2000000 *** -16.49 % ±3.07% ±4.10% ±5.39%
streams/writable-manywrites.js callback='no' writev='yes' sync='yes' n=2000000 *** -31.43 % ±2.28% ±3.04% ±3.99%
streams/writable-manywrites.js callback='yes' writev='no' sync='no' n=2000000 *** -28.67 % ±1.74% ±2.31% ±3.01%
streams/writable-manywrites.js callback='yes' writev='no' sync='yes' n=2000000 *** -32.39 % ±2.74% ±3.65% ±4.76%
streams/writable-manywrites.js callback='yes' writev='yes' sync='no' n=2000000 *** -16.59 % ±3.40% ±4.56% ±6.00%
streams/writable-manywrites.js callback='yes' writev='yes' sync='yes' n=2000000 *** -30.27 % ±2.81% ±3.75% ±4.92%
Yes, that's expected. We should inline the |
This doesn't actually fix the root problem though. It just fixes one of the edge cases with the |
@@ -70,7 +71,7 @@ function processTicksAndRejections() { | |||
let tock; | |||
do { | |||
while (tock = queue.shift()) { | |||
const asyncId = tock[async_id_symbol]; | |||
const asyncId = tickId = tock[async_id_symbol]; |
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.
This might not be exactly correct since it will only run if there is a nextTick
scheduled and won't take other possible async entry points into account such as IO and what not. This is not my area of expertise.
Maybe would require some help from @addaleax if this approach is deemed worth looking further into.
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.
Yeah, I’m happy to investigate this in more detail… It looks like you’re essentially going for an id for each individual synchronous block of JS execution, which should be doable, but I agree that it makes sense to get a green light for this approach first :)
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.
btw, fwiw I've been mulling over the details of a proposal to TC-39 that does precisely that: assign execution and trigger IDs for every JS execution at the language level. It would make several things significantly easier.
I'm not convinced that this PR is the way to go forward tho and definitely need to give it a bit more thought.
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.
Hm yeah … I’m not sure if this would work fully when using async_hooks
because we’d basically always want the lowest async id on the stack, not the highest. But maybe we could expose + use that? That would also solve the readable-stream issue for all supported versions of Node.js (although obviously not browsers…)
Clarifies a userland invariant until a better solution can be found. Also moves a misplaced sentence from _write to write. Refs: nodejs#31756 Refs: nodejs#31765
Maybe @lpinca has some input/ideas on this? |
ObjectDefineProperties(WritableState.prototype, { | ||
sync: { | ||
get() { | ||
return this.tick === process.tickId; |
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.
@mcollina: one way to get around the readable-stream
problem could be:
process.tickId === undefined || this.tick === process.tickId
The downside would be that it would be slower for sync streams (but probably correct?) on old node versions, i.e . it would always think it is "sync" and use nextTick
even though it is not strictly necessary.
We could wait with backporting this to readable-stream until v14 becomes LTS?
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.
readable-stream has to work on old version of Node.js as well, as long as browsers. I would prefer to avoid this completely.
Clarifies a userland invariant until a better solution can be found. Also moves a misplaced sentence from _write to write. Refs: #31756 Refs: #31765 PR-URL: #31812 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
I think the consensus was to document the limitation since this cannot be easily ported to |
This PR currently is mostly a suggestion to discuss related to #31756. It's not properly cleaned up and is mostly to illustrate the problem and possible solution.
Today when working with callbacks and events we need to keep in mind ordering and whether or not a callback is invoked synchronously or asynchronously.
The current pattern for this is something like:
Which mostly works fine. However, as in #31756, if the callback is leaked outside it can still be invoked in the same tick breaking the assumption that when
state.sync === false
a tick has elapsed.i.e.
This PR tries to resolve this by keeping track of which tick we are currently in and making sure that
state.sync === false
always is a different tick.Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes