Retries and failure handling
What it is
Section titled “What it is”A task can fail for reasons that go away (a timeout, a 503, a lock) or reasons that won’t (bad input, a bug). gisila_queue gives you tools for each:
| Tool | Use for |
|---|---|
ctx.retry() |
A failure you recognise as temporary. You choose which errors retry. |
autoRetry: true |
Handlers where any exception is probably temporary. |
RejectTask() |
The task is pointless now (the record was deleted). Drop it quietly. |
RejectTask(requeue: true) |
Try again right away without using a retry (a resource is busy). |
timeLimit / softTimeLimit |
Stop waiting on hung work / let long work wind down. |
| Dead-letter queue | Keep what failed for good so you can fix and replay it. |
When to use it
Section titled “When to use it”- Network calls to services that blip:
ctx.retry(cause: e)on timeouts and 5xx. - Rate-limited APIs that answer 429 with a
Retry-After:ctx.retry(countdown: ...). - Idempotent tasks with simple failure modes:
autoRetry: trueplusmaxRetries. - Anything that must eventually succeed: keep the default dead-letter queue and alert on its depth.
When not to use it
Section titled “When not to use it”- Don’t retry validation errors or bugs; they fail the same way every time and delay the
dead letter. Let them throw (with
autoRetry: false) orRejectTask(). - Don’t use retries to poll for something (“is the export ready?”) with a huge
maxRetries. Use a periodic task or a chain. - Don’t retry non-idempotent side effects without a guard (see Delivery guarantees).
How it works
Section titled “How it works”A retry is a new copy of the message with retries + 1 and an ETA; the original is acked.
The task’s state becomes retry, then started again when the copy runs. No worker slot
is held during the wait.
The delay is countdown if you passed one, otherwise exponential backoff:
delay(n) = min(retryBackoff × 2^(n-1), retryBackoffMax) then, with retryJitter, uniform in [delay/2, delay]With defaults (1 s base, 10 min cap, jitter on), retries land at roughly 0.5–1 s, 1–2 s, 2–4 s, … Jitter spreads retries from many failing tasks so they don’t hit a recovering service at once.
When retries reaches maxRetries, the next failure is terminal: state failure, error
stored, message dead-lettered. AsyncResult.get() then throws TaskFailureException.
maxRetries travels in the message. TaskRef.delay/applyAsync copy the task’s setting;
app.send(), Beat and Rose’s fire action send 0 unless told otherwise.
Time limits: timeLimit makes the worker give up waiting and treat the run as a failure
(so it retries with autoRetry), but Dart cannot kill the handler, which keeps its slot
until it returns. Poll ctx.isAbandoned or ctx.softTimeLimitExceeded in loops.
Example
Section titled “Example”import 'package:gisila_queue/gisila_queue.dart';import 'package:http/http.dart' as http;
app.task<void>('webhooks.deliver', (ctx) async { final res = await http.post(Uri.parse(ctx.arg(0) as String), body: ctx.arg(1) as String); if (res.statusCode == 429) { final wait = int.tryParse(res.headers['retry-after'] ?? '') ?? 30; ctx.retry(countdown: Duration(seconds: wait)); } if (res.statusCode >= 500) ctx.retry(cause: 'HTTP ${res.statusCode}'); if (res.statusCode == 410) throw const RejectTask(reason: 'endpoint gone'); return null;}, maxRetries: 8, retryBackoff: const Duration(seconds: 5), timeLimit: const Duration(seconds: 30));