From 7474ae922781bceb05530f40569023ae5eba6a8c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:06:27 -0700 Subject: [PATCH 1/3] Fix slow shutdown on SIGINT/SIGTERM The process was hanging on Ctrl+C because: 1. The rate-limiter's setInterval kept the event loop alive (no .unref()) 2. No signal handlers existed to stop active Claude queries or disconnect Prisma Fixes: - Add .unref() to the rate-limiter cleanup interval - Add SIGINT/SIGTERM handlers that stop all active Claude queries and disconnect Prisma before exiting - A second Ctrl+C force-exits immediately Co-Authored-By: Claude Opus 4.6 (1M context) --- src/instrumentation.ts | 41 ++++++++++++++++++++++++++++ src/lib/rate-limiter.ts | 3 +- src/server/services/claude-runner.ts | 11 ++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 3b3ccd7d..b3a691b3 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -22,5 +22,46 @@ export async function register() { } catch (err) { console.error('Error reconciling sessions:', err); } + + // Register graceful shutdown handler + registerShutdownHandler(); } } + +/** + * Register signal handlers for graceful shutdown. + * Stops active Claude queries and disconnects Prisma so the process can exit cleanly. + */ +function registerShutdownHandler() { + let shuttingDown = false; + + const shutdown = async (signal: string) => { + if (shuttingDown) { + console.log(`Received ${signal} again, forcing exit`); + process.exit(1); + } + shuttingDown = true; + console.log(`Received ${signal}, shutting down gracefully...`); + + try { + // Stop all active Claude queries + const { stopAllSessions } = await import('@/server/services/claude-runner'); + await stopAllSessions(); + } catch (err) { + console.error('Error stopping sessions during shutdown:', err); + } + + try { + // Disconnect Prisma + const { prisma } = await import('@/lib/prisma'); + await prisma.$disconnect(); + } catch (err) { + console.error('Error disconnecting Prisma during shutdown:', err); + } + + process.exit(0); + }; + + process.on('SIGINT', () => void shutdown('SIGINT')); + process.on('SIGTERM', () => void shutdown('SIGTERM')); +} diff --git a/src/lib/rate-limiter.ts b/src/lib/rate-limiter.ts index 09f81f8c..6b115c4c 100644 --- a/src/lib/rate-limiter.ts +++ b/src/lib/rate-limiter.ts @@ -212,11 +212,12 @@ export class RateLimiter { export const loginRateLimiter = new RateLimiter(); // Start cleanup interval (every hour) +// Use .unref() so this timer doesn't prevent graceful shutdown if (typeof setInterval !== 'undefined') { setInterval( () => { loginRateLimiter.cleanup(); }, 60 * 60 * 1000 - ); + ).unref(); } diff --git a/src/server/services/claude-runner.ts b/src/server/services/claude-runner.ts index 71b99183..103b43fb 100644 --- a/src/server/services/claude-runner.ts +++ b/src/server/services/claude-runner.ts @@ -658,6 +658,17 @@ export async function stopSession(sessionId: string): Promise { sessions.delete(sessionId); } +/** + * Stop all active Claude queries. Called during graceful shutdown. + */ +export async function stopAllSessions(): Promise { + const sessionIds = [...sessions.keys()]; + if (sessionIds.length === 0) return; + + log.info('Stopping all active sessions for shutdown', { count: sessionIds.length }); + await Promise.allSettled(sessionIds.map((id) => stopSession(id))); +} + /** * Mark all running sessions as stopped. * Called on server startup since all in-memory state is lost. From a45daf6e228c499c0d6a481a1b62302d74df23a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:17:52 -0700 Subject: [PATCH 2/3] Add 10s timeout for graceful shutdown If stopAllSessions() or prisma.$disconnect() hangs, force exit after 10 seconds. This is especially important for SIGTERM from systemd where there's no second signal to trigger force-exit. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/instrumentation.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/instrumentation.ts b/src/instrumentation.ts index b3a691b3..f092019f 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -43,6 +43,13 @@ function registerShutdownHandler() { shuttingDown = true; console.log(`Received ${signal}, shutting down gracefully...`); + // Force exit after 10s if graceful shutdown hangs + // (important for SIGTERM from systemd where there's no second signal) + setTimeout(() => { + console.error('Graceful shutdown timed out, forcing exit'); + process.exit(1); + }, 10_000).unref(); + try { // Stop all active Claude queries const { stopAllSessions } = await import('@/server/services/claude-runner'); From c6794d955c0fbaa1390c6fde39c20442c7ee5411 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 23:20:23 -0700 Subject: [PATCH 3/3] Make stopSession synchronous (no awaits needed) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/server/services/claude-runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/services/claude-runner.ts b/src/server/services/claude-runner.ts index 103b43fb..0a4b4431 100644 --- a/src/server/services/claude-runner.ts +++ b/src/server/services/claude-runner.ts @@ -635,7 +635,7 @@ export async function markLastMessageAsInterrupted(sessionId: string): Promise { +export function stopSession(sessionId: string): void { const state = sessions.get(sessionId); if (!state) return;