-
Notifications
You must be signed in to change notification settings - Fork 7.3k
timers: fix processing of nested same delay timers #25763
Conversation
/cc @misterdjules |
// Whenever a timer creates a new timer in the current list being processed | ||
// you can end up processing the new timer immediately instead of waiting | ||
// the scheduled time. Whenever we encounter this situation stop | ||
// processing this list and reschedule. (Issue 25607) |
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.
Could you put the full issue URL here? Thanks. :)
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.
Of course, consider it done.
a794864
to
3964aed
Compare
@@ -87,13 +87,20 @@ function listOnTimeout() { | |||
|
|||
var first; | |||
while (first = L.peek(list)) { | |||
// If the previous iteration caused a timer to be added, |
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.
We want to keep these comments because they are still relevant: we still need to update now
or use Timer.now()
to make sure timing computations are done correctly. However we may want to move these comments to were we use Timer.now()
right below.
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.
I am already using Timer.now()
to do the computation and since we didn't need to update now
, it's not used anywhere else and we quick return, I didn't think the previous comments made sense sticking around since we were no longer updating now
. Am I misunderstanding?
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.
Agreed, it's probably self-explanatory in the current state of this PR. Thank you for questioning my comment :) 👍
/cc @joyent/node-collaborators |
Added the P-1 label because this PR fixes a few flaky tests ( |
@whitlockjc I would actually suggest removing EDIT: My bad, |
@whitlockjc Thank you very much for submitting this PR. I left a few comments after a first review. This definitely looks like it's going in the right direction. Please let me know when my comments are addressed, and I'll do another review. |
@misterdjules I originally wrote this code on |
@whitlockjc Ok, that sounds good! |
3964aed
to
9bb2b67
Compare
@misterdjules I do believe I've addressed your above requests:
|
In the absence of I would suggest starting with making sure that with the following code:
I would add another variant with nested Then you can go crazy and add even more nesting, anything you can think of basically :) |
@whitlockjc Another test you can add too is something like the following:
I believe that would fail without the changes in this PR. |
@tunniclm Continuing the discussion regarding your latest comment here, because this way we can look at the changes that are being discussed.
Yes, that's the same conclusion @whitlockjc and I arrived at in this PR.
I don't think you missed anything, unless we both missed it (which is possible :) ).
My opinion is there are at least two things to consider:
For 1), the previous behavior of Node.js was definitely to schedule nested timers' callbacks in different ticks of the event loop, which is why As of 2), it seems that all browsers I have access to (Chrome, Firefox, Opera, Safari), with the following code:
consistently produce the following output:
The HTML spec also indicates that timers are queued in their own tasks queue (see item number 14). In other words, it seems that:
|
9bb2b67
to
976936c
Compare
As requested, I just updated the PR to include a unit test. I wrote this test against Note: This commit also includes a changes to |
@misterdjules ... did you have any further thoughts on this one? |
} | ||
debug(msecs + ' list wait because timer was added from another timer'); | ||
list.start(timeRemaining, 0); | ||
return; |
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.
Would it be better to just track if we are in a timeout and do the fix in like timer.active()
?
In general, touching the actual timeout loop seems pretty hairy.
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.
Let me wrap my head around this. I'm not sure what extra state, etc. would be required for your approach. Do you think the edge case, now <= timer._monotonicStartTime
would still be necessary to catch in listOnTimeout
. If so, it might not be worth doing thing differently as we'd still have the edge case to deal with.
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.
@Fishrock123 What did you have in mind more specifically? Some actual code would help.
I don't want this to drop through the cracks so can we update this with the next steps? Really hoping to either find out that this isn't necessary in the new node repository or be told what the next steps are. If it's still useful/important, I'll gladly port this to the new repository via a PR. /cc @nodejs/collaborators and @nodejs/tsc |
I'm mostly away right now (vacation) so ping me every few weeks until I am able to take a look at it, if no-one else does first. As a good fist step, could you move this to the nodejs/node repo? thanks! |
Done. I fixed the merge conflicts but the PR is primarily the same. I did move |
common.busyLoop(delay); | ||
|
||
process.nextTick(function() { | ||
assert.ok(!nestedCalled); |
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.
I'd suggest adding a comment that this assert
is done within a nextTick
callback so that it runs after A
completed, but before the next turn of libuv's event loop. Otherwise it's not clear for anyone but who wrote the code why it's done this way.
@whitlockjc This will look good to me once my latest two comments are addressed. @nodejs/tsc Is it actually a rule now to submit PRs against master before landing changes in v0.12/v0.10? If so that would be very tedious, since there's a lot of difference between some internal modules between v0.10/v0.12 and nodejs/master, and that would essentially mean forward-porting changes to nodejs/master and then back porting them again. So unless there's a real benefit in doing that that I missed, I would suggest just landing this PR in v0.10, merging it in v0.12 and then forward-porting it in nodejs/master with a separate PR. |
@misterdjules no need to add in master first where it doesn't make sense, sometimes things are just not going to belong in master at all and sometimes it's completely different code so separate PRs makes sense. So, no strict rules, don't jump through too many hoops if it's too complex, particularly for 0.10 and 0.12 which are far detached now. Just keep a record in the commit of what happened so when we're cherry-picking things that we know what's going on. |
@rvagg Sounds good. |
Whenever a timer with a specific timeout value creates a new timer with the same timeout, the newly added timer might be processed immediately in the same tick of the event loop instead of during the next tick of the event loop at the earliest. Fixes nodejs#25607
976936c
to
65de5d0
Compare
For posterity, I went ahead and updated this PR with the changes @misterdjules requested in nodejs/node#3063 |
Can this still be a problem if the clock in the process is not monotonically updated at some moment? For example when the computer hibernates and then wakes up or the system time being updated by NTP daemon? This problem petkaantonov/bluebird#1034 with bluebird promise looks to be related. |
To test, you could build node with this PR applied to see if it still happens: nodejs/node#3063 Long story short, the whole purpose of this PR is to make it so that a timer added within a timer gets fired on the next tick instead of the tick it was added. That being said, I'm not sure why that would cause the issue you're seeing but I'd be willing to look deeper. |
Thanks, I tried 0.10.41 which as I understand already has this PR applied and the issue with promises call stack still happens. Its hard to reproduce so I'm trying to narrow down the circumstances that can replicate it. |
I don't think it's been merged to any stream yet. Might tray a custom build. The easiest thing would be to clone my repo, checkout the |
Whenever a timer is scheduled within another timer, there are a few known issues that we are fixing: * Whenever the timer being scheduled has the same timeout value as the outer timer, the newly created timer can fire on the same tick of the event loop instead of during the next tick of the event loop * Whenever a timer is added in another timer's callback, its underlying timer handle will be started with a timeout that is actually incorrect This commit consists of nodejs/node-v0.x-archive#17203 and nodejs/node-v0.x-archive#25763. Fixes: nodejs/node-v0.x-archive#9333 Fixes: nodejs/node-v0.x-archive#15447 Fixes: nodejs/node-v0.x-archive#25607 Fixes: nodejs#5426 PR-URL: nodejs#3063
Whenever a timer is scheduled within another timer, there are a few known issues that we are fixing: * Whenever the timer being scheduled has the same timeout value as the outer timer, the newly created timer can fire on the same tick of the event loop instead of during the next tick of the event loop * Whenever a timer is added in another timer's callback, its underlying timer handle will be started with a timeout that is actually incorrect This commit consists of nodejs/node-v0.x-archive#17203 and nodejs/node-v0.x-archive#25763. Fixes: nodejs/node-v0.x-archive#9333 Fixes: nodejs/node-v0.x-archive#15447 Fixes: nodejs/node-v0.x-archive#25607 Fixes: #5426 PR-URL: #3063
Whenever a timer is scheduled within another timer, there are a few known issues that we are fixing: * Whenever the timer being scheduled has the same timeout value as the outer timer, the newly created timer can fire on the same tick of the event loop instead of during the next tick of the event loop * Whenever a timer is added in another timer's callback, its underlying timer handle will be started with a timeout that is actually incorrect This commit consists of nodejs/node-v0.x-archive#17203 and nodejs/node-v0.x-archive#25763. Fixes: nodejs/node-v0.x-archive#9333 Fixes: nodejs/node-v0.x-archive#15447 Fixes: nodejs/node-v0.x-archive#25607 Fixes: #5426 PR-URL: #3063
Whenever a timer is scheduled within another timer, there are a few known issues that we are fixing: * Whenever the timer being scheduled has the same timeout value as the outer timer, the newly created timer can fire on the same tick of the event loop instead of during the next tick of the event loop * Whenever a timer is added in another timer's callback, its underlying timer handle will be started with a timeout that is actually incorrect This commit consists of nodejs/node-v0.x-archive#17203 and nodejs/node-v0.x-archive#25763. Fixes: nodejs/node-v0.x-archive#9333 Fixes: nodejs/node-v0.x-archive#15447 Fixes: nodejs/node-v0.x-archive#25607 Fixes: #5426 PR-URL: #3063
Whenever a timer with a specific timeout value creates a new timer with
the same timeout, the newly added timer might be processed immediately
in the same tick of the event loop instead of during the next tick of
the event loop at the earliest.
Fixes #25607