( fastify: FastifyInstance, signal: string, exitCode = 0 )
| 28 | |
| 29 | // Graceful shutdown handler |
| 30 | export async function shutdown( |
| 31 | fastify: FastifyInstance, |
| 32 | signal: string, |
| 33 | exitCode = 0 |
| 34 | ) { |
| 35 | if (isShuttingDown()) { |
| 36 | logger.warn({ signal }, 'Shutdown already in progress, ignoring signal'); |
| 37 | return; |
| 38 | } |
| 39 | |
| 40 | logger.info({ signal }, 'Starting graceful shutdown'); |
| 41 | |
| 42 | setShuttingDown(true); |
| 43 | |
| 44 | // Hard deadline: if any close() below hangs (Kafka producer stuck, |
| 45 | // Redis still connecting, BullMQ wait), force-exit before Docker's |
| 46 | // stop_grace_period elapses. Without this the container can sit in |
| 47 | // "Stopping" until SIGKILL (exit 137) and look like a real crash. |
| 48 | const forceExitMs = Number(process.env.SHUTDOWN_FORCE_EXIT_MS || '15000'); |
| 49 | const forceExit = setTimeout(() => { |
| 50 | logger.error( |
| 51 | { signal, forceExitMs }, |
| 52 | 'Graceful shutdown timed out — forcing exit', |
| 53 | ); |
| 54 | process.exit(exitCode); |
| 55 | }, forceExitMs); |
| 56 | forceExit.unref(); |
| 57 | |
| 58 | // Step 1: Wait for load balancer to stop sending traffic (matches preStop sleep) |
| 59 | const gracePeriod = Number(process.env.SHUTDOWN_GRACE_PERIOD_MS || '5000'); |
| 60 | await new Promise((resolve) => setTimeout(resolve, gracePeriod)); |
| 61 | |
| 62 | // Step 2: Close Fastify to drain in-flight requests |
| 63 | try { |
| 64 | await fastify.close(); |
| 65 | logger.info('Fastify server closed'); |
| 66 | } catch (error) { |
| 67 | logger.error({ err: error }, 'Error closing Fastify server'); |
| 68 | } |
| 69 | |
| 70 | // Step 3: Destroy MCP sessions |
| 71 | try { |
| 72 | await mcpSessionManager.destroy(); |
| 73 | logger.info('MCP sessions closed'); |
| 74 | } catch (error) { |
| 75 | logger.error({ err: error }, 'Error closing MCP sessions'); |
| 76 | } |
| 77 | |
| 78 | // Step 4: Close database connections |
| 79 | try { |
| 80 | await db.$disconnect(); |
| 81 | logger.info('Database connection closed'); |
| 82 | } catch (error) { |
| 83 | logger.error({ err: error }, 'Error closing database connection'); |
| 84 | } |
| 85 | |
| 86 | // Step 5: Close ClickHouse connections |
| 87 | try { |
no test coverage detected