Reactor pattern, libuv and Node.js

The magic behind the non-blocking nature of Node.js.

Posted by Krzysztof Zbiciński on 2018-12-07.

Being able to perform non-blocking I/O operations was one of the most vibrant selling points of Node.js back in the days. This article provides an introduction to the reactor pattern, the role of the libuv library and how they both empower Node.js to deal with multitasking pretty much as cool as Xabi Alonso does:

What is the reactor pattern?

The reactor pattern is one of the most commonly used patterns in web development. There is a pretty good chance that you are very familiar with it without even realizing it. Simply speaking, the application starts listening for an I/O event of a specified type, providing a function that should be launched as a 'reaction' to this event. Sounds exactly like JavaScript callbacks, isn't it? ;)

Thanks to this pattern we can use non-blocking I/O operations, which usually can take some time when being done in a classical, synchronous way (e.g. a database query, an HTTP request, reading a file).

When some part of code requests an I/O operation, the request is put in an Event Queue within an Event Demultiplexer. Then the control is immediatley returned back to the developer's code. Thanks to this the application doesn't freeze and can immediately continue with other operations (e.g. performing calculations, listening for UI events and responding to user's input, etc.).

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

// waits until the whole file contents are loaded
const bigFile1 = fs.readFileSync('bigFile.txt');

const bigFile2 = null;
// requests to load file contents, provides a handler and immediately proceeds with code execution
fs.readFile('bigFile.txt', function handler(err, data) {
bigFile2 = data;
});

In the meantime, the so-called Event Loop processes the events stored in the Event Queue. It iterates through them and invokes the associated functions (callbacks or handlers) when ready.

Let's see the example pseudocode implementation of an Event Loop taken from the book An Introduction to libuv by Nikhil Marathe:

1
2
3
4
while there are still events to process:
e = get the next event
if there is a callback associated with e:
call the callback

Each of the operating systems (Linux, Mac OSX, Windows) has its own implementation of an Event Demultiplexer. Therefore an abstraction layer above those implementations had to be introduced.

libuv to the rescue

In the early days of Node.js a library called libev was used to handle abstraction above Mac OSX and Linux implementations of Event Demultiplexers (kqueue and (e)poll). As Node.js grew in popularity, this library became insufficient, because support for Windows was demanded as well.

Then the famous libuv was introduced, which covered it all (including Windows implementation: IOCP). It takes the responsibility for collecting events from the operating system or monitoring other sources of events. While being initially developed mostly with Node.js in mind, the libuv library is now widely used by different projects, including Mozilla's Rust programming language.

How it all fits together?

I think that the best idea is to show it on a diagram. The architecture of a Node.js application can be presented as follows:

s architecture

  • Application code: self explanatory ;),
  • Node.js core: (also called node-core) is a JavaScript implementation of Node.js APIs,
  • Bindings: wrap and expose libuv and other low level functionality to JavaScript,
  • V8: execution engine developed initially for Google Chrome,
  • libuv: abstraction over Event Demultiplexer of a hosting OS.

Final thoughts

I hope this short introduction is a good starting point of learning about the inner mechanisms that make Node.js environment work. We've only scratched the surface, so if you're interested in more detailed information, I can suggest going through the references as the next step.

References

Photo by Frédéric Paulussen on Unsplash.


Comments: