This repository has been archived by the owner. It is now read-only.

[timers] setImmediate executes after setTimeout #25788

Open
a0viedo opened this Issue Jul 31, 2015 · 6 comments

Comments

Projects
None yet
3 participants
@a0viedo
Member

a0viedo commented Jul 31, 2015

Is this a known issue? From the docs:

To schedule the "immediate" execution of callback after I/O events callbacks and before setTimeout and setInterval .

But this code

setTimeout(function(){console.log('timeout')}, 0)
setImmediate(function(){console.log('immediate')})

outputs first timeout in v0.12.5.

@joaocgreis

This comment has been minimized.

Member

joaocgreis commented Jul 31, 2015

This looks very similar to what is being discussed in #25607 and #25763

@joaocgreis joaocgreis added the timer label Jul 31, 2015

@misterdjules

This comment has been minimized.

misterdjules commented Aug 8, 2015

Thanks @a0viedo for taking the time to file an issue!

It's a duplicate of #6034.

What happens with the code you mentioned is that the timer and the immediate are added, and then the libuv event loop starts. When it starts, the libuv event loop first check for timers, and if the time between when a timer was added and when the event loop starts is greater than the timer's timeout value, then that timer will fire before any immediate.

The output is not consistent because it depends on the timing of these operations: sometimes the timer will fire before the immediate, sometimes it will be the other way around.

There are other ways this inconsistency could be reproduced. This issue has been present for a while (including in v0.10.x versions), and hasn't been introduced recently.

Maybe what the docs should highlight is that the guarantee is that setImmediate callbacks will be called before timers' callbacks only when they are scheduled within an I/O callback. That is in the case of:

some.asyncIO(function callback() {
  setImmediate(immediateCallback);
  setTimeout(timerCallback, 0);
})

immediateCallback will always be called before timerCallback.

However, scheduling the same operations differently doesn't guarantee any ordering between timer callbacks and immediate callbacks.

There are other guarantees made by immediates, such as the fact that immediates scheduled within another immediate are called on the next turn of the event loop. However, I'm not sure having a stronger contract than that would have any significant impact on users, so I would think that this doesn't need to be fixed in code.

Do you have a use case that this bug makes impossible to manage?

@a0viedo

This comment has been minimized.

Member

a0viedo commented Aug 13, 2015

Thanks @misterdjules! That gave a lot of insight on the use case. I think we could improve the docs to say the same you wrote here.

Another quick question, in the case of using process.nextTick it shouldn't execute the immediate callback first too? e.g.:

process.nextTick(function() {
  setTimeout(function() {
    console.log('timeout')
  }, 0)
  setImmediate(function() {
    console.log('immediate')
  })
});
@misterdjules

This comment has been minimized.

misterdjules commented Aug 13, 2015

@a0viedo The same problem arises in the example above (scheduling a timeout and an immediate in the callback of process.nextTick before the first tick of the event loop).

The reason is that when that nextTick callback is called, the libuv event loop hasn't started yet, so it's essentially the same test case as the one described originally as far as timers/immediate/nextTicks are concerned.

@a0viedo

This comment has been minimized.

Member

a0viedo commented Aug 14, 2015

I looked at #6034 and seems like it's not guaranteed that it will run always before:

@trevnorris wrote:

We don't guarantee that any differing set of asynchronous functions run before any other. The only guarantee is that in the case:

  setTimeout(function A() { }, 10);
  setTimeout(function B() { }, 20);

Function A() will run before B(). That's because they depend on the same internal queue. But anything through setImmediate() and setInterval() each have their own queue, and are not dependent on one another in terms of timing.

So I created assertImmediate.js:

var assert = require('assert');
var i;
setTimeout(function() {
  var called = false;

  function callback(caller) {
    if (called) {
      assert.equal(caller, 'timeout');
    } else {
      called = true;
      assert.equal(caller, 'immediate');
    }
  }
  setTimeout(callback.bind(null, 'timeout'), 0)
  setImmediate(callback.bind(null, 'immediate'))
});

And running that several times:

$ for i in `seq 50`; do node assertImmediate.js; done

will throw with some probability an AssertionError. Is that the expected behavior, that will have high chances of running before any other timer?

@misterdjules

This comment has been minimized.

misterdjules commented Aug 14, 2015

I looked at #6034 and seems like it's not guaranteed that it will run always before:

Exactly, and it's in line with my previous comments on this issue.

The assertImmediate.js script mentioned above is another edge case. It behaves as you expect with node v0.10.x, but a change in node v0.11.x (which is still in node v0.12.x) changed this behavior and makes your test fail.

Like I said before, I'm not sure there's any value in "fixing" these use cases, but any feedback on that is more than welcome.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.