Skip to content

Reset retry counters on queue:retry - #19

Merged
necessarylion merged 1 commit into
mainfrom
fix-retry-counters
Nov 6, 2026
Merged

necessarylion merged 1 commit into
mainfrom
fix-retry-counters

Conversation

@mydoxie

@mydoxie mydoxie commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Problem

queue:retry put a failed job back with its old exceptions count and its old retryUntil time (from #18).

Effect

  • A job with maxExceptions: 3 that already had 3 errors failed again at its first error.
  • A job past its retryUntil time got only one try.

Fix

A retried job now starts fresh, like a new dispatch:

  • The tries and the exceptions count go back to 0.
  • retryUntil is read again from the job class. At dispatch it also comes from the job class (job.retryUntil), so the job gets a new time window. Laravel does the same in its retry command.
  • Jobs with no retryUntil are not decoded on retry. Nothing changes for them.
  • If the job can not be read (unknown name, bad key), retryUntil is left empty. The worker then fails the job with the real error.

The change is in JobPayload.retried(). Both drivers (database and Redis) use it.

Tests

  • New tests for every store (SQLite, Postgres, MariaDB, Redis): a retried job gets its full maxExceptions again, and a retried job gets a new retryUntil window.
  • Both tests fail without the fix.
  • Full suite passes with all four stores. dart analyze --fatal-infos and dart format are clean.

A retried job kept its old exceptions count and its old retryUntil.
So it failed at its first error, or got only one try.
Now queue:retry resets the tries and the exceptions count.
It also reads retryUntil again from the job class, like a new dispatch.
@mydoxie

mydoxie commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Bot review

Verdict: OK. Nothing blocking.

Blocking

None.

Checked

  • Both drivers (database and redis) now call retried(). Tries and the exceptions count go back to 0.
  • A job saved by the released version (no exceptions, no retryUntil) still retries. It is not decoded, and its saved text has no new fields.
  • A job that cannot be read (unknown job name, or bad or encrypted data that cannot be decrypted) with retryUntil set: retry still works. retry all still retries the other jobs. The worker then fails the job once with the real error (No job class named ... or DecryptException). It does not loop.
  • Decoding on retry runs the same job constructor and retryUntil getter as the worker and dispatch, so no new side effects.
  • Both new tests fail without the fix (8 failures: 2 tests x 4 stores).

Non-blocking

  1. lib/src/queue.dart:208: retryUntilOf hides every error. Sometimes only the retry process cannot read the job, for example because of a different app key or job registry, but the worker can. Then the job loses retryUntil without any message. It falls back to tries, so it may run only once. This is finite, not an endless loop. A one-line QueueLog.warn in the catch would make this easy to see.
  2. lib/src/queue_driver.dart:4: queue_driver.dart now imports queue.dart, so the payload class depends on the global Queue.config. Another option: the drivers could pass the value in, like retried(Queue.retryUntilOf(payload)). This is only about style.

Checks

  • dart test (SQLite, Postgres 16, MariaDB 11, Redis 7): all 253 pass.
  • dart analyze --fatal-infos: no issues. dart format --set-exit-if-changed .: no changes.
  • GitHub CI test: pass.
  • CHANGELOG is under ## Unreleased. There is no version bump.

@necessarylion
necessarylion merged commit 74a7f6d into main Nov 6, 2026
1 check passed
@necessarylion
necessarylion deleted the fix-retry-counters branch November 6, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants