diff --git a/.env.example b/.env.example index 7a97539..4ee8386 100644 --- a/.env.example +++ b/.env.example @@ -87,3 +87,7 @@ PAGERDUTY_ROUTING_KEY= # Admin dashboard URL for DLQ inspection links in alerts ADMIN_DASHBOARD_URL=https://admin.neurowealth.io + +# Graceful shutdown +# Grace period (ms) for in-flight requests to complete before force-exit +SHUTDOWN_DRAIN_TIMEOUT_MS=30000 diff --git a/src/config/env.ts b/src/config/env.ts index a493863..de71282 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -274,4 +274,8 @@ export const config = { circuitBreakerThreshold: parseInt(process.env.HTTP_CLIENT_CIRCUIT_BREAKER_THRESHOLD || '5'), circuitBreakerResetMs: parseInt(process.env.HTTP_CLIENT_CIRCUIT_BREAKER_RESET_MS || '30000'), }, + shutdown: { + /** Grace period (ms) for in-force requests to complete before force-exit */ + drainTimeoutMs: parseInt(process.env.SHUTDOWN_DRAIN_TIMEOUT_MS || '30000'), + }, } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 26c87c8..a9add22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ const serviceStatus: Record = { let isShuttingDown = false let httpServer: Server | null = null -const REQUEST_DRAIN_TIMEOUT_MS = 30000 +let sessionCleanupHandle: NodeJS.Timeout | null = null function allServicesReady(): boolean { return Object.values(serviceStatus).every(s => s.ready) @@ -149,6 +149,13 @@ async function gracefulShutdown(signal: string): Promise { logger.info(`[Shutdown] Received ${signal}, initiating graceful shutdown...`) isShuttingDown = true + // Stop the session cleanup interval so it doesn't fire during shutdown + if (sessionCleanupHandle) { + clearInterval(sessionCleanupHandle) + sessionCleanupHandle = null + logger.info('[Shutdown] Session cleanup timer cleared') + } + if (!httpServer) { logger.warn('[Shutdown] No HTTP server to close') process.exit(0) @@ -180,11 +187,11 @@ async function gracefulShutdown(signal: string): Promise { } }) - // Force shutdown after timeout + // Force shutdown after configurable timeout setTimeout(() => { - logger.error('[Shutdown] Timeout reached, forcing shutdown...') + logger.error('[Shutdown] Grace period exhausted, forcing shutdown...') process.exit(1) - }, REQUEST_DRAIN_TIMEOUT_MS) + }, config.shutdown.drainTimeoutMs) } // ── Startup sequence ────────────────────────────────────────────────────────── @@ -279,7 +286,7 @@ async function main(): Promise { process.on('SIGINT', () => gracefulShutdown('SIGINT')) // Non-critical jobs start after the server is up - scheduleSessionCleanup() + sessionCleanupHandle = scheduleSessionCleanup() } // ── Process-level error guards ────────────────────────────────────────────────