Skip to content

Upgrading to 0.2

0.2.0 changed the broker topology. Code changes are small; the topology needs a decision.

x-max-priority is a breaking topology change

Section titled “x-max-priority is a breaking topology change”

Queues are now declared with x-max-priority = 10 by default, because that is what lets a deferred job come back ahead of the backlog.

x-max-priority is a declaration argument, and RabbitMQ refuses to change the arguments of a queue that already exists. A queue declared by 0.1 has no priority argument, so redeclaring it with one closes the channel with PRECONDITION_FAILED.

Two ways forward:

Recreate the queues. Drain each queue first, delete it, and let the next declare recreate it with priorities. You get the full behaviour.

Or turn priorities off on the existing queues.

#[queue(max_priority = 0)]
Emails,

Deferral still works — the job waits exactly as long as it should — but held jobs come back FIFO instead of ahead of the backlog. This is the no-downtime option, and it can be reversed later whenever recreating the queue is convenient.

0.1 held retries in a single q.retry queue with per-message TTLs. That queue is no longer declared or used, and RabbitMqOptions::retry_suffix has been removed with it.

Retries now go into the same per-delay hold queues as deferrals, q.deferred.{ttl_ms}, for a concrete reason: RabbitMQ expires messages from the head of a queue, so in a shared retry queue a message with a long TTL blocks every shorter one behind it. Exponential backoff produces a wide spread of delays, which is the case that suffers most.

Replace retry_suffix with retry_granularity, which controls how delays are rounded and therefore how many hold queues can exist at once:

0.1
RabbitMqOptions::default().retry_suffix(".retry")
// 0.2
RabbitMqOptions::default().retry_granularity(Duration::from_secs(1))

After upgrading, q.retry is left over. Drain it before deleting it: anything still in there is a retry that has not fired yet. Messages in it are ordinary envelopes, so they can also be shovelled onto q to run immediately.

RabbitMqOptions::deferred_queue_grace no longer exists. x-expires is now fixed at 2 * ttl_ms, computed from the name alone.

That is deliberate rather than an oversight. Hold-queue arguments have to be a pure function of the queue name, so that every process declares the same queue identically and no two processes can deadlock each other with PRECONDITION_FAILED. A tunable grace period would break that.

Envelopes written by 0.1 decode fine: deferrals and priority are #[serde(default)], so an older envelope reads as zero deferrals at normal priority. You do not need to drain your work queues to upgrade.

  1. Decide the priority question above, and set max_priority = 0 on existing queues if you are not recreating them.
  2. Replace retry_suffix with retry_granularity, and delete any use of deferred_queue_grace.
  3. Deploy the workers.
  4. Deploy the producers.
  5. Drain and delete the leftover q.retry queues.

Workers before producers, as always: a producer that enqueues a job type no worker recognises gets it dead-lettered. See Dead letters.

  • Deferral, both handler-side (JobError::deferred_msg) and producer-side (Producer::defer).
  • #[queue(max_priority = ...)].
  • JobContext::deferrals and JobContext::priority.
  • MemoryBackend::deferred() for asserting on held jobs in tests.