Socket
Socket
Sign inDemoInstall

express-graceful-exit

Package Overview
Dependencies
Maintainers
2
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

express-graceful-exit - npm Package Compare versions

Comparing version 0.4.2 to 0.5.0-rc.1

12

CHANGELOG.md

@@ -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 @@ ==================

2

index.js
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)
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc