@@ -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