Recipes
A server that stops gracefully
Stop accepting connections on SIGTERM, flush the access log first, and still die of the signal so the orchestrator sees a clean stop — with a deadline that bounds a connection that will not close.
An orchestrator sends SIGTERM and waits a grace period before SIGKILL. In that window a server should flush what it has buffered, stop accepting connections, and leave.
import { createServer } from 'node:http';
import { install } from 'closeout';
const { onExit } = install({ deadline: 5000 });
const server = createServer((_req, res) => res.end('ok'));
const log = [];
onExit(() => console.log(`flushed ${log.length} log lines`), 'flush');
onExit(
() =>
new Promise((resolve) => {
server.close(() => {
console.log('stopped accepting connections');
resolve();
});
}),
{ label: 'close-the-server' },
);
server.listen(0, () => {
log.push('listening');
console.log('listening');
process.kill(process.pid, 'SIGTERM'); // the orchestrator
});listening
flushed 1 log lines
stopped accepting connectionsflushbeforerelease. The log is written while the server is still up, and the close starts only after the flush has settled.- Set the deadline inside the grace period. Kubernetes waits 30 seconds by default; a
deadline of 5 seconds leaves the rest for the process to actually leave. A keep-alive
connection that holds
server.close()open is then named on stderr —close-the-server— instead of eating the grace period and ending in SIGKILL. - It still dies of SIGTERM. The orchestrator, and anything reading its status, sees a process that stopped on request rather than one that exited with a code.
Keep the evidence on a crash
Remove a scratch directory on a clean exit and keep it when the program crashed, by reading which door the program left by.
Testing shutdown
Test exit handlers without killing the test runner: drive a createRegistry directly, or install closeout against a fake process and assert how it was asked to leave.