Skip to content
This repository was archived by the owner on Dec 17, 2022. It is now read-only.

Commit 50e358e

Browse files
committed
Add a cleaner shutdown routine (hopefully, but it is still buggy)
1 parent 2f4ad68 commit 50e358e

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

beforeShutdown.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Inspired by: https://stackoverflow.com/a/64028857/975333
2+
3+
import process from 'node:process';
4+
5+
/**
6+
* @callback BeforeShutdownListener
7+
* @param {string} [signalOrEvent] The exit signal or event name received on the process.
8+
*/
9+
10+
/**
11+
* System signals the app will listen to initiate shutdown.
12+
* @const {string[]}
13+
*/
14+
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'];
15+
16+
/**
17+
* Time in milliseconds to wait before forcing shutdown.
18+
* @const {number}
19+
*/
20+
const SHUTDOWN_TIMEOUT = 15_000;
21+
22+
/**
23+
* A queue of listener callbacks to execute before shutting
24+
* down the process.
25+
* @type {BeforeShutdownListener[]}
26+
*/
27+
const shutdownListeners = [];
28+
29+
/**
30+
* Listen for signals and execute given `fn` function once.
31+
* @param {string[]} signals System signals to listen to.
32+
* @param {function(string)} fn Function to execute on shutdown.
33+
*/
34+
const processOnce = (signals, fn) => {
35+
for (const sig of signals) {
36+
process.once(sig, fn);
37+
}
38+
};
39+
40+
/**
41+
* Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
42+
* @param {number} timeout Time to wait before forcing shutdown (milliseconds)
43+
*/
44+
const forceExitAfter = timeout => () => {
45+
setTimeout(() => {
46+
// Force shutdown after timeout
47+
console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
48+
return process.exit(1);
49+
}, timeout).unref();
50+
};
51+
52+
/**
53+
* Main process shutdown handler. Will invoke every previously registered async shutdown listener
54+
* in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will
55+
* be logged out as a warning, but won't prevent other callbacks from executing.
56+
* @param {string} signalOrEvent The exit signal or event name received on the process.
57+
*/
58+
async function shutdownHandler(signalOrEvent) {
59+
console.warn(`Shutting down: received [${signalOrEvent}] signal`);
60+
61+
for (const listener of shutdownListeners) {
62+
try {
63+
await listener(signalOrEvent);
64+
} catch (error) {
65+
console.warn(`A shutdown handler failed before completing with: ${error.message || error}`);
66+
}
67+
}
68+
69+
return process.exit(0);
70+
}
71+
72+
/**
73+
* Registers a new shutdown listener to be invoked before exiting
74+
* the main process. Listener handlers are guaranteed to be called in the order
75+
* they were registered.
76+
* @param {BeforeShutdownListener} listener The shutdown listener to register.
77+
* @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
78+
*/
79+
function beforeShutdown(listener) {
80+
shutdownListeners.push(listener);
81+
return listener;
82+
}
83+
84+
// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds
85+
// This prevents custom shutdown handlers from hanging the process indefinitely
86+
processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT));
87+
88+
// Register process shutdown callback
89+
// Will listen to incoming signal events and execute all registered handlers in the stack
90+
processOnce(SHUTDOWN_SIGNALS, shutdownHandler);
91+
92+
export default beforeShutdown;

0 commit comments

Comments
 (0)