Die, Child Process, Die!
In Node.js it is a fairly common practice to fork one or more companion child processes to a main process, such as when you need precise timing over short intervals, or when you need to run something computationally intensive and don't want to tie up the main thread of operation. Node.js provides the core child_process and cluster modules to manage forking, sharing of ports, and communication between processes to support various different types of multi-process application.
A topic that arises fairly frequently is how to ensure that child processes end when the main process ends. This is actually not as easy to arrange as you might think, as there are a potentially wide variety of circumstances to accommodate. For example, to pick a few:
- Running on Windows
- Running on Unix
- Running under the Forever process manager
- Running under a Vows test suite
- Running from the command line during development
- The parent process shuts itself down via process.exit()
- The parent process receives a SIGKILL signal
- The parent process receives a SIGTERM signal
- The parent process receives a SIGINT signal
- The parent process throws an uncaught exception
Considering Unix Signals
It this context it's important to have at least a rudimentary understanding of the difference between various signals sent to processes in a Unix OS. On the command line SIGKILL is generated by a "kill -9" command, SIGTERM by "kill", and SIGINT by crtl-c. The latter two allow a chance for the program to shut itself down gracefully, while the former is just an abrupt end with no chance to clean up. A server process should probably shut down in response to SIGINT - the most likely scenario is that it is running from the command line during development and the developer wants to stop it.
Windows has no such signals, though you can fake SIGINT as follows:
var readLine = require("readline"); if (process.platform === "win32") { var rl = readLine.createInterface ({ input: process.stdin, output: process.stdout }); rl.on("SIGINT", function () { process.emit("SIGINT"); }); }
Tools such as Vows and Forever kill processes under them via SIGTERM, so as to allow for a graceful shutdown. A Node.js process can listen for SIGTERM events and act accordingly:
// Only fire once to avoid potential overlap (not that this will happen in a // well-ordered environment, but you never know). process.once("SIGTERM", function () { // Cleanup activities go here... // Then shutdown. process.exit(0); });
You can't listen for a SIGKILL event, as a SIGKILL signal (and the equivalent ending of a process on Windows) just kills the Node.js process immediately. This is why Vows and Forever use SIGTERM, and so should you if you need to shut down a managed process.
Companion Child Process Scenario
For the purposes of this post we'll consider the use case of creating a companion child process that we want to keep around until the parent process ends, at which point it should shut down gracefully. The parent and child process will be communicating with one another, so that means we should use child_process.fork() to create it:
var childProcess = require("child_process"); var path = require("path"); // If the child process accepts arguments, they go here. var childArguments = []; var child = childProcess.fork( // Run a Node.js script called child.js that is in the same directory as this file. path.join(__dirname, "child.js"), childArguments, { // Pass over all of the environment. env: process.ENV, silent: false } ); // Listen for messages from the child process. child.on("message", function (message) { // Respond to the message. });
The first thing to consider is that if the child process dies unexpectedly, then we probably want to so something about that. One option is to fork a new child, but here let's go with exiting the parent as that requires a little more organization:
// Helper function added to the child process to manage shutdown. child.onUnexpectedExit = function (code, signal) { console.log("Child process terminated with code: " + code); process.exit(1); } child.on("exit", child.onUnexpectedExit);
Of the various ways that the parent process can end the three cases that we care about are (a) process.exit() is called somewhere, (b) a SIGINT or SIGTERM signal is received, (c) an uncaught exception is thrown. These can all be managed in a way that causes the child process to shut down as well as the parent:
// A helper function to shut down the child. child.shutdown = function () { // Get rid of the exit listener since this is a planned exit. this.removeListener("exit", this.onUnexpectedExit); this.kill("SIGTERM"); } // SIGTERM AND SIGINT will trigger the exit event. process.once("SIGTERM", function () { process.exit(0); }); process.once("SIGINT", function () { process.exit(0); }); // And the exit event shuts down the child. process.once("exit", function () { child.shutdown(); }); // This is a somewhat ugly approach, but it has the advantage of working // in conjunction with most of what third parties might choose to do with // uncaughtException listeners, while preserving whatever the exception is. process.once("uncaughtException", function (error) { // If this was the last of the listeners, then shut down the child and rethrow. // Our assumption here is that any other code listening for an uncaught // exception is going to do the sensible thing and call process.exit(). if (process.listeners("uncaughtException").length === 0) { child.shutdown(); throw error; } });
SIGKILL is a Problem
A SIGKILL signal sent to the parent means there is no opportunity to clean up, and it will leave a forked child running, at least until the communication channel between the now dead parent and the child times out or otherwise results in an uncaught exception. This should be an exceptional circumstance, however: you should not find yourself in a situation where in the ordinary course of operations someone is sending SIGKILL to your parent process. Process managers like Forever and Vows should always send SIGTERM signals instead.