fix: bound RetryTaskException so permanently-failing tasks stop looping - #18
Merged
Merged
Conversation
RetryTaskException was re-queued unconditionally:
except RetryTaskException as e:
new_task_dict = task.to_dict()
new_task_dict["payload"] = task.original_payload
self.enqueue_delayed_task(new_task_dict, delay_seconds=self.delay_seconds)
There is no attempt counter, so a task whose failure is permanent never stops.
It re-enters the queue every delay_seconds, forever.
Observed in production on an image worker: over one 3,238-line log window,
120 tasks started and 119 requested a retry, spread across only 47 distinct
task ids - one task had been retried 9 times inside that window alone. Every
one had failed on a source image that no longer exists (HTTP 404 from an
ephemeral delivery URL, or HTTP 403 from a CloudFront signature that expired
3-4 days earlier). None of them could ever succeed, and together they were
consuming essentially the entire worker capacity of the container.
Bound the loop. The counter rides on the payload so it survives the round trip
through Redis, under a reserved "_retry_attempts" key so it cannot collide with
the caller-facing "retries" budget that the TaskProcessingError path already
decrements. Once the budget is spent the task is marked failed and taken
through the normal failure path (final state stored, error log, on_error
middleware, webhook), rather than being silently re-queued.
The limit is configurable via retry_task_max_attempts and defaults to 3, so
genuinely transient retries (a model that briefly fails to load) still work as
before while an unfixable task now dies after a few attempts.
Tests cover the budget being spent, the reserved counter leaving the caller's
own "retries" value untouched, a zero budget failing immediately, and the
default being finite.
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.
Problem
RetryTaskExceptionis re-queued unconditionally, with no attempt counter:A task whose failure is permanent therefore never stops. It re-enters the queue every
delay_seconds, forever.Impact measured in production
On one image worker, over a single 3,238-line log window:
Every failure was a source image that no longer exists — HTTP 404 from an ephemeral delivery URL, or HTTP 403 from a CloudFront signature that had expired 3–4 days earlier. None could ever succeed, and together they were consuming essentially the whole worker capacity of the container.
Fix
Bound the loop.
_retry_attemptskey, so it cannot collide with the caller-facingretriesbudget that theTaskProcessingErrorpath already decrements (base.py~864–877).on_errormiddleware, webhook — instead of being silently re-queued.retry_task_max_attempts, default 3, so genuinely transient retries (e.g. a model that briefly fails to load) behave as before.Tests
tests/test_retry_budget.pycovers:retriesvalue untouchedNo regressions; the 4 new tests are the difference.
Note
This caps the blast radius for every task type. The matching upstream fix — not classifying a permanent 4xx download failure as retryable in the first place — is handled separately in
dreambooth-server.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.