Skip to content
This repository has been archived by the owner on Mar 25, 2018. It is now read-only.

docs: improving documentation of setTimeout/setImmediate/process.nextTick #34

Closed
jasnell opened this issue Aug 15, 2015 · 18 comments
Closed

Comments

@jasnell
Copy link
Member

jasnell commented Aug 15, 2015

There are several outstanding issues in joyent/node requesting better documentation
of setTimeout, setImmediate, and process.nextTick. Opening this to consolidate the
reports and track status. Will add links to the original issues here, but closing those
original issues.

@drewfish
Copy link
Contributor

PR #24 is a topic on async programming, which seems a natural place to describe this.

@Qard
Copy link
Member

Qard commented Aug 15, 2015

I think it makes more sense for it to be reference. The information provided is specific to the behaviour of those functions.

@a0viedo
Copy link
Member

a0viedo commented Jan 18, 2016

I'm in favor of getting better docs for the highly misunderstood timers. process.nextTick doesn't seems to be replaceable by setImmediate simply because setImmediate doesn't have a deterministic behaviour when used with setTimeout but process.nextTick seems to do so. See nodejs/node-v0.x-archive#25788

cc @davepacheco

@davepacheco
Copy link

In what situations would it make sense to continue using process.nextTick() over setImmediate()?

@techjeffharris
Copy link

I watched a NodeSource "Need to Node" presentation hosted by @trevnorris called "Event Scheduling and the Node.js Event Loop" that very clearly explained the nuances and gotchas (setImmediate and nextTick should have their names swapped to be conceptually accurate, but they are likely not to change for a long time if ever due to historical precedent).

I'll put together a transcription of that presentation to help clarify the behaviors of the different timers.

@davepacheco
Copy link

