Add graceful shutdown - close BullMQ workers, HTTP server, and pools on SIGTERM #85
Labels
No labels
bug
duplicate
enhancement
future
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/TeaLeaves#85
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium · Confidence: High · Effort: S · Category: ops
Evidence
api/src/index.ts- no SIGTERM/SIGINT handler; workers started at :17-26 are never closed;app.listennever closed.api/src/services/mediaProcessorWorker.ts:23-51(ffmpeg/ffprobe),api/src/services/backup.ts:96,111,143(restic/pg_dump).Problem
On the documented deploy (
docker compose up --build, CLAUDE.md step 6) the container gets SIGTERM. Nothing callsworker.close()or closes the server/pools, so in-flight import/media/backup jobs are hard-killed, BullMQ locks linger, and ffmpeg/restic/pg_dump children can orphan.Impact
Interrupted backups/imports on every deploy; possible zombie children; a backup left stuck
status='running'which then blocks the/triggerguard (backup.ts route :44-49).Fix
Return worker refs from the
startX()functions; on SIGTERM/SIGINTawait Promise.all(workers.map(w => w.close())), close the HTTP server and pg/redis pools, then exit.Acceptance criteria
status='running'after a normal redeploy.Related: F-09 (backup timeouts), F-13 (migration ordering).
Filed from the 2026-07-15 codebase audit. Full report:
docs/.internal/report-2026-07-15.md(gitignored).Fixed in
b640072+eb07b31(v7.1.0 wave 3).Every
startX()now returns itsWorker;api/src/index.tscollects them and on SIGTERM/SIGINT: closes the HTTP server (stop accepting new requests) →await Promise.all(workers.map(w => w.close()))(each worker finishes its current job) → closes the Redis session client and the pg pool → exit 0. A 30 sSHUTDOWN_TIMEOUT_MShard cap forces exit rather than hanging.⚠️ The handler alone was inert — worth knowing. On first test,
docker compose stop apistill died withnpm error signal SIGTERMand exit code 1, and none of the[shutdown]logs appeared. The image ranCMD ["npm", "start"], and npm does not forward SIGTERM to its node child — so the handler never ran. Fixed ineb07b31:api/Dockerfileexecsnode dist/index.jsdirectly (node is the signal target).docker-compose.ymlsetsinit: true— tini as PID 1, which forwards the signal and reaps the orphaned restic/pg_dump/ffmpeg children this issue flags — plusstop_grace_period: 40sto allow the drain.Acceptance criteria (verified live on the dev server):
/sbin/docker-init, anddocker compose stop apinow logs[shutdown] SIGTERM received — draining in-flight work…→[shutdown] clean exit, exit code 0 (was 1).status='running'after a normal redeploy — workers drain cleanly, andreconcileStaleJobs()(#86) is the backstop for a hard crash.CI green (238/238 tests).