zhang-wei-jian's picture
:rocket:
7d73cf2
'use strict';
// =============================================================================
// _
// |_ _|_ _|_ ._ __ _ ._ _. _ _ _|_ | __ _ |_ _|_ _| _ ._
// | | |_ |_ |_) (_| | (_| (_ (/_ | |_| | _> | | |_| |_ (_| (_) \/\/ | |
// | _|
// -----------------------------------------------------------------------------
// gracefully shuts downs http server
// can be used with http, express, koa, ...
// (c) 2023 Sebastian Hildebrandt
// License: MIT
// =============================================================================
const debug = require('debug')('http-graceful-shutdown');
const http = require('http');
/**
* Gracefully shuts down `server` when the process receives
* the passed signals
*
* @param {http.Server} server
* @param {object} opts
* signals: string (each signal separated by SPACE)
* timeout: timeout value for forceful shutdown in ms
* forceExit: force process.exit() - otherwise just let event loop clear
* development: boolean value (if true, no graceful shutdown to speed up development
* preShutdown: optional function. Needs to return a promise. - HTTP sockets are still available and untouched
* onShutdown: optional function. Needs to return a promise.
* finally: optional function, handled at the end of the shutdown.
*/
function GracefulShutdown(server, opts) {
// option handling
// ----------------------------------
opts = opts || {};
// merge opts with default options
let options = Object.assign({
signals: 'SIGINT SIGTERM',
timeout: 30000,
development: false,
forceExit: true,
onShutdown: (signal) => Promise.resolve(signal),
preShutdown: (signal) => Promise.resolve(signal),
}, opts);
let isShuttingDown = false;
let connections = {};
let connectionCounter = 0;
let secureConnections = {};
let secureConnectionCounter = 0;
let failed = false;
let finalRun = false;
function onceFactory() {
let called = false;
return (emitter, events, callback) => {
function call() {
if (!called) {
called = true;
return callback.apply(this, arguments);
}
}
events.forEach(e => emitter.on(e, call));
};
}
const signals = options.signals
.split(' ')
.map(s => s.trim())
.filter(s => !!s.length);
const once = onceFactory();
once(process, signals, (signal) => {
debug('received shut down signal', signal);
shutdown(signal)
.then(() => {
if (options.forceExit) {
process.exit(failed ? 1 : 0);
}
})
.catch((err) => {
debug('server shut down error occurred', err);
process.exit(1);
});
});
// helper function
// ----------------------------------
function isFunction(functionToCheck) {
let getType = Object.prototype.toString.call(functionToCheck);
return /^\[object\s([a-zA-Z]+)?Function\]$/.test(getType);
}
function destroy(socket, force = false) {
if ((socket._isIdle && isShuttingDown) || force) {
socket.destroy();
if (socket.server instanceof http.Server) {
delete connections[socket._connectionId];
} else {
delete secureConnections[socket._connectionId];
}
}
}
function destroyAllConnections(force = false) {
// destroy empty and idle connections / all connections (if force = true)
debug('Destroy Connections : ' + (force ? 'forced close' : 'close'));
let counter = 0;
let secureCounter = 0;
Object.keys(connections).forEach(function (key) {
const socket = connections[key];
const serverResponse = socket._httpMessage;
// send connection close header to open connections
if (serverResponse && !force) {
if (!serverResponse.headersSent) {
serverResponse.setHeader('connection', 'close');
}
} else {
counter++;
destroy(socket);
}
});
debug('Connections destroyed : ' + counter);
debug('Connection Counter : ' + connectionCounter);
Object.keys(secureConnections).forEach(function (key) {
const socket = secureConnections[key];
const serverResponse = socket._httpMessage;
// send connection close header to open connections
if (serverResponse && !force) {
if (!serverResponse.headersSent) {
serverResponse.setHeader('connection', 'close');
}
} else {
secureCounter++;
destroy(socket);
}
});
debug('Secure Connections destroyed : ' + secureCounter);
debug('Secure Connection Counter : ' + secureConnectionCounter);
}
// set up server/process events
// ----------------------------------
server.on('request', function (req, res) {
req.socket._isIdle = false;
if (isShuttingDown && !res.headersSent) {
res.setHeader('connection', 'close');
}
res.on('finish', function () {
req.socket._isIdle = true;
destroy(req.socket);
});
});
server.on('connection', function (socket) {
if (isShuttingDown) {
socket.destroy();
} else {
let id = connectionCounter++;
socket._isIdle = true;
socket._connectionId = id;
connections[id] = socket;
socket.once('close', () => {
delete connections[socket._connectionId];
});
}
});
server.on('secureConnection', (socket) => {
if (isShuttingDown) {
socket.destroy();
} else {
let id = secureConnectionCounter++;
socket._isIdle = true;
socket._connectionId = id;
secureConnections[id] = socket;
socket.once('close', () => {
delete secureConnections[socket._connectionId];
});
}
});
process.on('close', function () {
debug('closed');
});
// shutdown event (per signal)
// ----------------------------------
function shutdown(sig) {
function cleanupHttp() {
destroyAllConnections();
debug('Close http server');
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) {
return reject(err);
}
return resolve(true);
});
});
}
debug('shutdown signal - ' + sig);
// Don't bother with graceful shutdown on development to speed up round trip
if (options.development) {
debug('DEV-Mode - immediate forceful shutdown');
return process.exit(0);
}
function finalHandler() {
if (!finalRun) {
finalRun = true;
if (options.finally && isFunction(options.finally)) {
debug('executing finally()');
options.finally();
}
}
return Promise.resolve();
}
// returns true if should force shut down. returns false for shut down without force
function waitForReadyToShutDown(totalNumInterval) {
debug(`waitForReadyToShutDown... ${totalNumInterval}`);
if (totalNumInterval === 0) { // timeout reached
debug(
`Could not close connections in time (${options.timeout}ms), will forcefully shut down`
);
return Promise.resolve(true);
}
// test all connections closed already?
const allConnectionsClosed = Object.keys(connections).length === 0 && Object.keys(secureConnections).length === 0;
if (allConnectionsClosed) {
debug('All connections closed. Continue to shutting down');
return Promise.resolve(false);
}
debug('Schedule the next waitForReadyToShutdown');
return new Promise((resolve) => {
setTimeout(() => {
resolve(waitForReadyToShutDown(totalNumInterval - 1));
}, 250);
});
}
if (isShuttingDown) {
return Promise.resolve();
}
debug('shutting down');
return options
.preShutdown(sig)
.then(() => {
isShuttingDown = true;
cleanupHttp();
})
.then(() => {
const pollIterations = options.timeout
? Math.round(options.timeout / 250)
: 0;
return waitForReadyToShutDown(pollIterations);
})
.then((force) => {
debug('Do onShutdown now');
// if after waiting for connections to drain within timeout period
// or if timeout has reached, we forcefully disconnect all sockets
if (force) {
destroyAllConnections(force);
}
return options.onShutdown(sig);
})
.then(finalHandler)
.catch((err) => {
const errString = typeof err === 'string' ? err : JSON.stringify(err);
debug(errString);
failed = true;
throw errString;
});
}
function shutdownManual() {
return shutdown('manual');
}
return shutdownManual;
}
module.exports = GracefulShutdown;