UNPKG

express-graceful-exit

Version:

Allow graceful exits for express apps, supporting zero downtime deploys

195 lines (165 loc) 5.91 kB
var _ = require('underscore'); var inspect = require('util').inspect; var sockets = []; var options = {}; var hardExitTimer; var connectionsClosed = false; var defaultOptions = { errorDuringExit : false, // false is existing behavior, deprecated as of v0.5.0 performLastRequest: false, // false is existing behavior, deprecated as of v0.5.0 log : false, logger : console.log, getRejectionError : function (err) { return err; }, suicideTimeout : 2*60*1000 + 10*1000, // 2m10s (nodejs default is 2m) exitProcess : true, exitDelay : 10, // wait in ms before process.exit, if exitProcess true force : false }; function logger (str) { if (options.log) { options.logger(str); } } /** * Track open connections to forcibly close sockets if and when the hard exit handler runs * @param server HTTP server */ exports.init = function init (server) { server.on('connection', function (socket) { sockets.push(socket); socket.on('close', function () { sockets.splice(sockets.indexOf(socket), 1); }); }); }; exports.disconnectSocketIOClients = function disconnectSocketIOClients () { var sockets = options.socketio.sockets; var connectedSockets; if (typeof sockets.sockets === 'object' && !Array.isArray(sockets.sockets)) { // socket.io 1.4+ connectedSockets = _.values(sockets.sockets); } else if (sockets.sockets && sockets.sockets.length) { // socket.io 1.0-1.3 connectedSockets = sockets.sockets; } else if (typeof sockets.clients === 'function') { // socket.io 0.x connectedSockets = sockets.clients(); } if (typeof options.socketio.close === 'function') { options.socketio.close(); } if (connectedSockets && connectedSockets.length) { logger('Killing ' + connectedSockets.length + ' socket.io sockets'); connectedSockets.forEach(function(socket) { socket.disconnect(); }); } }; function exit (code) { if (hardExitTimer === null) { return; // server.close has finished, don't callback/exit twice } if (_.isFunction(options.callback)) { options.callback(code); } if (options.exitProcess) { logger("Exiting process with code " + code); // leave a bit of time to write logs, callback to complete, etc setTimeout(function() { process.exit(code); }, options.exitDelay); } } exports.hardExitHandler = function hardExitHandler () { if (connectionsClosed) { // this condition should never occur, see serverClosedCallback() below. // the user callback, if any, has already been called if (options.exitProcess) { process.exit(1); } return; } if (options.force) { sockets = sockets || []; logger('Destroying ' + sockets.length + ' open sockets'); sockets.forEach(function (socket) { socket.destroy(); }); } else { logger('Suicide timer ran out before some connections closed'); } exit(1); hardExitTimer = null; }; exports.gracefulExitHandler = function gracefulExitHandler (app, server, _options) { // Get the options set up if (!_options) { _options = {}; } options = _.defaults(_options, defaultOptions); if (options.callback) { if (!_.isFunction(options.callback)) { logger("Ignoring callback option that is not a function"); } else if (options.exitProcess) { logger("Callback has " + options.exitDelay + "ms to complete before hard exit"); } } logger('Closing down the http server'); // Let everything know that we wish to exit gracefully app.set('graceful_exit', true); // Time to stop accepting new connections server.close(function serverClosedCallback () { // Everything was closed successfully, mission accomplished! connectionsClosed = true; logger('No longer accepting connections'); exit(0); clearTimeout(hardExitTimer); // must be cleared after calling exit() hardExitTimer = null; }); // Disconnect all the socket.io clients if (options.socketio) { exports.disconnectSocketIOClients(); } // If any connections linger past the suicide timeout, exit the process. // When this fires we've run out of time to exit gracefully. hardExitTimer = setTimeout(exports.hardExitHandler, options.suicideTimeout); }; exports.handleFinalRequests = function handleFinalRequests (req, res, next) { var headers = inspect(req.headers) || '?'; // safe object to string var connection = req.connection || {}; if (options.performLastRequest && connection.lastRequestStarted === false) { logger('Server exiting, performing last request for this connection. Headers: ' + headers); connection.lastRequestStarted = true; return next(); } if (options.errorDuringExit) { logger('Server unavailable, incoming request rejected with error. Headers: ' + headers); return next( options.getRejectionError() || defaultOptions.getRejectionError( new Error('Server unavailable, no new requests accepted during shutdown') ) ); } // else silently drop request without response (existing deprecated behavior) logger('Server unavailable, incoming request dropped silently. Headers: ' + headers); res.end(); // end request without calling next() return null; }; exports.middleware = function middleware (app) { // This flag is used to signal the below middleware when the server wants to stop. app.set('graceful_exit', false); return function checkIfExitingGracefully (req, res, next) { if (app.settings.graceful_exit === false) { return next(); } var connection = req.connection || {}; connection.lastRequestStarted = connection.lastRequestStarted || false; // Set connection closing header for response, if any. Fix to issue 14, thank you HH res.set('Connection', 'close'); return exports.handleFinalRequests(req, res, next); }; };