Attempts Semantics¶
The attempts counter records how many times a job has already been executed. It is carried on the
queue envelope as attempts and surfaced to handlers via JobContext::$attempt.
Where it lives¶
Envelope (
attempts) — a 0-based counter of completed runs before the current one. A freshly enqueued message hasattempts = 0. Each requeue (nack) advances it by one.JobContext::$attempt— a 1-based view for the handler: it equalsenvelope->attempts + 1, so the very first execution seesattempt = 1.
JobDefinition describes the retry budget (maxRetries); the live attempt count travels on the
envelope, not on the definition.
Life cycle¶
A job is enqueued with
attempts = 0(never executed).The worker fetches the message and runs one attempt. The handler sees
JobContext::$attempt = attempts + 1.On success, the message is acked — the counter is irrelevant after that.
On failure with retries left, the worker
nacks the message; the backend requeues it withattemptsincremented, so the next delivery sees a higherattempt.When
attempts == maxRetriesand the attempt fails, the workerabandons the message (dead-letter). Total executions therefore equalmaxRetries + 1.
Why this model¶
The worker (not the runtime) owns the retry decision, so the counter advances exactly once per delivery — there is no double counting across backends.
Backoff strategies need a monotonic counter to compute the next delay;
RetryPolicy::computeDelay()is called with the next attempt number.Handlers can branch on
attempt(e.g. log differently on the final try) without knowing anything about the queue.
Reading the attempt in a handler¶
use Daycry\Jobs\Handlers\AbstractJobHandler;
use Daycry\Jobs\Execution\JobContext;
final class SyncStripe extends AbstractJobHandler
{
public function handle(JobContext $ctx): mixed
{
if ($ctx->attempt > 1) {
log_message('warning', "Retry #{$ctx->attempt} for {$ctx->name}");
}
// ... business logic ...
return 'ok';
}
}
Relation to backoff¶
The worker computes the requeue delay from the next attempt number:
delay = RetryPolicy::computeDelay(attemptIndex + 2)
where attemptIndex is the 0-based envelope attempts for the run that just failed. With the
exponential strategy the first retry delay equals retryBackoffBase. See Retries.
Relation to delivery guarantees¶
Delivery is at-least-once: if a worker crashes mid-run, reapExpired() (via jobs:queue:reap)
returns the message for redelivery. The redelivered message keeps its attempts, so a crash does
not silently burn a retry — but it does mean the same attempt may run more than once. Make handlers
idempotent, and use idempotencyKey() when a duplicate run must be prevented.