Restarting Node Applications

A persistent pain point in NodeBB usage has always been installing plugins/themes. For example, the steps to install a plugin are as follows:

  1. npm install nodebb-plugin-myplugin
  2. Navigate to the "Plugins" page and activate it
  3. Restart NodeBB (plugin is not truly active otherwise)
  4. (Optional) If the plugin requires setup, navigate to its page in the control panel
  5. (Optional) Restart NodeBB again

This is a definite step back from other (non-node based) platforms, where plugin installation is a two step process:

  1. Install plugin (e.g. unzip to plugin directory)
  2. Activate plugin

Problem

We wanted to be able to have plugin installation, activation, and deactivation be a seamless process, but our underlying architecture didn't support that.

Specifically, our static routes (both for Plugins and Themes) could only be defined at the start of NodeBB, during the middleware declaration phase, and express (our framework) did not document (nor do they directly support) on-the-fly hotswapping of middlewares.

Our first solution was decidedly simple:

Restart NodeBB

As you can imagine, this didn't really solve anything, so a better solution was required.

What I knew was that a standalone process couldn't restart itself. It could exit, though that only solved half of the problem, I'd need it to come back up, as well.

The problem was exascerbated by the multitude of ways NodeBB could be run:

  • ./nodebb start
  • forever start app.js
  • plus many other options (Upstart, initscripts, supervisor, etc)

I needed a single solution that accounted for the use of programs like forever, as well.

Solution

The solution was to take a cue from other monitoring processes like forever and supervisor. Those processes stayed up while it restarted individual scripts. So we built a loader:

var fork = require('child_process').fork,
    start = function() {
        nbb = fork('./app', process.argv.slice(2), {
            env: {
                'NODE_ENV': process.env.NODE_ENV
            }
        });
    };

On invocation, it would fork a new processes that runs the actual application. We use fork specifically because it establishes a communication channel from parent to child (and from child to parent). We use this communication channel to trigger a restart:

nbb.on('message', function(cmd) {
    if (cmd === 'nodebb:restart') {
        nbb.on('exit', function() {
            start();
        });
        nbb.kill();
    }
});	

All NodeBB has to do is use process.send to tell its loader "restart me!", and the loader kills the process, waits for it to die, and forks another one.

The neat part is, even if forever is running loader.js, it does not interfere, as the child process is the one that is being restarted, and forever is watching the loader, which stays running.

If you wanted, you could even duplicate the functionality of forever, by adding this into your application code*:

process.on('uncaughtException', function(err) {
    winston.error('[app] Encountered Uncaught Exception: ' + err.message);
    console.log(err.stack);
    process.send('nodebb:restart');
});

Knowing what you know now, you can just have your app send "restart me!" to its parent whenever you'd like the process to restart**. Handy!

loader.js in its entirety:

var fork = require('child_process').fork,
    start = function() {
        nbb = fork('./app', process.argv.slice(2), {
            env: {
                'NODE_ENV': process.env.NODE_ENV
            }
        });

        nbb.on('message', function(cmd) {
            if (cmd === 'nodebb:restart') {
                nbb.on('exit', function() {
                    start();
                });
                nbb.kill();
            }
        });
    },
    stop = function() {
        nbb.kill();
    },
    nbb;

process.on('SIGINT', stop);
process.on('SIGTERM', stop);

start();

Notes

  • * It is worth mentioning that it is very bad form to use process.on('uncaughtException') unless you are planning to exit right away. Allowing the process to continue after an uncaught exception is like running PHP with error handling disabled. u insane, bro?
  • ** Just make sure only admins can trigger a restart!