- Published on
Reactor Pattern in Node.js
A little about Node.js
Node.js is an asynchronous event-driven JavaScript runtime. It operates on a single-threaded event loop, using non-blocking I/O calls which allows it to support many concurrent connections without incurring the cost of context switching. This is in contrast to a more common concurrency model, where each connection is handled by a relatively heavyweight OS process or thread.
Node.js uses libuv as an abstraction layer over different I/O polling mechanisms, cross-platform file I/O, threading functionality, and much more for its event-driven asynchronous I/O model. Take a look at its feature list and see what this unicorn is all about.
Reactor Pattern
Quoting from the original paper which describes this concurrency pattern,
The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consist of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer.
from Reactor - An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events, 1995, insightful and very interesting read.
The key components that participate in the Reactor patterns are as follows:
- Handles
- unique identifiers for resources managed by the operating system.
- for example, file descriptors (
fd) in Linux and file handles in Windows.
- Synchronous Event Demultiplexer
- blocks and awaits for events to happen on a set of Handles. returns when it is possible to initiate an operation on a Handle without blocking.
- for example,
select,poll,epollin Unix,kqueuein OSX and other BSDs, andIOCPon Windows.
- Initiation Dispatcher
- defines the interface for registering, removing, and dispatching Event Handlers.
- uses Synchronous Event Demultiplexer to detect new events for the Handles.
- some common events include connection acceptance events, data input, data output, and timeout.
- Event Handler
- defines interface specifying the hook event handler method. It is implemented by application code.
- Event Handler Concrete
- implements the hook method as well as other helper methods needed to handle the event.
The above components function together as follows,
- An application registers Concrete Event Handler with the Initiation Dispatcher, mentioning the type of event(s) this Event Handler should be used to notify about when the event(s) happen on an associated Handle.
- Event Handlers are identified by their associated Handles
- An Event Handler hook method will be called by the Initiation Dispatcher for a given Handle when a new event is detected by the Synchronous Event Demultiplexer for that particular Handle.
Taking Node.js as an example for Reactor Pattern implementation, the following is how we can relate,
Handles
In Node.js process running in a UNIX system, handles are the file descriptors created for every:
- target file to read or write to
- socket for an incoming connection
- socket which a server listens to
Synchronous Event Demultiplexer
Node.js uses
epollin Unix,kqueuein OSX and other BSDs, andIOCPon Windows as an implementation of Synchronous Event Demultiplexer to detect new events on Handles i.e. open files, open connections sockets, etc. Event Demultiplexer is not an atomic entity, but a collection of an I/O processing APIs abstracted by thelibuv.epolltakes file descriptorfdand event type(s) as input to be added to the watchlist (or the interested list). When queried, it gives the set of file descriptors that are ready from the intereseted list, eg,fdcorresponding for a TCP socket is "ready for reading".Initiation Dispatcher
Node.js uses
libuvas an abstraction that provides the functionality of the event loop including event queuing mechanism. The event loop registers Handles and a list of event(s) of interest to the Synchronous Event Demultiplexer, likeepollin UNIX.Event Handler and Event Handler Concrete
Node.js have inbuilt event handlers of various events for all types of Handles which in the end triggers a callback in the JavaScript domain.
Benefits
- Allows for simple concurrency without the additional complexity of multiple threads to application code.
- Modular and extensible application code separate from reactor implementations. In Node.js, application code is written in javascript and the actual reactor is inside the runtime.
Limitations
- Difficult to debug than procedural pattern due to inverted flow of control.
- Limited maximum concurrency because of the synchronous calling of event handlers. In Node.js, the event handlers execution and the event loop run in the same thread. So technically any code getting executed in event handlers is blocking the event loop and stalling pending events.
- Limited scalibility because of Event Handlers running synchronously and Synchronous Event Demultiplexer. In Node.js, the event loop can process only one event handler at a time.
- It can only to applied in OS which supports handles, file descriptor in Linux, and file handle in Windows.
Comparison with other Patterns
- Observer Pattern: Reactor and Observer both react to events. Reactor is generally used to demultiplex events from multiple event sources to their associated event handlers, whereas an Observer is associated with a single source of event.
- Proactor Pattern: In Reactor, we poll for interest events on handlers before doing something, whereas in Proactor we do something and poll for its completion. For example, take a look at the behavior of send operation on a socket with
IOCPin Windows as an example of Proactor. Quoting from the original paper,The Proactor supports the demultiplexing and dispatching of multiple event handlers that are triggered by the completion of asynchronous events. In contrast, the Reactor pattern is responsible for demultiplexing and dispatching of multiple event handlers that are triggered when it is possible to initiate an operation synchronously without blocking
- Chain of Responsibility Pattern: Reactor pattern associates a specific Event Handler to a particular source of an event whereas the COR pattern searches a chain to find the first matching Event Handler.
- Fascade Pattern: Implementation of Reactor Pattern provides a Fascade for event demultiplexing.
This concludes the write-up. Patterns and designs solve specific problems and within constraints, there is always a tradeoff behind every decision. This being my first write-up was full of ups and downs and distractions. I hope this reduces the barrier.
References
- Reactor - An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events, 1995
- Reactor Pattern - Wikipedia
- Blocking I/O, Nonblocking I/O, And Epoll
- An Introduction to libuv
- Comparing Two High-Performance I/O Design Patterns, 2005
- libuv - Design Overview
- Linux – IO Multiplexing – Select vs Poll vs Epoll