Expand description
Pure helpers describing the RabbitMQ topology used by this backend.
For a logical queue q the backend maintains two long-lived broker queues
plus one short-lived hold queue per distinct delay:
| queue | role | arguments |
|---|---|---|
q | main work queue | x-message-ttl when QueueConfig::message_ttl is set, x-max-priority when QueueConfig::max_priority is Some |
q.dead | dead-letter queue | none |
q.deferred.{ttl_ms} | hold queue for one delay | x-message-ttl = ttl_ms, x-dead-letter-exchange = "", x-dead-letter-routing-key = q, x-expires = 2 * ttl_ms |
Every wait, whether a retry backoff, a delayed enqueue or a deferral, goes through a hold queue. There is no shared wait queue with per-message expirations, and this is why:
§Why hold queues instead of per-message expiration
A classic queue only ever expires the message at its head. Messages behind it are not examined until it has gone, so in a shared wait queue a message with a five-minute expiration at the head holds back every one-second expiration behind it (head-of-line blocking). Exponential backoff produces exactly that mix, so retries were the worst-hit case.
A hold queue never sets a per-message expiration. Instead the delay is
baked into the name of the queue the message waits in (q.deferred.30000
holds every 30-second wait for q), and the wait is the queue-wide
x-message-ttl. Every message in one hold queue therefore has the same TTL,
so they expire in exactly the order they were published and the head is
always the message that is due next. A short wait can never be stuck behind a
long one, because the two live in different queues. The price is one queue
per distinct delay, which is why delays are rounded up to a granularity:
RabbitMqOptions::retry_granularity
for retries and delayed enqueues,
RabbitMqOptions::deferred_granularity
for deferrals (both default to one second), so 29.2s and 30s share
q.deferred.30000.
Retries and deferrals share the hold queues: the arguments depend only on the
delay, and what differs between the two, the message priority, travels on the
message and only matters once it is back on q. A retry returns at priority
0 and joins the back of the queue; a deferral returns at the queue’s top
priority and overtakes the backlog.
§Why a hold queue’s arguments depend only on its name
The TTL is in the name, and every other argument is derived from it or from
the main queue, so two processes running different builds of an application
compute the same arguments for q.deferred.30000. That matters because
RabbitMQ refuses a declaration whose arguments differ from the existing
queue’s (PRECONDITION_FAILED, which closes the declaring channel): a
tunable in x-expires would deadlock the two processes against each other
for ever, one of them unable to schedule a single wait. Hence x-expires = 2 * ttl_ms
and nothing else: an idle hold queue deletes itself one TTL after the last
publish to it, and every declare resets that timer, which is why the
queue is redeclared before every publish.
x-expires must also be strictly greater than x-message-ttl, or the broker
could delete a queue that still owes a message. 2 * ttl_ms satisfies that
for every TTL up to MAX_DEFERRAL_MS, which is why a longer delay is
refused rather than silently clamped.
Everything in this module is pure: it never touches a connection, so it can be unit-tested without a broker.
Constants§
- ARG_
DEAD_ LETTER_ EXCHANGE - Queue argument naming the exchange used for dead-lettering.
- ARG_
DEAD_ LETTER_ ROUTING_ KEY - Queue argument naming the routing key used for dead-lettering.
- ARG_
EXPIRES - Queue argument making the broker delete a queue after it has been unused for that many milliseconds.
- ARG_
MAX_ PRIORITY - Queue argument declaring how many priority levels a queue supports.
- ARG_
MESSAGE_ TTL - Queue argument setting a queue-wide message time-to-live in milliseconds.
- DEFAULT_
DEAD_ SUFFIX - Default suffix appended to a queue name to build its dead-letter queue.
- DEFAULT_
DEFERRED_ SUFFIX - Default infix between a queue name and a hold queue’s TTL.
- HEADER_
ATTEMPT - Header mirroring
queuey_core::Envelope::attempton every publish. - HEADER_
ATTEMPTS - Header carrying the number of attempts made before dead-lettering.
- HEADER_
DEATH_ REASON - Header carrying the dead-lettering reason on messages routed to
q.dead. - HEADER_
DEFERRALS - Header mirroring
queuey_core::Envelope::deferralson every publish. - HEADER_
ORIGINAL_ QUEUE - Header carrying the originating queue name on messages routed to
q.dead. - MAX_
DEFERRAL_ MS - Longest delay this backend will hold, in milliseconds (~24.8 days).
- MAX_
TTL_ MS - Largest time-to-live RabbitMQ accepts, in milliseconds (~49.7 days).
Functions§
- dead_
queue_ args - Declaration arguments for the dead-letter queue
q.dead. - dead_
queue_ name - Name of the dead-letter queue for
queue. - deferred_
queue_ args - Declaration arguments for the hold queue
q.deferred.{ttl_ms}. - deferred_
queue_ name - Name of the hold queue holding
ttl_ms-long waits ofqueue. - deferred_
ttl_ ms - The hold queue TTL for
delay:delayrounded up to a whole multiple ofgranularity, at least one whole step, orNonewhen that lands pastMAX_DEFERRAL_MS. - queue_
args - Declaration arguments for the main queue
q.