Skip to content

Commit 6a32da1

Browse files
author
CryptoJones
committed
feat: graceful shutdown on SIGTERM / SIGINT (#20)
1 parent 83649e6 commit 6a32da1

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

server.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,62 @@ const server = app.listen(port, host, () => {
143143
const addr = server.address();
144144
log.info({ host: addr.address, port: addr.port }, 'Server listening');
145145
});
146+
147+
// ---- graceful shutdown ----
148+
//
149+
// SIGTERM is what `docker stop`, `systemctl stop`, and Kubernetes
150+
// pod-eviction all send. The default behavior is to drop in-flight
151+
// requests + leak pg pool connections. Trap it and drain instead.
152+
//
153+
// Sequence:
154+
// 1. server.close() — stops accepting new connections, lets the
155+
// ones already in flight finish (Node ≥18 honors keep-alive
156+
// headers and waits for the body).
157+
// 2. db.sequelize.close() — drains the pg pool cleanly.
158+
// 3. process.exit(0).
159+
//
160+
// If anything in the drain hangs longer than SHUTDOWN_TIMEOUT_MS
161+
// (default 25s — under most orchestrators' 30s SIGTERM→SIGKILL
162+
// window), we force-exit with code 1. SIGINT (Ctrl-C in dev) follows
163+
// the same path so dev shutdowns aren't dirty either.
164+
165+
const shutdownTimeoutMs = parseInt(process.env.SHUTDOWN_TIMEOUT_MS, 10) || 25_000;
166+
let shuttingDown = false;
167+
168+
async function shutdown(signal) {
169+
if (shuttingDown) {
170+
return;
171+
}
172+
shuttingDown = true;
173+
log.info({ signal }, 'received shutdown signal, draining');
174+
175+
// Force-exit if drain hangs.
176+
const killer = setTimeout(() => {
177+
log.error({ signal, timeoutMs: shutdownTimeoutMs }, 'drain timeout, force-exiting');
178+
process.exit(1);
179+
}, shutdownTimeoutMs);
180+
killer.unref();
181+
182+
try {
183+
// Stop accepting new connections.
184+
await new Promise((resolve, reject) => {
185+
server.close((err) => (err ? reject(err) : resolve()));
186+
});
187+
log.info('http server closed');
188+
} catch (err) {
189+
log.error({ err }, 'error closing http server');
190+
}
191+
192+
try {
193+
await db.sequelize.close();
194+
log.info('db pool closed');
195+
} catch (err) {
196+
log.error({ err }, 'error closing db pool');
197+
}
198+
199+
log.info('shutdown complete');
200+
process.exit(0);
201+
}
202+
203+
process.on('SIGTERM', () => shutdown('SIGTERM'));
204+
process.on('SIGINT', () => shutdown('SIGINT'));

0 commit comments

Comments
 (0)