214 lines
6.3 KiB
JavaScript
Raw Normal View History

2025-09-05 14:59:21 +08:00
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;