Skip to main content

Settlement and retries

What ack, drop, retry and retry_after become on RabbitMQ, where every settlement is a frame on the delivery's channel.

OutcomeRabbitQueue (classic)RabbitQuorumQueue
ackbasic.ack: the message leaves the queueThe same
dropbasic.reject without requeue: dead-lettered when the queue has a dead-letter exchange, dropped otherwiseThe same
retry()basic.reject with requeue: back in the queue, nothing countedbasic.reject with requeue: back in the queue, the server counts the delivery
retry() under max_attempts and dead_letterThe runtime publishes a counted copy, belowThe server's own delivery limit, below
retry_after(delay)A copy that waits, belowThe same with delay; an immediate requeue without it

The crate never sends basic.nack. On one delivery it does what basic.reject does, and since RabbitMQ 4.3 only the rejection counts toward a quorum queue's delivery limit. A batch handler's outcome settles each delivery of the batch with the same frame. A reply that arrives through a requester needs no settlement: it comes to a consumer without acknowledgements.

Retries​

A retry under max_attempts and dead_letter: a classic queue leaves the count to the runtime, a quorum queue counts on the serverThe handler returns retry()under max_attempts and dead_letterRabbitQueue:the runtime counts inx-ruststream-retry-countRabbitQuorumQueue:the server counts inx-delivery-countBelow thecap: a copyto the queueAt the cap:a copy tothe dead-letter queueBelow thelimit:rejected,requeuedPast thelimit: theserver dead-letters itBack in the queuefor another deliveryWith neither declared, retry() is basic.rejectwith requeue, on both queue types
The diagram as text
  1. The handler returns retry() under max_attempts and dead_letter → RabbitQueue: the runtime counts in x-ruststream-retry-count
  2. The handler returns retry() under max_attempts and dead_letter → RabbitQuorumQueue: the server counts in x-delivery-count
  3. RabbitQueue: the runtime counts in x-ruststream-retry-count → Below the cap: a copy to the queue: below
  4. RabbitQueue: the runtime counts in x-ruststream-retry-count → At the cap: a copy to the dead- letter queue
  5. RabbitQuorumQueue: the server counts in x-delivery-count → Below the limit: rejected, requeued: below
  6. RabbitQuorumQueue: the server counts in x-delivery-count → Past the limit: the server dead- letters it
  7. Below the cap: a copy to the queue → Back in the queue for another delivery
  8. Below the limit: rejected, requeued → Back in the queue for another delivery
  9. With neither declared, retry() is basic.reject with requeue, on both queue types

max_attempts(n) and dead_letter(name) at the mount site end a message that keeps failing. The first delivery counts as one attempt. Who counts depends on the queue type.

A classic queue counts nothing, so the runtime does. On a retry below the cap it rejects the original without requeue and publishes a copy to the default exchange under the queue's name, which lands at the back of the same queue. The copy carries the attempt number in the x-ruststream-retry-count header. At the cap the copy goes to the queue named by dead_letter instead, or, with no dead_letter, the delivery is rejected and nothing is published. On a classic queue that has a dead-letter exchange of its own, the rejection of each original reaches that exchange too.

A quorum queue counts its deliveries itself. The pair becomes the queue's x-delivery-limit (max_attempts minus one, since the server counts returns) and its dead-letter route to the dead_letter queue. A retry is a plain basic.reject with requeue, and the server dead-letters the message once it passes the limit, whether the service is running or not. The arguments reach the queue only when this service declares it, so the pair needs declare_topology(true), and the start fails without it. A quorum queue publishes no copies, so a retry publisher (out_retry) does not compile on it.

Delayed retries​

A delayed retry waits in the service process, in a TTL waiting queue or in a delayed-message exchange, and each sends the copy back to the queueThe handler returnsretry_after(delay)No delay set:a timer inthe serviceprocessDelay::dlx_ttl:<queue>.retrywith a TTLper messageDelay::plugin_dme:x-delay on aplugin exchangeThe queue the delivery came fromthe copy is a new messageThe timer in the process is lost if the process dies;a copy on the broker outlives the service
The diagram as text
  1. The handler returns retry_after(delay) → No delay set: a timer in the service process
  2. The handler returns retry_after(delay) → Delay::dlx_ttl: <queue>.retry with a TTL per message
  3. The handler returns retry_after(delay) → Delay:: plugin_dme: x-delay on a plugin exchange
  4. No delay set: a timer in the service process → The queue the delivery came from the copy is a new message: republished
  5. Delay::dlx_ttl: <queue>.retry with a TTL per message → The queue the delivery came from the copy is a new message: released
  6. Delay:: plugin_dme: x-delay on a plugin exchange → The queue the delivery came from the copy is a new message: released
  7. The timer in the process is lost if the process dies; a copy on the broker outlives the service

retry_after(delay) asks for a new delivery no sooner than delay. AMQP has no delay per message, so the wait happens in one of three places.

With no delay on a classic queue, the runtime holds the copy. It rejects the original and publishes the copy when a timer in the service fires. A graceful shutdown waits for the timer, and a process that dies before it fires loses the copy. On a quorum queue without delay the runtime has no copy to hold: the delivery is requeued at once, and a warning says the delay was dropped.

Delay::dlx_ttl keeps the copy on the broker. The crate publishes it to a waiting queue, <queue>.retry by default, with the delay as its per-message expiration, and acknowledges the original after it on the same channel. When the expiration passes, the waiting queue dead-letters the copy back to the origin queue. A queue releases expired messages only from its head, so a short delay behind a long one waits for the long one; one waiting queue per delay keeps them apart.

Delay::plugin_dme, under the plugin-dme feature, publishes the copy to an exchange of the delayed-message plugin, <queue>.delay by default, with the delay in its x-delay header. The plugin routes it to the origin queue once the delay has passed, and delays of different lengths do not wait for each other.

Either target is infrastructure. Under declare_topology(true) each subscription declares its own. Without declaration the subscription checks at startup that the target exists, and a missing one fails the start, because the server would drop a copy sent to a waiting queue that does not exist. The copy is a new message, so the server counts it from zero. It carries the x-ruststream-retry-count header raised by one, and on a classic queue the runtime reads the cap from it before the copy is sent: a delivery at the cap goes to the dead-letter queue instead of waiting again.

The rules on the server side are RabbitMQ's (negative acknowledgements, quorum queue delivery limit, dead lettering, per-message TTL, delayed message exchange).