Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
}
17 changes: 12 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const serviceStatus: Record<string, ServiceStatus> = {

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)
Expand Down Expand Up @@ -149,6 +149,13 @@ async function gracefulShutdown(signal: string): Promise<void> {
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)
Expand Down Expand Up @@ -180,11 +187,11 @@ async function gracefulShutdown(signal: string): Promise<void> {
}
})

// 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 ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -279,7 +286,7 @@ async function main(): Promise<void> {
process.on('SIGINT', () => gracefulShutdown('SIGINT'))

// Non-critical jobs start after the server is up
scheduleSessionCleanup()
sessionCleanupHandle = scheduleSessionCleanup()
}

// ── Process-level error guards ────────────────────────────────────────────────
Expand Down
Loading