asked    Basil     2018-10-16       javascript       293 view        2 Answers

[SOLVED] Node.js - are setTimeout callbacks fired in the same order as registered?

I was wondering whether node.js guarantees the execution order of "expired" (ready to be executed) callbacks scheduled via setTimeout. The manual seems to claim that the Timers phase of the event loop has a FIFO queue of callbacks.

Taking this into account in the example below, I expected that node schedules the first callback and after 1 second the remaining two in the order as specified in the code. Now, when the first callback fires, the execution "stops" for 5 seconds which means that when the callback returns, the other two are ready to be executed as well.

However, when I run the example, the output seems to be first, third, second. Strangely, when the delay time of the second callback is modified to, e.g., 2001 instead of 2000, the order is as expected, i.e., first, second, third. Is this behavior by design?

const spawnSync = require('child_process').spawnSync;

function wait(delta){
    spawnSync('sleep', [delta]);
}

setTimeout(() => {
    console.log('first');
    wait(5);
}, 2000);

wait(1);

setTimeout(() => {
    console.log('second');
}, 2000);

setTimeout(() => {
    console.log('third');
}, 4000);

  2 Answers  

        answered    Darnell     2018-10-16      

From the docs

The timeout interval that is set cannot be relied upon to execute after that exact number of milliseconds. This is because other executing code that blocks or holds onto the event loop will push the execution of the timeout back. The only guarantee is that the timeout will not execute sooner than the declared timeout interval.

I think your example is demonstrating all these points, moreso the one around the fact that timers can be pushed back. It would appear to me that the calls to spawnSync are delaying the timers just enough to cause the overlap - presumably if you reduced the delay of the wait call in the first setInterval or increase the timeout of the last setInterval you’d see a more consistent behaviour.



        answered    Jerry     2018-10-16      

After a closer inspection of the callback scheduling implementation in Node, it does indeed seem that the order of the second/third callbacks in the posted example is not really guaranteed.

The reason for this is how Node handles setTimeout callbacks in lib/timers.js. In short, these callbacks are stored in linked lists grouped by their corresponding delay time. Now, if one callback fires, Node determines its group, marks current time t_now and processes all callbacks within the group. For each one, it knows its registration time t_reg (when it was registered via setTimeout) and delay time delta. If t_now - t_reg >= delta, it invokes the callback. The thing is that t_now is evaluated only once for the entire group of callbacks corresponding to the same delay time.

To illustrate this, I compiled Node in debug mode (--debug option of ./configure script) and executed the example as:

NODE_DEBUG=timer node ./example.js

On my machine, I get the following:

TIMER 38963: no 2000 list was found in insert, creating a new one
TIMER 38963: no 4000 list was found in insert, creating a new one
TIMER 38963: timeout callback 2000
TIMER 38963: now: 2069
TIMER 38963:    _idleStart = 64
first
TIMER 38963:    _idleStart = 1074
TIMER 38963: 2000 list wait because diff is 995
TIMER 38963: timeout callback 4000
TIMER 38963: now: 7075
TIMER 38963:    _idleStart = 1074
third
TIMER 38963: 4000 list empty
TIMER 38963: timeout callback 2000
TIMER 38963: now: 7076
TIMER 38963:    _idleStart = 1074
second
TIMER 38963: 2000 list empty

Here, we see that the first and second callbacks were scheduled at times t1=64 and t2=1074, respectively. When the first callback is ready to fire at time T=2069, it takes more or less 5 seconds to execute it. Once this execution finishes, Node continues within the same group of callbacks (i.e., callbacks associated with the same delay time) thus checking the second callback. However, it takes into account as current time not the time after the execution of the first callback but the time T when it started processing the callbacks (entered the listOnTimeout function in lib/timers.js). On my machine, since the second callback was registered at time 1074, 2069 - 1074 is less than the delay time of 2000 and thus the second callback is not executed but rescheduled instead for later with delay time of 995 (however, with respect to the current time, not with respect to T).

To be more specific, since the first callback fires, we know that T - t1 >= delta. However, the relationship between T - t2 and delta is not guaranteed. To illustrate this, if I remove the wait(1) call, I get:

TIMER 39048: no 2000 list was found in insert, creating a new one
TIMER 39048: no 4000 list was found in insert, creating a new one
TIMER 39048: timeout callback 2000
TIMER 39048: now: 2067
TIMER 39048:    _idleStart = 66
first
TIMER 39048:    _idleStart = 67
second
TIMER 39048: 2000 list empty
TIMER 39048: timeout callback 4000
TIMER 39048: now: 7080
TIMER 39048:    _idleStart = 67
third
TIMER 39048: 4000 list empty

So now, the second callback is scheduled at t2=67, i.e., 2 units of time later than the first callback. Now when first fires at time T=2067, Node processes the entire group of callbacks associated with delay time of 2000 as mentioned above thus proceeding to the second callback - in this case, T-t2 is exactly equal to 2000 so that second is "luckily" fired as well. However, were the scheduling of the second callback delayed by mere 1 unit (due to for example some extraneous function call before invoking setTimeout), the third callback would fire sooner.





Your Answer





 2018-10-16         Yvette

Implementations for event loop in C/C++ that's nice on the call stack

TL;DRWhat is the best implementation for an event loop in C/C++ that manages the call stack well?Why?Benchmarks/Comparisons?Literature/Publications/Documentation?Event Loops, the ProblemThey're an easy concept: Wait for an event, handle event, wait for more events.I was looking back over some old projects and came across a simple (and kinda poor) implementation of a search engine, and my curiosity was sparked about the proper way to do event loops.At the time I'd done something like this (very) simplified example:int wait_for_query();int handle_query();int main(int argc, co...
 c++                     2 answers                     65 view
 2018-10-16         Sidney

Detect whether current thread is main thread of the libuv default event loop

I'm writing a piece of C code for Node.js and want to distinguish synchroneous from asynchroneous calls. In other words, I want to detect whether my code is running on the V8 event dispatch thread, called from within the main event loop, or whether it's called from some separate worker thread. In the former case, I could call back to JavaScript immediately, while in the latter I'd have to use a more complicated async callback.The libuv threading API provides uv_thread_self to identify the current thread, and uv_thread_equal to compare threads for equality. So all I need is ...
 c                     2 answers                     63 view
 2018-10-16         Ella

How to make a proper Event Loop (or whatever it is called)?

I am not a programmer or any of such, I am only someone interested in this stuff.I have been trying to make myself an Xbox 360 Controller Mapper, translating controller key presses into simulated keyboard key presses. Reason is that I sincerely dislike the ones out there and one has to pay to get most of them. Promise, if I ever make it good enough that I like what I've produced, I will distribute it for everyone's use for free.Anyway, I already have made a working command-line version, yet to be as friendly as I want it to be, but still does better what most would want fro...
 c                     1 answers                     65 view