├── .gitignore ├── index.js ├── package.json ├── index.d.ts ├── LICENSE ├── CHANGELOG.md ├── README.md └── lib └── graceful-exit.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/graceful-exit'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-graceful-exit", 3 | "version": "0.5.2", 4 | "description": "Allow graceful exits for express apps, supporting zero downtime deploys", 5 | "keywords": [ 6 | "express", 7 | "graceful", 8 | "exit", 9 | "shutdown", 10 | "clean", 11 | "tidy" 12 | ], 13 | "main": "index.js", 14 | "types": "index.d.ts", 15 | "scripts": { 16 | "test": "echo \"Error: just manually tested, so far\" && exit 1" 17 | }, 18 | "repository": "git://github.com/mathrawka/express-graceful-exit.git", 19 | "homepage": "https://github.com/mathrawka/express-graceful-exit", 20 | "author": "Jon Keating ", 21 | "contributors": [ 22 | "Ivo Havener " 23 | ], 24 | "license": "MIT", 25 | "dependencies": { 26 | "underscore": "^1.12.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'net' 2 | import { Express, NextFunction, Request, RequestHandler, Response } from 'express' 3 | 4 | declare namespace GracefulExit { 5 | interface Configuration { 6 | errorDuringExit?: boolean 7 | performLastRequest?: boolean 8 | callback?: (code: number) => void 9 | log?: boolean 10 | logger?: (message: string) => void 11 | getRejectionError?: () => Error 12 | suicideTimeout?: number 13 | exitProcess?: boolean 14 | exitDelay?: number 15 | force?: boolean 16 | } 17 | 18 | function init(server: Server): void 19 | function gracefulExitHandler(app: Express, server: Server, options?: Configuration): void 20 | function middleware(app: Express): RequestHandler 21 | 22 | function disconnectSocketIOClients(): void 23 | function hardExitHandler(): void 24 | function handleFinalRequests(req: Request, res: Response, next: NextFunction): void 25 | } 26 | 27 | export = GracefulExit 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Jon Keating 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.5.2 / 2021-03-29 2 | ================== 3 | 4 | * Fix registry entry 5 | 6 | 0.5.1 / 2021-03-29 7 | ================== 8 | Thank you @techmunk and @kevynb for the typescript work 9 | 10 | * Typescript definitions 11 | * Security patch addressing vulnerability with update to underscore version 12 | 13 | 0.5.0 / 2019-10-21 14 | ================== 15 | Changes reflected in the below release candidate versions 16 | 17 | 0.5.0-rc.2 / 2019-10-18 18 | ======================= 19 | Thank you hhunt for additional testing and the fix PR 20 | 21 | * Fix errors in new option to handle a last request, including a crasher 22 | 23 | 0.5.0-rc.1 / 2019-10-15 24 | ======================= 25 | Thank you hhunt for finding this bug, as well as for the fix PR and test code 26 | 27 | Issue #14 fixes, and configuration options for an improved graceful exit: 28 | * Fix side effects from handling of rejected incoming requests 29 | * Connections are no longer closed prematurely during request processing 30 | * Rejected requests during graceful exit end cleanly 31 | * Return connection close header with response(s), if any 32 | * Add option to perform one last request per connection 33 | * Add option to respond with default or custom http error for rejected requests 34 | 35 | 0.4.2 / 2018-09-30 36 | ================== 37 | 38 | * Fix undefined socket array error 39 | * Use intended exit code upon forced exit after timeout 40 | 41 | 0.4.1 / 2018-01-15 42 | ================== 43 | 44 | * Names for anonymous functions, for better stack traces 45 | 46 | 0.4.0 / 2017-03-19 47 | ================== 48 | 49 | * Support disconnect for more socket.io versions 50 | 51 | 0.3.2 / 2016-08/05 52 | ================== 53 | 54 | * Released version to npm 55 | * This version entry, npm keeping me honest 56 | 57 | 0.3.1 / 2016-08/05 58 | ================== 59 | 60 | * Doc format fixes 61 | 62 | 0.3.0 / 2016-08/05 63 | ================== 64 | 65 | * Released version to npm 66 | * Configurable delay for timer that calls process exit 67 | * Hard exit function now obeys exitProcess option 68 | * Doc updates, options in table format 69 | 70 | 0.2.1 / 2016-07-27 71 | ================== 72 | 73 | * Released version to npm 74 | * Updated package metadata, version string 75 | * Code style overhaul, many semicolons 76 | 77 | 0.2.0 / 2016-07-26 78 | ================== 79 | Thanks to shaharke for the majority of these changes. 80 | 81 | * Delay process exit to allow any streams to flush, etc. 82 | * Option to force close sockets on timeout 83 | * Minor doc and logging improvements 84 | 85 | Issue #1 feature request and fixes: 86 | * Exit handler callback when done or on timeout 87 | * Option for exit handler to not exit process itself 88 | * Clear hard exit timeout on successful server close 89 | * Avoid duplicate callback invocation 90 | 91 | 0.1.0 / 2013-03-28 92 | ================== 93 | 94 | * Released version to npm 95 | * Don't keep track of Keep-Alive connections 96 | * Switch to not catching the exit message on our own 97 | 98 | 0.0.2 / 2013-03-26 99 | ================== 100 | 101 | * Typo fix in README 102 | 103 | 0.0.1 / 2013-03-26 104 | ================== 105 | 106 | * Initial Release 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-graceful-exit 2 | 3 | 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/). 4 | 5 | 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. 6 | 7 | ## Installation 8 | 9 | ```` bash 10 | $ cd /path/to/your/project 11 | $ npm install express-graceful-exit 12 | ```` 13 | 14 | ## Compatibility 15 | 16 | v0.X.X versions are backwards API compatible, with these minor behavior changes: 17 | 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+. 18 | 2. After exit was triggered, incoming requests were mismanaged prior to v0.5.0.
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. 19 | 20 | ## Usage 21 | 22 | The following two components must both be used to enable clean server shutdown, where incoming requests are gracefully declined. 23 | 24 | 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. 25 | 26 | ### middleware 27 | 28 | This middleware should be the very first middleware that gets setup with your Express app. 29 | 30 | ```` javascript 31 | var express = require('express'); 32 | var app = express(); 33 | var gracefulExit = require('express-graceful-exit'); 34 | 35 | var server = app.listen(port) 36 | 37 | gracefulExit.init(server) // use init() if configured to exit the process after timeout 38 | app.use(gracefulExit.middleware(app)); 39 | ```` 40 | 41 | ### gracefulExitHandler 42 | 43 | This function tells express to accept no new requests and gracefully closes the http server. It can be attached to a signal, or used as a normal function call if another tool is used (such as [naught](https://github.com/indabamusic/naught)). 44 | 45 | ```` javascript 46 | // Example for naught 47 | process.on('message', function(message) { 48 | if (message === 'shutdown') { 49 | gracefulExit.gracefulExitHandler(app, server, { 50 | 51 | }); 52 | } 53 | }); 54 | ```` 55 | 56 | ## Options 57 | 58 | ### Middleware 59 | 60 | There are no options available currently. 61 | 62 | ### Exit Handler 63 | 64 | The following options are available: 65 | 66 | Option | Description | Default 67 | :------------------ | :---------------------------------------------- | :------- 68 | __log__ | Print status messages and errors to the logger | false 69 | __logger__ | Function that accepts a string to output a log message | console.log 70 | __callback__ | Optional function that is called with the exit status code once express has shutdown, gracefully or not
Use in conjunction with `exitProcess: false` when the caller handles process shutdown | no-op 71 | __performLastRequest__ | Process the first request received per connection after exit starts, and include a connection close header in the response for the caller and/or load balancer.
The current default is `false`, but will default to `true` in the next major release, `false` is deprecated as of v0.5.0 | false, **true is recommended** 72 | __errorDuringExit__ | When requests are refused during graceful exit, respond with an error instead of silently dropping them.
The current default is `false`, but will default to `true` in the next major release, `false` is deprecated as of v0.5.0 | false, **true is recommended** 73 | __getRejectionError__ | Function returning rejection error for incoming requests during graceful exit | `function () { return new Error('Server unavailable, no new requests accepted during shutdown') }` 74 | __exitProcess__ | If true, the module calls `process.exit()` when express has shutdown, gracefully or not | true 75 | __exitDelay__ | Wait timer duration in the final internal callback (triggered either by gracefulExitHandler or the hard exit handler) if `exitProcess: true` | 10ms 76 | __suicideTimeout__ | How long to wait before giving up on graceful shutdown, then returns exit code of 1 | 2m 10s (130s) 77 | __socketio__ | An instance of `socket.io`, used to close all open connections after timeout | none 78 | __force__ | Instructs the module to forcibly close sockets once the suicide timeout elapses.
For this option to work you must call `gracefulExit.init(server)` when initializing the HTTP server | false 79 | 80 | ## Details 81 | 82 | To gracefully exit this module does the following things: 83 | 84 | 1. Closes the http server so no new connections are accepted 85 | 2. Sets connection close header for Keep-Alive connections, if configured for responses
If `errorDuringExit` is true, HTTP status code 502 is returned by default, so nginx, ELB, etc will resend to an active server
If `errorDuringExit` and/or `performLastRequest` are set to true, a response is sent with a `Connection: close` header 86 | 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)
The client should have code to reconnect on disconnect 87 | 4. Once the server fully disconnects or the hard exit timer runs 88 | 1. If all in-flight requests have resolved and/or disconnected, the exit handler returns `0` 89 | 2. OR if any connections remain after `suicideTimeout` ms, the handler returns `1` 90 | 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 91 | 92 | ## Zero Downtime Deploys 93 | 94 | This module does not give you zero downtime deploys on its own. It enables the http server to exit gracefully, which when used with a module like naught can provide zero downtime deploys. 95 | 96 | #### Author: [Jon Keating](http://twitter.com/emostar) 97 | This module was originally developed for Frafty (formerly www.frafty.com), a Daily Fantasy Sports site. 98 | #### Maintainer: [Ivo Havener](https://github.com/ivolucien) 99 | 100 | -------------------------------------------------------------------------------- /lib/graceful-exit.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('underscore'); 3 | var inspect = require('util').inspect; 4 | 5 | var sockets = []; 6 | var options = {}; 7 | var hardExitTimer; 8 | var connectionsClosed = false; 9 | 10 | var defaultOptions = { 11 | errorDuringExit : false, // false is existing behavior, deprecated as of v0.5.0 12 | performLastRequest: false, // false is existing behavior, deprecated as of v0.5.0 13 | log : false, 14 | logger : console.log, 15 | getRejectionError : function (err) { return err; }, 16 | suicideTimeout : 2*60*1000 + 10*1000, // 2m10s (nodejs default is 2m) 17 | exitProcess : true, 18 | exitDelay : 10, // wait in ms before process.exit, if exitProcess true 19 | force : false 20 | }; 21 | 22 | function logger (str) { 23 | if (options.log) { 24 | options.logger(str); 25 | } 26 | } 27 | 28 | /** 29 | * Track open connections to forcibly close sockets if and when the hard exit handler runs 30 | * @param server HTTP server 31 | */ 32 | exports.init = function init (server) { 33 | server.on('connection', function (socket) { 34 | sockets.push(socket); 35 | 36 | socket.on('close', function () { 37 | sockets.splice(sockets.indexOf(socket), 1); 38 | }); 39 | }); 40 | }; 41 | 42 | exports.disconnectSocketIOClients = function disconnectSocketIOClients () { 43 | var sockets = options.socketio.sockets; 44 | var connectedSockets; 45 | if (typeof sockets.sockets === 'object' && !Array.isArray(sockets.sockets)) { 46 | // socket.io 1.4+ 47 | connectedSockets = _.values(sockets.sockets); 48 | } 49 | else if (sockets.sockets && sockets.sockets.length) { 50 | // socket.io 1.0-1.3 51 | connectedSockets = sockets.sockets; 52 | } 53 | else if (typeof sockets.clients === 'function') { 54 | // socket.io 0.x 55 | connectedSockets = sockets.clients(); 56 | } 57 | if (typeof options.socketio.close === 'function') { 58 | options.socketio.close(); 59 | } 60 | if (connectedSockets && connectedSockets.length) { 61 | logger('Killing ' + connectedSockets.length + ' socket.io sockets'); 62 | connectedSockets.forEach(function(socket) { 63 | socket.disconnect(); 64 | }); 65 | } 66 | }; 67 | 68 | function exit (code) { 69 | if (hardExitTimer === null) { 70 | return; // server.close has finished, don't callback/exit twice 71 | } 72 | if (_.isFunction(options.callback)) { 73 | options.callback(code); 74 | } 75 | if (options.exitProcess) { 76 | logger("Exiting process with code " + code); 77 | // leave a bit of time to write logs, callback to complete, etc 78 | setTimeout(function() { 79 | process.exit(code); 80 | }, options.exitDelay); 81 | } 82 | } 83 | 84 | exports.hardExitHandler = function hardExitHandler () { 85 | if (connectionsClosed) { 86 | // this condition should never occur, see serverClosedCallback() below. 87 | // the user callback, if any, has already been called 88 | if (options.exitProcess) { 89 | process.exit(1); 90 | } 91 | return; 92 | } 93 | if (options.force) { 94 | sockets = sockets || []; 95 | logger('Destroying ' + sockets.length + ' open sockets'); 96 | sockets.forEach(function (socket) { 97 | socket.destroy(); 98 | }); 99 | } else { 100 | logger('Suicide timer ran out before some connections closed'); 101 | } 102 | exit(1); 103 | hardExitTimer = null; 104 | }; 105 | 106 | exports.gracefulExitHandler = function gracefulExitHandler (app, server, _options) { 107 | // Get the options set up 108 | if (!_options) { 109 | _options = {}; 110 | } 111 | options = _.defaults(_options, defaultOptions); 112 | if (options.callback) { 113 | if (!_.isFunction(options.callback)) { 114 | logger("Ignoring callback option that is not a function"); 115 | } 116 | else if (options.exitProcess) { 117 | logger("Callback has " + options.exitDelay + "ms to complete before hard exit"); 118 | } 119 | } 120 | logger('Closing down the http server'); 121 | 122 | // Let everything know that we wish to exit gracefully 123 | app.set('graceful_exit', true); 124 | 125 | // Time to stop accepting new connections 126 | server.close(function serverClosedCallback () { 127 | // Everything was closed successfully, mission accomplished! 128 | connectionsClosed = true; 129 | 130 | logger('No longer accepting connections'); 131 | exit(0); 132 | 133 | clearTimeout(hardExitTimer); // must be cleared after calling exit() 134 | hardExitTimer = null; 135 | }); 136 | 137 | // Disconnect all the socket.io clients 138 | if (options.socketio) { 139 | exports.disconnectSocketIOClients(); 140 | } 141 | 142 | // If any connections linger past the suicide timeout, exit the process. 143 | // When this fires we've run out of time to exit gracefully. 144 | hardExitTimer = setTimeout(exports.hardExitHandler, options.suicideTimeout); 145 | }; 146 | 147 | exports.handleFinalRequests = function handleFinalRequests (req, res, next) { 148 | var headers = inspect(req.headers) || '?'; // safe object to string 149 | var connection = req.connection || {}; 150 | 151 | if (options.performLastRequest && connection.lastRequestStarted === false) { 152 | logger('Server exiting, performing last request for this connection. Headers: ' + headers); 153 | 154 | connection.lastRequestStarted = true; 155 | return next(); 156 | } 157 | 158 | if (options.errorDuringExit) { 159 | logger('Server unavailable, incoming request rejected with error. Headers: ' + headers); 160 | 161 | return next( 162 | options.getRejectionError() || 163 | defaultOptions.getRejectionError( 164 | new Error('Server unavailable, no new requests accepted during shutdown') 165 | ) 166 | ); 167 | } 168 | 169 | // else silently drop request without response (existing deprecated behavior) 170 | logger('Server unavailable, incoming request dropped silently. Headers: ' + headers); 171 | 172 | res.end(); // end request without calling next() 173 | return null; 174 | }; 175 | 176 | exports.middleware = function middleware (app) { 177 | // This flag is used to signal the below middleware when the server wants to stop. 178 | app.set('graceful_exit', false); 179 | 180 | return function checkIfExitingGracefully (req, res, next) { 181 | 182 | if (app.settings.graceful_exit === false) { 183 | return next(); 184 | } 185 | 186 | var connection = req.connection || {}; 187 | connection.lastRequestStarted = connection.lastRequestStarted || false; 188 | 189 | // Set connection closing header for response, if any. Fix to issue 14, thank you HH 190 | res.set('Connection', 'close'); 191 | 192 | return exports.handleFinalRequests(req, res, next); 193 | }; 194 | }; 195 | --------------------------------------------------------------------------------