Settlement
What ack, retry, retry_after and drop become on SQS: each outcome is one call against the
queue, and a retry waits in the queue itself.
The diagram as text
- Outcome: ack; retry(); retry_after(d); drop; dropped unsettled
- SQS call: DeleteMessage; ChangeMessage Visibility(0); ChangeMessage Visibility(d); DeleteMessage; at the cap: Visibility(0); No call, extension stops
- Effect: Gone; Visible now; Back after d, within 12 h of the receive; Gone, or to the dead-letter queue; Back when the timeout lapses
- ack → DeleteMessage
- DeleteMessage → Gone
- retry() → ChangeMessage Visibility(0)
- ChangeMessage Visibility(0) → Visible now
- retry_after(d) → ChangeMessage Visibility(d)
- ChangeMessage Visibility(d) → Back after d, within 12 h of the receive
- drop → DeleteMessage; at the cap: Visibility(0)
- DeleteMessage; at the cap: Visibility(0) → Gone, or to the dead-letter queue
- dropped unsettled → No call, extension stops
- No call, extension stops → Back when the timeout lapses
retry_after(d) counts the delay in whole seconds, rounded up, so the message never comes back
before d. A delay that reaches past the 12 hours from the receive is shortened to end there, and
the crate logs a warning that names the queue. A delivery dropped
without a settlement makes no call: its extension stops, and the message returns when its current
timeout lapses.
SQS has no verb that rejects a message without deleting it, so drop deletes. A queue whose
registration declares a redrive policy is the exception. There, a delivery received
maxReceiveCount times is returned instead of deleted, because the next receive is what moves it
to the dead-letter queue, and a delete would lose it.
The delivery's redelivery_count is the
queue's ApproximateReceiveCount, and the first delivery counts as one. It counts every receive,
a lapsed timeout included, not only the retries the service asked for.
Retries
The diagram as text
- A delivery its receive count says which attempt it is → The handler returns retry_after(d), or retry(): handled
- The handler returns retry_after(d), or retry() → ChangeMessageVisibility(d) the message waits in the queue: a retry
- ChangeMessageVisibility(d) the message waits in the queue → The next receive: has the message been received maxReceiveCount times?: after d
- The next receive: has the message been received maxReceiveCount times? → Not yet: delivered again, the same message: not yet
- The next receive: has the message been received maxReceiveCount times? → Yes: SQS moves it to the dead-letter queue: yes
- A drop on the last allowed receive returns the message with visibility 0, so SQS moves it rather than lose it
- The queue holds the waiting message: a crash of the service loses no retry
A retry never leaves the queue. retry_after(d) makes the message invisible for d, and it comes
back as the same message, with the same id and its receive count one higher. Nothing is
republished, so the runtime's republish fallback never runs here, and .out_retry(..) does not
compile on an SqsQueue. SQS refuses a visibility that ends
more than 12 hours after the receive.
max_attempts(n) and
dead_letter(q) together are the queue's
redrive policy: maxReceiveCount is n and the dead-letter queue is q. The subscription writes the
policy onto the queue when it opens, and SQS counts the receives and moves a spent message itself.
One half without the other is
SqsError::IncompleteRedrive at startup.
The queue holds a waiting message, so a crash of the service loses no retry: the message comes back when its visibility ends, to whichever consumer polls next.