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;