5

I was reading about the node's event loop phases, and says that

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration.
  • idle, prepare: only used internally.
  • poll:retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
  • check:setImmediate() callbacks are invoked here.
  • close callbacks: some close callbacks, e.g. socket.on('close', ...).

So here i have a simple code to test some od the phases above. When you execute the code you get this ouput:

  1. close
  2. immediate
  3. timeout

But sockets callbacks as per doc, are last in the phases. Why it is executed first?

let socket = require("net").createServer();

socket.on("data", function (data) {
  console.log(data.toString());
});

socket.on("close", function (data) {
  console.log("close");
});

socket.listen(8080);

const fs = require("fs");

fs.readFile("readme.txt", () => {
  socket.close();
  setTimeout(() => {
    console.log("timeout");
  }, 0);

  setImmediate(() => {
    console.log("immediate");
  });
});
George Paouris
  • 525
  • 4
  • 16
  • Fully understanding this is almost a black hole and while you may be curious is really not necessary for anything but very detailed system programming (like implementing your own spec-compliant promise library). In general your code should not depend on these minute implementation details. If you require a specific sequence of events your code should just code that sequence rather than trying to rely on this level of system detail. – jfriend00 Apr 11 '20 at 20:09
  • Another thing you should know is that the event loop is a loop of checking various types of events. What comes after what also depends upon what type of event you start with in the loop. In your example, you're starting things off with a file I/O event so everything is relative to where you were in the loop when processing that type of event. Like I said, this is a bit of a black hole to try to fully understand it all. Prepare to spend days on the topic and very few people here fully understand it. – jfriend00 Apr 11 '20 at 20:10
  • FYI, keep in mind that `socket.close()` is an entirely local operation. The `close` event could even be synchronous. If not synchronous, then it will depend entirely upon which function the socket implementation chooses to delay it to the next cycle of the event loop (like whether they use something like `setImmediate()` or a promise or something else to delay it to the next cycle of the event loop. So, it's entirely implementation dependent (how the socket library chooses to send out the close event). This close event is not a networking event, it's a local event. – jfriend00 Apr 11 '20 at 20:14
  • "But sockets callbacks as per doc, are last in the phases." - As per what doc? Please link the resource. It's possible that the doc is wrong. The code of [`net.js` module](https://github.com/nodejs/node/blob/master/lib/net.js) appears to be using [`process.nextTick`](https://nodejs.org/api/all.html#process_process_nexttick_callback_args). As stated in the documentation, the `nextTick` will trigger after the current operation on the stack and before the event loop continues. This contradicts what is written in the doc you have mentioned. – Marko Gresak Apr 11 '20 at 20:16
  • Also as @jfriend00 said, do not create a code which relies on this. It's more of "how it works" and not a semantic guarantee. In node it's at least reliable because it's a single runtime, but it gets messy in the browser. This post explains just how messy it can get: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ – Marko Gresak Apr 11 '20 at 20:18
  • FYI, I stepped through your code in the debugger and stepped into `socket.close()` to try to see what the implementation did. It gets to this line [here](https://github.com/nodejs/node/blob/master/lib/net.js#L1591) where is calls `this._handle.close()`. That goes into some TCP wrapper layer written in C++ and the debugger won't let you trace into it to see further. That function then goes to call `emitCloseIfDrained()` which calls `defaultTriggerAsyncIdScope()` and is passed `process.nextTick()` as the asynchronous mechanism to use. – jfriend00 Apr 11 '20 at 20:28
  • So, it appears that it's using `process.nextTick()` for the `close` event which will give it a fairly high priority to get processed before other things. But, I hope you can now see how this is super implementation dependent (upon how the event is processed) and not documented so should not be relied up in your implementation. – jfriend00 Apr 11 '20 at 20:28
  • @MarkoGrešak The official docs are wrong? https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ – George Paouris Apr 11 '20 at 21:21
  • @GeorgePaouris I was not aware of what docs are you referring to. The linked documentation is indeed correct, but your interpretation is wrong. If you look at the chart, `close` connects to the top of stack. So in this regard, it means that it's the first thing to happen. In your example, `close` should be considered as the starting point and then the rest follows. It would be strange if `close` would wait for pending callbacks and poll before the pipe is actually closed. – Marko Gresak Apr 11 '20 at 22:40

1 Answers1

2

First off, keep in mind that the close event triggered by you closing the socket locally is not a network operation. It's not triggered by an incoming networking event. It's triggered by the local socket implementation deciding when to fire the close event to notify anyone else monitoring the local socket that it is now closed. It could even fire synchronously (if the net library that implements it chose to do so). So, anything you've read about how network events are prioritized or sequenced in the event loop does not apply here. This event is not triggered by an incoming network operation and doesn't flow through the event loop as an incoming networking operation would.

I stepped through your code in the debugger and stepped into socket.close() to try to see what the implementation did. It gets to this line here where is calls this._handle.close(). That goes into some TCP wrapper layer written in C++ and the debugger won't let you trace into it to see further.

That function then goes on to call emitCloseIfDrained() which calls defaultTriggerAsyncIdScope() and is passed process.nextTick() as the asynchronous mechanism to use.

So, it appears that calling socket.close() locally causes the socket library to use process.nextTick() before calling the close event which will give it a fairly high priority to get processed before other things, but will process on a future tick of the event loop.

But, I hope you can now see how this is super implementation dependent (upon how the event is triggered in whatever library is triggering it) and not documented so should not be relied up in your implementation. It's fine to want to understand as much of this as you can for intellectual curiosity purposes, but you should not design code that relies on this level of implementation detail. A particular feature like when the close event fires on a socket is not documented and there is no promise that it won't change in the future. If your code requires a particular sequencing of asynchronous events, you need to write your code to manage that sequence to be sure it happens regardless of this level of implementation detail.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • So do i trust the doc or not? https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ – George Paouris Apr 11 '20 at 21:25
  • @GeorgePaouris - Well, the part of that document relevant to the socket close event says that socket `close` callbacks will be emitted via `process.nextTick()` (if not an abnormal close). That seems accurate to me. There's a zillion things in that doc page so I don't what else you were questioning. – jfriend00 Apr 11 '20 at 21:28
  • @GeorgePaouris - But, in general, documents like that are called "guides" for a reason. They are trying to explain in general how things work. They are not the "bible" when it comes to a particular API implementation. For that you'd first look in the actual doc for that API and absent compelling detail there, you'd have to devise a test or examine the source. – jfriend00 Apr 11 '20 at 21:31
  • @GeorgePaouris - In open source projects like this with hundreds or thousands of volunteer contributors, if someone in the future changes the implementation of the `socket.close()` event, that person is likely not going to update this "guide" so it might take awhile for it to adapt. It would be part of the process of changing the source in a way that affects the specific API documentation for the documentation to get updated. Guides, not so much. – jfriend00 Apr 11 '20 at 21:32
  • Thank you so much for your answers. One last question. Does event loop give bigger priorities to callback timers? For instance lets say that a time callback has its timer completed (so its ready to be executed) and lets say node is in the poll phase. Will node skip all the phases (check, close callbacks) to go to timers? – George Paouris Apr 11 '20 at 21:42
  • @GeorgePaouris - No. timers have their spot in the event loop and they don't get to go to the front of the line. In fact, they are specifically behind other types of things like `.nextTick()` and promises. Because of the single threaded, event loop nature of timers in nodejs, they are not a precision timing tool. You would probably not implement high precision, real-time control of some hardware device in node.js Javascript, for example. Timers fire sometime after their expiration time when their normal turn arrives in the event loop. – jfriend00 Apr 11 '20 at 21:50