Skip to main content

Module topology

Module topology 

Source
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:

queuerolearguments
qmain work queuex-message-ttl when QueueConfig::message_ttl is set, x-max-priority when QueueConfig::max_priority is Some
q.deaddead-letter queuenone
q.deferred.{ttl_ms}hold queue for one delayx-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::attempt on 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::deferrals on 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 of queue.
deferred_ttl_ms
The hold queue TTL for delay: delay rounded up to a whole multiple of granularity, at least one whole step, or None when that lands past MAX_DEFERRAL_MS.
queue_args
Declaration arguments for the main queue q.