Skip to main content

queuey_rabbitmq/
lib.rs

1//! RabbitMQ backend for [`queuey`], built on [`lapin`].
2//!
3//! [`RabbitMqBackend`] implements [`queuey_core::Backend`]: it owns one
4//! AMQP connection, publishes with publisher confirms, and hands the worker
5//! runtime a stream of [`RabbitMqDelivery`] values.
6//!
7//! ```no_run
8//! use std::{sync::Arc, time::Duration};
9//!
10//! use queuey_core::{Backend, QueueConfig};
11//! use queuey_rabbitmq::{RabbitMqBackend, RabbitMqOptions};
12//!
13//! # async fn example() -> queuey_core::Result<()> {
14//! let backend = RabbitMqBackend::with_options(
15//!     "amqp://guest:guest@localhost:5672/%2f",
16//!     RabbitMqOptions::default().retry_suffix(".retry"),
17//! )
18//! .await?;
19//!
20//! let emails = QueueConfig::new("myapp.emails").prefetch(10);
21//! backend.declare(std::slice::from_ref(&emails)).await?;
22//!
23//! let backend = Arc::new(backend);
24//! // ... hand `backend` to a `Producer` / `Worker` ...
25//! backend.close().await?;
26//! # Ok(()) }
27//! ```
28//!
29//! # Topology
30//!
31//! Each logical queue `q` is backed by three broker queues, `q`, `q.retry` and
32//! `q.dead`, plus a short-lived *hold* queue `q.deferred.{ttl_ms}` per distinct
33//! deferral delay. See [`topology`] for the exact arguments and for the
34//! head-of-line caveat that comes with TTL-based retry queues.
35//!
36//! # Deferral
37//!
38//! [`Backend::defer`](queuey_core::Backend::defer) and
39//! [`Delivery::defer`](queuey_core::Delivery::defer) hold a job for a
40//! delay and then put it back on `q` **ahead of the backlog**. That is the shape a
41//! `429 Too Many Requests` with `Retry-After: 30` needs, where the job did not
42//! fail and must not burn an attempt.
43//!
44//! Two mechanisms do that:
45//!
46//! * **Hold queues.** A deferral is published to `q.deferred.{ttl_ms}`, a queue
47//!   whose whole purpose is to dead-letter its contents back onto `q` after
48//!   `ttl_ms`. The delay is the queue's `x-message-ttl`, never a per-message
49//!   `expiration`, so every message in it expires in publish order and short
50//!   deferrals are never stuck behind long ones. The delay is rounded up to
51//!   [`RabbitMqOptions::deferred_granularity`] (default `1s`) to bound how many
52//!   such queues exist, and the queue is declared on demand right before each
53//!   deferred publish: an idle hold queue deletes itself one TTL after the last
54//!   deferred publish to it (`x-expires = 2 * TTL`), and every declare resets
55//!   that timer.
56//! * **Priorities.** `q` is declared with `x-max-priority` from
57//!   [`QueueConfig::max_priority`](queuey_core::QueueConfig::max_priority)
58//!   (default `Some(10)`), and every publish carries the envelope's `priority`.
59//!   Normal work is `0`; a deferred envelope carries the queue's top level, so
60//!   when it comes back it is served before everything that piled up meanwhile.
61//!   "Ahead of the backlog" means ahead of what is still *on* the queue: a
62//!   consumer with prefetch `N` already holds up to `N` backlog messages, and
63//!   the returning deferral is first among what is left.
64//!
65//! ## What deferral requires
66//!
67//! * **The queue must have been declared through this backend, in this
68//!   process.** Otherwise the hold queue's durability and the queue it
69//!   dead-letters back to would be guesses, and a TTL expiry into a queue that
70//!   does not exist is discarded silently by the broker. Unlike a
71//!   `mandatory` publish, nothing comes back and nothing is logged. Deferring
72//!   onto an unknown queue is
73//!   [`Error::UnknownQueue`](queuey_core::Error::UnknownQueue)
74//!   instead. `Producer::new` and `WorkerBuilder::build` declare the queue set;
75//!   `Producer::new_undeclared` deliberately does not.
76//! * **The delay must fit.** It is capped at
77//!   [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS), about 24.8 days. That is half of
78//!   what a 32-bit millisecond TTL can express, because the hold queue's
79//!   `x-expires` is twice its TTL. A longer delay is refused rather than
80//!   clamped: releasing a job early is the one thing a deferral promises not to
81//!   do. Rounding up to the granularity happens first, so a delay just under the
82//!   cap can be refused too.
83//!
84//! Both failures happen *before* anything is acked, so from
85//! [`Delivery::defer`](queuey_core::Delivery::defer) they leave the
86//! original message unacknowledged and the broker redelivers it.
87//!
88//! ## Breaking topology change
89//!
90//! `x-max-priority` is a *declaration* argument, and RabbitMQ refuses to change
91//! the arguments of a queue that already exists: the declaration is answered
92//! with `PRECONDITION_FAILED`, which closes the channel and surfaces here as an
93//! error from [`declare`](queuey_core::Backend::declare).
94//!
95//! A `q` created before this feature has no `x-max-priority`, so **declaring it
96//! again with the default config will fail**. Either:
97//!
98//! * drain and delete `q`, then let this backend redeclare it. Deferred jobs
99//!   then come back ahead of the backlog; or
100//! * set `max_priority = 0` on the queue's
101//!   [`QueueConfig`](queuey_core::QueueConfig) (or
102//!   `#[queue(max_priority = 0)]`), which declares `q` exactly as before.
103//!   Deferral still works, it just returns jobs FIFO instead of ahead of the
104//!   queue.
105//!
106//! `q.retry`, `q.dead` and hold queues are unchanged, so only `q` is affected.
107//!
108//! # Guarantees
109//!
110//! * Every publish (enqueue, retry, defer, dead-letter) is `mandatory` and
111//!   confirmed by the broker before it is reported as successful. A routing key
112//!   that matches no queue is returned by the broker and reported as an error
113//!   rather than passing for a confirmed publish.
114//! * [`Delivery::retry`](queuey_core::Delivery::retry),
115//!   [`Delivery::defer`](queuey_core::Delivery::defer) and
116//!   [`Delivery::dead_letter`](queuey_core::Delivery::dead_letter)
117//!   publish first and ack second, and skip the ack entirely when the publish
118//!   fails, so a job is never lost. At worst it is redelivered. With
119//!   [`RabbitMqOptions::declare_dead_letter_queues`] off, `dead_letter` rejects
120//!   the delivery instead of publishing to a `q.dead` this backend does not own.
121//! * Messages whose body is not a valid [`queuey_core::Envelope`] are
122//!   moved aside and logged, never surfaced as a stream error, so one poison
123//!   message cannot stall a consumer.
124//!
125//! # Not in scope for v1
126//!
127//! Reconnection. When the connection drops, consumer streams end and further
128//! calls fail; supervising and rebuilding the backend is the caller's job.
129//!
130//! [`queuey`]: queuey_core
131
132#![forbid(unsafe_code)]
133#![warn(missing_docs)]
134
135mod backend;
136mod delivery;
137mod error;
138mod options;
139mod publisher;
140
141pub mod codec;
142pub mod topology;
143
144pub use backend::RabbitMqBackend;
145pub use delivery::RabbitMqDelivery;
146pub use options::RabbitMqOptions;
147
148/// Re-export of the `lapin` version this backend is built against, so callers
149/// can name [`lapin::ConnectionProperties`] without pinning it themselves.
150pub use lapin;