214 lines
6.3 KiB
JavaScript
214 lines
6.3 KiB
JavaScript
|
|
import http from "node:http";
|
||
|
|
const debug = (...args) => {
|
||
|
|
};
|
||
|
|
function GracefulShutdown(server, opts) {
|
||
|
|
opts = opts || {};
|
||
|
|
const options = Object.assign(
|
||
|
|
{
|
||
|
|
signals: "SIGINT SIGTERM",
|
||
|
|
timeout: 3e4,
|
||
|
|
development: false,
|
||
|
|
forceExit: true,
|
||
|
|
onShutdown: (signal) => Promise.resolve(signal),
|
||
|
|
preShutdown: (signal) => Promise.resolve(signal)
|
||
|
|
},
|
||
|
|
opts
|
||
|
|
);
|
||
|
|
let isShuttingDown = false;
|
||
|
|
const connections = {};
|
||
|
|
let connectionCounter = 0;
|
||
|
|
const 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 Reflect.apply(callback, this, arguments);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const e of events) {
|
||
|
|
emitter.on(e, call);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const signals = options.signals.split(" ").map((s) => s.trim()).filter((s) => s.length > 0);
|
||
|
|
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((error) => {
|
||
|
|
debug("server shut down error occurred", error);
|
||
|
|
process.exit(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
function isFunction(functionToCheck) {
|
||
|
|
const 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) {
|
||
|
|
debug("Destroy Connections : " + (force ? "forced close" : "close"));
|
||
|
|
let counter = 0;
|
||
|
|
let secureCounter = 0;
|
||
|
|
for (const key of Object.keys(connections)) {
|
||
|
|
const socket = connections[key];
|
||
|
|
const serverResponse = socket._httpMessage;
|
||
|
|
if (serverResponse && !force) {
|
||
|
|
if (!serverResponse.headersSent) {
|
||
|
|
serverResponse.setHeader("connection", "close");
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
counter++;
|
||
|
|
destroy(socket);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
debug("Connections destroyed : " + counter);
|
||
|
|
debug("Connection Counter : " + connectionCounter);
|
||
|
|
for (const key of Object.keys(secureConnections)) {
|
||
|
|
const socket = secureConnections[key];
|
||
|
|
const serverResponse = socket._httpMessage;
|
||
|
|
if (serverResponse && !force) {
|
||
|
|
if (!serverResponse.headersSent) {
|
||
|
|
serverResponse.setHeader("connection", "close");
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
secureCounter++;
|
||
|
|
destroy(socket);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
debug("Secure Connections destroyed : " + secureCounter);
|
||
|
|
debug("Secure Connection Counter : " + secureConnectionCounter);
|
||
|
|
}
|
||
|
|
server.on("request", (req, res) => {
|
||
|
|
req.socket._isIdle = false;
|
||
|
|
if (isShuttingDown && !res.headersSent) {
|
||
|
|
res.setHeader("connection", "close");
|
||
|
|
}
|
||
|
|
res.on("finish", () => {
|
||
|
|
req.socket._isIdle = true;
|
||
|
|
destroy(req.socket);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
server.on("connection", (socket) => {
|
||
|
|
if (isShuttingDown) {
|
||
|
|
socket.destroy();
|
||
|
|
} else {
|
||
|
|
const 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 {
|
||
|
|
const id = secureConnectionCounter++;
|
||
|
|
socket._isIdle = true;
|
||
|
|
socket._connectionId = id;
|
||
|
|
secureConnections[id] = socket;
|
||
|
|
socket.once("close", () => {
|
||
|
|
delete secureConnections[socket._connectionId];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
process.on("close", () => {
|
||
|
|
debug("closed");
|
||
|
|
});
|
||
|
|
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);
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
function waitForReadyToShutDown(totalNumInterval) {
|
||
|
|
debug(`waitForReadyToShutDown... ${totalNumInterval}`);
|
||
|
|
if (totalNumInterval === 0) {
|
||
|
|
debug(
|
||
|
|
`Could not close connections in time (${options.timeout}ms), will forcefully shut down`
|
||
|
|
);
|
||
|
|
return Promise.resolve(true);
|
||
|
|
}
|
||
|
|
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 (force) {
|
||
|
|
destroyAllConnections(force);
|
||
|
|
}
|
||
|
|
return options.onShutdown(sig);
|
||
|
|
}).then(finalHandler).catch((error) => {
|
||
|
|
const errString = typeof error === "string" ? error : JSON.stringify(error);
|
||
|
|
debug(errString);
|
||
|
|
failed = true;
|
||
|
|
throw errString;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
function shutdownManual() {
|
||
|
|
return shutdown("manual");
|
||
|
|
}
|
||
|
|
return shutdownManual;
|
||
|
|
}
|
||
|
|
export default GracefulShutdown;
|