From d5417cd6c010e1a6e36968d49aee27734c87886b Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 21 Jun 2026 10:41:42 +0000 Subject: [PATCH] fix(server): skip the native close on analyze-worker SIGTERM cancellation (#2264 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cancelJob() / the 30-min timeout (analyze-job.ts) send SIGTERM to the forked analyze worker, but its SIGTERM handler still did a full `await closeLbug()` (native conn/db teardown) — even though normal completion now skips it via skipNativeCloseOnExit. A cancelled or timed-out --pdg server analyze could therefore still hit the LadybugDB ClientContext destructor double-free, or block behind the in-flight COPY's connection lock before exiting. Mirror the CLI SIGINT path: a best-effort CHECKPOINT with closeLbug({ skipNativeClose: true }) bounded by a 2s Promise.race timeout, then process.exit(0) (which reclaims the handles). A CHECKPOINT failure is reported to the parent over IPC rather than swallowed; the exit is in .finally so it always fires. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm --- gitnexus/src/server/analyze-worker.ts | 33 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/gitnexus/src/server/analyze-worker.ts b/gitnexus/src/server/analyze-worker.ts index 6dd1f4e21..5054682c0 100644 --- a/gitnexus/src/server/analyze-worker.ts +++ b/gitnexus/src/server/analyze-worker.ts @@ -73,19 +73,26 @@ process.on('unhandledRejection', (reason: unknown) => { } }); -// Handle graceful shutdown — notify the parent over IPC, then exit. A cleanup -// failure is reported to the parent (not swallowed), and the exit lives in -// `finally` so it always fires even if a send() throws on a closed channel. -process.on('SIGTERM', async () => { - try { - send({ type: 'error', message: 'Analysis cancelled (worker received SIGTERM)' }); - await closeLbug(); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Worker cleanup failed during SIGTERM'; - send({ type: 'error', message }); - } finally { - process.exit(0); - } +// Handle cancellation / timeout shutdown (analyze-job.ts `cancelJob` sends +// SIGTERM). Mirror the CLI SIGINT path (#2264): a best-effort CHECKPOINT that +// SKIPS the native close. A real conn.close()/db.close() here can double-free in +// LadybugDB's ClientContext destructor after --pdg writes, AND it would block +// behind the in-flight COPY's connection lock — so a single cancel could abort +// the worker or hang until the COPY releases. Bound the CHECKPOINT with a short +// timeout; process.exit reclaims the native handles regardless. A CHECKPOINT +// failure is reported to the parent over IPC, not swallowed; the exit lives in +// `finally` so it always fires. +const SIGTERM_CLEANUP_TIMEOUT_MS = 2000; +process.on('SIGTERM', () => { + send({ type: 'error', message: 'Analysis cancelled (worker received SIGTERM)' }); + void Promise.race([ + closeLbug({ skipNativeClose: true }).catch((err: unknown) => { + const message = + err instanceof Error ? err.message : 'Worker checkpoint failed during SIGTERM'; + send({ type: 'error', message }); + }), + new Promise((resolve) => setTimeout(resolve, SIGTERM_CLEANUP_TIMEOUT_MS)), + ]).finally(() => process.exit(0)); }); // Listen for start command from parent — guarded against re-entry