fix: keep started_at through completion and stop re-queuing live tasks - #19
Merged
Merged
Conversation
Two coupled defects in in-flight task tracking.
1. started_at was erased on every completion. The worker writes it into
task_dict at pickup but never onto the Task object, and
_store_final_task_state() serialises from the object -- so the finished
record came back with started_at: null. Across 6,953 completed tasks
sampled from production, it survived on 127 (1.8%), and on zero tasks in
18 of 24 queues. Nothing downstream could separate queue wait from run
time, which is exactly the number you need to decide whether a slow
endpoint needs more replicas or a faster model.
2. requeue_stuck_processing_tasks() decided "stuck" from age alone, so a
healthy long job was re-queued and rendered a second time on another GPU.
On the wan22 text2video queue the median job takes 499s and p90 takes
949s against a 180s threshold; 7.8% of its tasks carry a rewritten
queued_at, the signature of a re-queue. ltx_2.3_server is at 1.0%,
flux_klein at 1.5%.
They have to ship together. Fixing (1) alone would make finished strays in
processing_tasks look like old stuck ones to the age rule, and re-running a
completed generation is worse than leaking a set member.
Liveness now comes from a heartbeat the owning worker republishes on the
existing heartbeat thread, so:
- a job that runs for 20 minutes is left alone as long as its worker is
alive, and
- a task whose worker actually died is picked up once its heartbeat goes
stale, which is sooner than the old age rule managed.
Tasks in a terminal state are drained from processing_tasks rather than
re-queued. That also clears the backlog the erased started_at had been
hiding: flux_klein's processing_tasks currently holds 61 ids of which 10 are
real -- 47 have already completed, the oldest is 23.7h old -- which inflates
every processing count that reads the set, including the one the Verda
autoscaler scales on.
A task with no heartbeat at all falls back to the age rule, so a fleet
running mixed versions behaves exactly as it does today and converges as
workers roll.
Side effect worth having: the sweep no longer GETs every in-flight payload
once a minute per worker just to decide to do nothing. A fresh heartbeat
short-circuits before the fetch.
Tanmaypatil123
force-pushed
the
fix/truthful-inflight-task-tracking
branch
from
August 20, 2026 07:43
233d32f to
5f5e993
Compare
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.
Two coupled defects in in-flight task tracking, found while timing the production generation pipeline end to end.
1.
started_atis erased on every completionThe worker writes the pickup time into
task_dictand saves it to Redis, but never sets it on theTaskobject:_store_final_task_state()then re-serialises from the object, overwriting the real value withnull:Measured on production Redis: of 6,953 completed tasks sampled across 24 queues,
started_atsurvived on 127 (1.8%) — and on zero tasks in 18 of the 24 queues.That is the only signal separating queue wait from actual run time, which is the number you need to tell "this endpoint needs more replicas" from "this model is slow". Reconstructing it required polling
processing_tasksat 1 Hz for 22 minutes.2. Healthy long jobs are re-queued and re-run
requeue_stuck_processing_tasks()asked "did this start more thanthresholdseconds ago?", which is not the same question as "is anyone still working on it?". A 500s video generation crosses the 180s default every time, gets pushed back ontoml_tasks, and a second GPU renders the same output again.queued_atvideo_server_ultra_wan22flux_kleinltx_2.3_serverWhy these ship together
Fixing (1) alone is a regression. Today a finished task left behind in
processing_taskshasstarted_at: null, so the age rule skips it. Oncestarted_atsurvives, that same stray looks exactly like an old stuck task — and gets re-run. Re-running a completed generation is worse than leaking a set member.What changed
Liveness is now a heartbeat the owning worker republishes on the existing heartbeat thread (
task_heartbeatshash, refreshed everyHEARTBEAT_INTERVAL):processing_tasksrather than re-queued.That last part also clears a backlog the erased
started_athad been hiding.flux_klein'sprocessing_taskscurrently holds 61 ids of which 10 are real — 47 have already completed, 50 are over an hour old, the oldest is 23.7h. Anything reading that set for a processing count (includingVerdaAutoscaler'squeued + processing) is reading a number 6× too large.Mixed-version safety: a task with no heartbeat falls back to the age rule, so a fleet running old and new workers side by side behaves exactly as it does today and converges as workers roll. Ordering is also tightened —
_store_final_task_state()leavesprocessing_tasksbefore publishing the terminal blob, so an old sweeper can never see "finished + old started_at" together.Side effect worth having: the sweep no longer GETs every in-flight payload once a minute on every worker just to decide to do nothing. A fresh heartbeat short-circuits before the fetch. On
flux_kleinthat set is 61 entries of multi-hundred-KB payloads, read by 10 workers every 60s.Tests
17 new tests in
tests/test_inflight_tracking.py, including two that run a real worker throughstart_workers(). Full suite: 83 passed.Mutation-tested — each fix removed, confirmed red, restored:
task.started_at = started_attest_started_at_survives_a_real_completion→assert None is not Nonetest_long_running_task_with_fresh_heartbeat_is_not_requeued+ 1 more failtest_finished_stray_is_drained_not_rerun+ 3 more fail_mark_task_inflightat pickuptest_a_task_is_marked_inflight_while_it_runsfailsNote on conflicts
Touches
worker_loopandprocess_task, so it will likely conflict with open PR #17 (per-worker in-flight list). Happy to rebase whichever lands second.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.