feat: hold tasks in a per-worker in-flight list so none can be lost in transit - #17
Open
adhikjoshi wants to merge 1 commit into
Open
Conversation
…n transit
BLPOP removes the task from the queue and *then* writes it to the client. If
that write is lost, the task exists nowhere: not in ml_tasks, not in
processing_tasks (which is only populated after the reply arrives), and so
invisible to requeue_stuck_processing_tasks and every other recovery path.
This is not theoretical. Reproduced against production on 2026-08-15 by
blackholing a worker's connection at the Redis host and pushing a marked probe
task. Redis served it to the blocked client — the client's idle timer reset at
the moment of the push, so the pop and the write both happened — and the task
was then absent from the queue with no worker ever logging receipt. It very
likely explains the generation records seen stuck in "processing" with nothing
queued behind them.
BLMOVE makes taking the task and recording who took it a single atomic step:
- the worker moves the task into `inflight:{server_id}:{worker_id}` and holds
custody until it is done, whatever the outcome, via a finally
- every in-flight list is registered in a set, so recovery enumerates them
instead of SCANning for them
- at startup a server drains its own lists, which can only be debris from a
previous run of the same id
- the pruning loop drains lists whose owning server is no longer registered,
running after prune_inactive_servers so the dead are already deregistered
include_self is the one flag that must not be wrong. At startup our lists are
debris and must be drained; from the periodic sweep they belong to our own
running workers, and draining those would hand a live task to a second worker.
Both directions are tested.
drain_inflight is bounded by the list length read up front rather than looping
until empty, so it cannot spin if the no-appender assumption ever breaks.
Requires Redis 6.2+ for BLMOVE/LMOVE. The fleet runs 7.4.2.
The bounded-read test from the connection-liveness work now asserts the
timeout on BLMOVE rather than BLPOP; the property it pins is unchanged.
Mutation-tested: reverting to BLPOP, dropping the custody release, and letting
the sweep drain live workers' lists each turn a test red. The first two
initially survived against weaker assertions, which is why the custody test now
observes a genuinely in-flight task mid-run rather than inspecting list
lengths after the fact.
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.
The hole
BLPOPremoves the task from the queue and then writes it to the client. If that write is lost, the task exists nowhere:ml_tasks— it was poppedprocessing_tasks— that is only populated after the reply arrivesrequeue_stuck_processing_tasksand every other recovery pathThis is not theoretical — I reproduced it in production
On 2026-08-15, with a worker's connection blackholed at the Redis host (verified biting: 23 packets dropped Redis→worker, 92 worker→Redis):
ml_tasksidlereset at that exact moment — pop and write both happenedllen=0Both BLPOP clients were on the blackholed IP, so no other replica could have taken it. The task was destroyed.
This very likely explains the generation records seen stuck in
processingwith nothing queued behind them — 15 on text_to_audio and 20 on flux_2_dev that same morning.The fix
BLMOVEmakes taking the task and recording who took it one atomic step.inflight:{server_id}:{worker_id}and holds custody until done — whatever the outcome — via afinally.SCANning (this codebase has been bitten by SCAN storms before).prune_inactive_serversso the dead are already deregistered on the same pass.The one flag that must not be wrong
include_self. At startup our lists are debris and must be drained. From the periodic sweep they belong to our own running workers, and draining those would hand a live task to a second worker while the first is still executing it. Both directions have a test.drain_inflightis bounded by the list length read up front rather than looping until empty — an unbounded loop would spin the day the no-appender assumption breaks.Compatibility
Requires Redis 6.2+ for
BLMOVE/LMOVE. The fleet runs 7.4.2.Testing
10 new tests in
tests/test_inflight_custody.py; suite 62 → 72. The bounded-read test from #16 now asserts the timeout onBLMOVErather thanBLPOP— same property, new command.Mutation-tested, and worth being explicit about: reverting to
BLPOPand dropping the custody release initially survived. My first assertions only inspected list lengths after the fact, which is true either way — a test that passes both ways is worse than no test. The custody test now observes a genuinely in-flight task mid-run, holding the worker inside the task body with an event:BLMOVE→BLPOPlremcustody releaseWhat this does not fix
Only loss in transit to a worker. A worker that receives a task and then dies mid-execution is covered by the existing
processing_taskssweep, unchanged here. And this is orthogonal to #16: #16 stops workers stalling, this stops tasks vanishing during whatever stall remains.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.