express-graceful-exit
Advanced tools
Comparing version 0.4.2 to 0.5.0-rc.1
@@ -0,1 +1,13 @@ | ||
0.5.0-rc.1 / 2019-10-15 | ||
======================= | ||
Thank you hhunt for finding this bug, as well as for the fix PR and test code | ||
Issue #14 fixes, and configuration options for an improved graceful exit: | ||
* Fix side effects from handling of rejected incoming requests | ||
* Connections are no longer closed prematurely during request processing | ||
* Rejected requests during graceful exit end cleanly | ||
* Return connection close header with response(s), if any | ||
* Add option to perform one last request per connection | ||
* Add option to respond with default or custom http error for rejected requests | ||
0.4.2 / 2018-09-30 | ||
@@ -2,0 +14,0 @@ ================== |
module.exports = require('./lib/graceful-exit') | ||
module.exports = require('./lib/graceful-exit'); |
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); | ||
} | ||
} | ||
/** | ||
* Keep track of open connections so we can forcibly close sockets when the suicide timeout elapses | ||
* Track open connections to forcibly close sockets if and when the hard exit handler runs | ||
* @param server HTTP server | ||
*/ | ||
exports.init = function (server) { | ||
exports.init = function init (server) { | ||
server.on('connection', function (socket) { | ||
@@ -19,47 +42,80 @@ sockets.push(socket); | ||
exports.gracefulExitHandler = function(app, server, _options) { | ||
// Get the options set up | ||
if (!_options) { | ||
_options = {}; | ||
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); | ||
} | ||
var options = _.defaults(_options, { | ||
log : false, | ||
logger : console.log, | ||
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 | ||
}); | ||
var suicideTimeout; | ||
var connectionsClosed = false; | ||
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 logger(str) { | ||
if (options.log) { | ||
options.logger(str); | ||
} | ||
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); | ||
} | ||
} | ||
function exit(code) { | ||
if (suicideTimeout === null) { | ||
return; // server.close has finished, don't callback/exit twice | ||
} | ||
if (options.callback) { | ||
if (_.isFunction(options.callback)) { | ||
options.callback(code); | ||
} else { | ||
logger("Registered callback is not a function"); | ||
} | ||
} | ||
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) { | ||
// leave a bit of time to write logs, callback to complete, etc | ||
setTimeout(function() { | ||
process.exit(code); | ||
}, options.exitDelay); | ||
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; | ||
}; | ||
if (options.callback && options.exitProcess) { | ||
logger("Callback will have " + options.exitDelay + "ms to complete before hard exit"); | ||
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'); | ||
@@ -71,11 +127,11 @@ | ||
// Time to stop accepting new connections | ||
server.close(function() { | ||
server.close(function serverClosedCallback () { | ||
// Everything was closed successfully, mission accomplished! | ||
connectionsClosed = true; | ||
logger('All connections closed gracefully'); | ||
logger('No longer accepting connections'); | ||
exit(0); | ||
clearTimeout(suicideTimeout); | ||
suicideTimeout = null; // must be after calling exit() | ||
clearTimeout(hardExitTimer); // must be cleared after calling exit() | ||
hardExitTimer = null; | ||
}); | ||
@@ -85,69 +141,55 @@ | ||
if (options.socketio) { | ||
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(); | ||
}); | ||
} | ||
exports.disconnectSocketIOClients(); | ||
} | ||
// If after an acceptable time limit is reached and we still have some | ||
// connections lingering around for some reason, just die... we tried to | ||
// be graceful, but failed. | ||
suicideTimeout = setTimeout(function() { | ||
if (connectionsClosed) { | ||
// this condition should never occur, see server.close() above | ||
// 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('Hard exit timer ran out before some connections closed'); | ||
} | ||
exit(1); | ||
suicideTimeout = null; | ||
// 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); | ||
}; | ||
}, options.suicideTimeout); | ||
exports.handleFinalRequests = function handleFinalRequests (req, res, next) { | ||
var headers = inspect(req.headers) || '?'; // safe object to string | ||
if (options.performLastRequest && connection.lastRequestStarted === false) { | ||
logger('Server exiting, performing last request for this connection. Headers: ' + headers); | ||
req.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(app) { | ||
// This flag is used to tell the middleware we create that the server wants | ||
// to stop, so we do not allow anymore connections. This is done for all new | ||
// connections for us by Node, but we need to handle the connections that are | ||
// using the Keep-Alive header to stay on. | ||
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 gracefulExitHandler(req, res, next) { | ||
// Sorry Keep-Alive connections, but we need to part ways | ||
if (app.settings.graceful_exit === true) { | ||
req.connection.setTimeout(1); | ||
return function checkIfExitingGracefully (req, res, next) { | ||
var connection = req.connection || {}; | ||
if (app.settings.graceful_exit === false) { | ||
connection.lastRequestStarted = connection.lastRequestStarted || false; | ||
next(); | ||
} | ||
next(); | ||
// 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); | ||
}; | ||
}; |
{ | ||
"name": "express-graceful-exit", | ||
"version": "0.4.2", | ||
"version": "0.5.0-rc.1", | ||
"description": "Allow graceful exits for express apps, supporting zero downtime deploys", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
# express-graceful-exit | ||
A component in having zero downtime deploys for Node.js with [Express](http://expressjs.com/). It was developed for Express 3.X, so it may need work to be compatible with Express 2.X | ||
Gracefully decline new requests while shutting down your application. A component that helps support zero downtime deploys for Node.js with [Express](http://expressjs.com/). | ||
This module was originally developed for [Frafty](https://www.frafty.com/), a Daily Fantasy Sports site. | ||
The project was originally developed for Express v3.X, but is used in production with Express v4.X. Please write up an issue or submit a PR if you find bugs using express-graceful-exit with Express v4.X and higher. | ||
@@ -16,8 +16,12 @@ ## Installation | ||
v0.X.X versions are backwards API compatible, with the caveate that process exit is called in a `setTimeout` block from v0.2.0 forward, so the timing is slightly different between v0.1.0 to v0.2.x+. | ||
v0.X.X versions are backwards API compatible, with these minor behavior changes: | ||
1. Process exit is called in a `setTimeout` block from v0.2.0 forward, so the timing is slightly different between v0.1.0 to v0.2.x+. | ||
2. After exit was triggered, incoming requests were mismanaged prior to v0.5.0. <br> As of v0.5.0 incoming requests are dropped cleanly by default, with new options such as responding with a custom error and/or performing one last request per connection. | ||
## Usage | ||
The following two components must both be used to enable fully graceful exits. | ||
The following two components must both be used to enable clean server shutdown, where incoming requests are gracefully declined. | ||
There are multiple exit options for how in-flight requests are handled, ranging from forced exist after a specified deadline to waiting indefinitely for processing to complete. | ||
### middleware | ||
@@ -32,2 +36,5 @@ | ||
var server = app.listen(port) | ||
gracefulExit.init(server) // use init() if configured to exit the process after timeout | ||
app.use(gracefulExit.middleware(app)); | ||
@@ -45,3 +52,3 @@ ```` | ||
gracefulExit.gracefulExitHandler(app, server, { | ||
socketio: app.settings.socketio | ||
<see options below> | ||
}); | ||
@@ -67,6 +74,9 @@ } | ||
__callback__ | Optional function that is called with the exit status code once express has shutdown, gracefully or not <br> Use in conjunction with `exitProcess: false` when the caller handles process shutdown | no-op | ||
__exitProcess__ | If true, the module calls `process.exit()` when express has shutdown, gracefully or not | true | ||
__exitDelay__ | Wait timer duration in the final internal callback (triggered either by gracefulExitHandler or the suicideTimeout) if `exitProcess: true` | 10ms | ||
__suicideTimeout__ | How long to wait before giving up on graceful shutdown, then returns exit code of 1 | 2m 10s (130s) | ||
__socketio__ | An instance of `socket.io`, used to close all open connections after timeout | none | ||
__performLastRequest__ | Process the first request received per connection after exit starts, and include a connection close header for callers and load balancers. <br> `false` is the existing behavior, deprecated as of v0.5.0 | false | ||
__errorDuringExit__ | Respond to incoming requests with an error instead of silently dropping them. <br> `false` is the existing behavior, deprecated as of v0.5.0 | false | ||
__getRejectionError__ | Function returning rejection error for incoming requests during graceful exit | `function () { return new Error('Server unavailable, no new requests accepted during shutdown') }` | ||
__exitProcess__ | If true, the module calls `process.exit()` when express has shutdown, gracefully or not | true | ||
__exitDelay__ | Wait timer duration in the final internal callback (triggered either by gracefulExitHandler or the hard exit handler) if `exitProcess: true` | 10ms | ||
__suicideTimeout__ | How long to wait before giving up on graceful shutdown, then returns exit code of 1 | 2m 10s (130s) | ||
__socketio__ | An instance of `socket.io`, used to close all open connections after timeout | none | ||
__force__ | Instructs the module to forcibly close sockets once the suicide timeout elapses. <br> For this option to work you must call `gracefulExit.init(server)` when initializing the HTTP server | false | ||
@@ -76,13 +86,11 @@ | ||
To gracefully exit this module will do the following things: | ||
To gracefully exit this module does the following things: | ||
1. Close the http server so no new connections are accepted | ||
2. Mark that the server will gracefully exit, so if a connection that is using the Keep-Alive header is still active, it will be told to close the connection | ||
The HTTP status code of 502 is returned, so nginx, ELB, etc will try again with a working server | ||
3. If a socket.io instance is passed in the options, it enumerates all connected clients and disconnects them | ||
The client should have code to reconnect on disconnect | ||
4. Server fully disconnects or the hard exit timer runs | ||
1. Once all connected clients are disconnected, the exit handler returns `0` | ||
2. OR If there are any remaining connections after `suicideTimeout` ms, the handler returns `1` | ||
5. In either case, if exitProcess is set to true the exit handler waits exitDelay ms and calls `process.exit` | ||
1. Closes the http server so no new connections are accepted | ||
2. Sets connection close header for Keep-Alive connections, if configured for responses</br> The HTTP status code of 502 is returned, so nginx, ELB, etc will try with an active server</br> If `errorDuringExit` and/or `performLastRequest` are set to true, a response is sent with a `Connection: close` header | ||
3. If a socket.io instance is passed in the options, all connected clients are immediately disconnected (socket.io v0.X through v1.4.x support)</br> The client should have code to reconnect on disconnect | ||
4. Once the server fully disconnects or the hard exit timer runs | ||
1. If all in-flight requests have resolved and/or disconnected, the exit handler returns `0` | ||
2. OR if any connections remain after `suicideTimeout` ms, the handler returns `1` | ||
5. In either case, if exitProcess is set to true the hard exit handler waits exitDelay ms and calls `process.exit(x)`, this allows the logger time to flush and the app's callback to complete, if any | ||
@@ -94,3 +102,4 @@ ## Zero Downtime Deploys | ||
#### Author: [Jon Keating](http://twitter.com/emostar) | ||
This module was originally developed for Frafty (formerly www.frafty.com), a Daily Fantasy Sports site. | ||
#### Maintainer: [Ivo Havener](https://github.com/ivolucien) | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
15729
165
100