[Fix] bullmq drains and aborts resumed Fast turns before shutting down - #2179
Merged
Conversation
Resumed Fast turns execute inside the bullmq process, but its shutdown only closed workers in sequence and never drained or aborted the turns, so a deploy could kill a resumed turn a second time with its durable claim and conversation lock held. The bullmq service now runs the same drain-then-abort sequence as the API, shared from cloud-agents, closing the Fast worker first so no new wakeups arrive while in-flight turns finish or hand back. R_BULLMQ_SHUTDOWN_DRAIN_MS sizes the window, falling back to R_API_SHUTDOWN_DRAIN_MS and then 20s.
Contributor
The drain never set durableRowId/durableResume on the lock it acquired, so a shutdown that aborted a resumed turn during setup had no row to release and the claim waited out its lease. Bind per row, unbind after delivery.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What the user sees today
You send a Fast message and a deploy lands while it is running. The API side is handled: it gets a 20s drain, and a turn that is not done hands itself off silently and picks up again on the queue worker within seconds (#2006, #2016). But the process that resumes it, the bullmq service, is being redeployed too, and it has no drain. When the platform kills it, the turn dies mid-run with its conversation lock and durable claim still held.
From the user's side the thread just goes quiet. The Session shows "responding" with no reply, no retry notice, and nothing to act on. Sending another message does not help, because the conversation lock belongs to a dead process. After about 10 minutes the lock expires and the turn resumes on its own, or after 15 minutes if the run had been going long enough to renew its claim. Any visible "Retrying in 30s" notice stays up for the whole wait.
This affects every deploy that lands during a Fast turn, which is exactly the population the drain work was built to protect. Without this fix, a good share of the turns the API path "saved" were dying a second time and looking hung.
Why
Durable admission (#2016, #2145, #2156) makes an interrupted Fast turn resume on the next process. The API hands the turn back on SIGTERM: it closes admissions, drains, aborts the stragglers, and each aborted turn releases its durable claim and wakes the queue.
The queue's consumer is the bullmq service, and it runs the resumed turn in-process. Its shutdown closed about twenty workers one after another with a waiting
close()and never called the drain or abort, with the Fast worker nineteenth in the sequence. On a deploy both services restart:The guarantee in practice covered one restart, not a deploy.
Change
drainAndAbortFastAgentTurnsandresolveFastAgentShutdownDrainMsin@roomote/cloud-agentshold the drain-then-abort ordering both services must follow. The API'sgraceful-shutdown.tsdelegates to it with the same options and log line as before.apps/bullmq/src/graceful-shutdown.ts: close admissions, close the Fast parent-event worker as the drain starts so no new wakeups are fetched, let in-flight turns finish inside the window, abort the stragglers so each hands its row back, then close everything else. Jobs that arrive during the drain hit the refused lock, throw the busy error, and land in the delayed state for the new process. A worker that ignores its abort cannot hold shutdown open: the post-abort wait for its close is bounded (5s), since SIGKILL is coming anyway. Double signal forces exit, as in the API.durableRowIdanddurableResumeon the lock for each inline row before delivery and clears them after, so a shutdown that aborts a resumed turn during its setup (before the turn's own abort handling) can still release the claim and wake the queue. (From review.)R_BULLMQ_SHUTDOWN_DRAIN_MS, falling back toR_API_SHUTDOWN_DRAIN_MS, then 20s. Documented inenvironment-variables.mdxand.env.production.example.FastAgentProcessShutdownErrornow says "process shutdown" rather than "API shutdown" and exposessignal.What the user sees after
The worker gets the same window to finish, and anything still running is aborted so it hands itself back immediately. The next process resumes it within seconds, the same as the API path. Worst case is a slightly slower answer with no visible interruption, instead of a silent 10 to 15 minute hang.
Follow-up for operators
The bullmq service needs a SIGTERM-to-SIGKILL grace window longer than its drain window, the same adjustment the API service received. That lives in the deployment configuration, not in this repo.
Testing
fast-agent-turn-shutdown.test.ts: resolver defaults, kill switch, key fallback; the sequence closes admissions, starts the parallel close, waits, then aborts, and stays quiet when everything settles.apps/bullmq/src/graceful-shutdown.test.ts: Fast worker closed as the drain starts and the rest closed only after the abort; bounded wait on a worker that never closes; exit still happens when the remaining closes fail; double-signal force exit.fast-agent-parent-event-queue.test.ts: a resumed inline row is bound to the lock during delivery, its wake callback queues the right job, and the binding is cleared afterwards.graceful-shutdown.test.tsunchanged in substance (7 tests) and now runs against the real shutdown error instead of a module mock.pnpm lint:fast,pnpm check-types:fast,pnpm knipall pass.Not exercised: a live deploy mid-resumed-turn. The mechanics on the turn side (abort → hand-back → wake) are the ones the API path already smoke-tested in #2016 and #2145; this change only makes bullmq trigger them.