I believe I understand the behavior (though more clarification is certainly helpful -- I tried to describe it for the official docs under nodejs/node-v0.x-archive#6725), but I'm not aware of any situations where nextTick() is required where the same behavior cannot be more clearly written with setImmediate(). A concrete example would be very helpful.

@eljefedelrodeodeljefe
Copy link
Contributor

@davepacheco would that then mean that the internal use of process.nextTick is historic? Because it's used quite often. If it's that easy core should remove those as a best practice, no?

@davepacheco
Copy link

Well, I may be wrong about that, and that's why I'm looking for cases where nextTick is considered necessary. But yes, I wonder if many instances of nextTick() could be replaced with setImmediate(). I suspect that would be more complicated than simply replacing the function call because in many cases the code as written today depends on the fact that such events are processed sooner. That doesn't mean nextTick() is actually necessary, just that it requires more significant refactoring than just replacing the function call.

I've also heard performance thrown out as a reason to prefer nextTick(), and that may be why it's used internally. But I'm not sure what data supports that, and I suspect that for most users of Node, the added complexity in their own code is not worth it.

@trevnorris
Copy link

@davepacheco At times it's necessary to allow a callback to run before the event loop proceeds. One example is to match user's expectations. Simple example:

var server = net.createServer();
server.on('connection', function(conn) { });

server.listen(8080);
server.on('listening', function() { });

Say that listen() is run at the beginning of the event loop, but the listening callback is placed in a setImmediate. Now, unless a hostname is passed binding to the port will happen immediately. Now for the event loop to proceed it must hit the polling phase, which means there is a non-zero chance that a connection could have been received. Allowing the connection event to be fired before the listening event.

@davepacheco
Copy link

@trevnorris Thanks for that example. In that case, all of the event management is handled in the "server" class, and it seems like that class could ensure that 'listening' has been emitted before emitting 'connection' (e.g., by checking a flag and queueing any 'connection' events, for example), right? All things being equal, that implementation would seem clearer to me than having two tiers of "immediate" events.

@trevnorris
Copy link

@davepacheco That logic would then need to be properly implemented across any class where that type of race condition can occur. Which, in node, is almost all of them. Instead, simply having a timing mechanism of "run after the call stack has unwound but before the event loop continues" is a simple way to queue callbacks that run asynchronously and prevent that race from occurring.

Also we're not recognizing the need for timing consistency. If I schedule a setImmediate from epoll then it will be the next thing to run. But if I schedule setImmediate from another setImmediate, or a timer, then both timers and I/O will be processed. (it is not a typo that timers are processed after timers; this is the case for using UV_RUN_ONCE which node uses).

To demonstrate how necessary a timing mechanism like nextTick() is, I'll demonstrate the same using v8's own API:

setImmediate(() => process._rawDebug('setImmediate'));

(function runner() {
  Promise.resolve(1).then(runner);
}());

setImmediate will never be printed. This is because Promises internally use a mechanism that operates fundamentally the same way as the nextTickQueue. They did this because the additional overhead of needing to wait until the event loop ran around was too great. This is also a case for node's usage.

@davepacheco
Copy link

Do you think this problem is unique to core code, where it's possible to perform actions like binding a new file descriptor synchronously as you described in your example? I've managed to never hit this writing my own code outside of core, but I wouldn't be surprised if this issue only happens when using the internal bindings that core uses. I'm also not sure what prevents the timing problem you describe from being recursive. Why is it never necessary to enqueue an event that will happen before the nextTick() handlers? Or before the handlers that happen before the nextTick handlers, and so on?

I'm not making any recommendations for core code because I haven't reviewed much of it. In all the cases I have seen, though, including your example, I felt the code was more clearly written with setImmediate() and explicitly coding the desired behavior, rather than relying on the subtle timing semantics of an interface like nextTick(). I don't think implementing the logic in multiple places is so bad, either. Common logic can be abstracted into common code. And as I said, I don't believe I've run into this, so I'm not sure it would be commonly needed outside core anyway.

But the reason we've gotten here is because many users have expressed a lot of confusion about this interface. I'm seriously doubtful that the existence of nextTick() as a public interface, either to implement those implicit semantics or for performance, is worth all that confusion.

@trevnorris
Copy link

@davepacheco

Do you think this problem is unique to core code, where it's possible to perform actions like binding a new file descriptor synchronously as you described in your example?

One more important usage is in error handling. In conjunction with the ideal of maintaining that all events are asynchronous, it's important that errors are reported before the event loop is allowed to continue.

Say, for example, your interface is working with a data stream. There is an error in the data stream that must be reported. The error passed to the event indicates that the data stream should be terminated. If that error has been propagated through setImmediate() then it's possible the data stream could have received additional data. Instead of having been closed "immediately".

Also if I'm writing a native module that creates an interface, which has the same timing constraints as creating a net server, in that it may happen immediately or it may not, for proper propagation of events it's important that the "connection", "listening", etc. event be emitted nextTick() style. While I could control that timing in my own logic, it's simpler to just use a built-in mechanism that does it for me.

I'm not saying that usage of nextTick() should be advocated. In fact I'd probably just simply put "if you're not sure whether you should use setImmediate() or nextTick() then you should use setImmediate()". I am saying that there are real use cases for it that a few people may need. So it should remain a public API.

@davepacheco
Copy link

@trevnorris That example is another case where ordering of events is important, but nextTick() is not strictly required to implement it. I don't believe it really is simpler to use it.

As for the action items at hand: I've submitted my suggested docs and weighed in a bunch on why, but obviously it's not my decision.

@trevnorris
Copy link

@davepacheco So it's easier to implement logic that handles proper event ordering than simply calling process.nextTick(fn)?

@techjeffharris
Copy link

Pushed an update to nodejs/node#4936, PTAL

@ajihyf
Copy link

ajihyf commented Nov 10, 2017

In the current documentation, it says if you move the two calls within an I/O cycle, the immediate callback is always executed first and gives a code snippet:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

I think it's the same if we put the two timers into another setTimeout callback(immediate in the check phase of the first event loop and timeout in the timer phase of the second event loop). But the code below doesn't work as expected:

setTimeout(() => { // timer a
  setTimeout(() => { // timer b
    console.log('timeout');
  }, 0);
  setImmediate(() => { // timer c
    console.log('immediate');
  });
}, 0);

The order in which the two timers inside timer-a are executed is non-deterministic. Did I misunderstand the event loop?

@Trott
Copy link
Member

Trott commented Mar 13, 2018

Closing as this repository is dormant and likely to be archived soon. If this is still an issue, feel free to open it as an issue on the main node repository.

@Trott Trott closed this as completed Mar 13, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants