queuey_rabbitmq/backend.rs
1//! The [`Backend`] implementation.
2
3use std::{
4 sync::{
5 Arc,
6 atomic::{AtomicU64, Ordering},
7 },
8 time::{Duration, SystemTime, UNIX_EPOCH},
9};
10
11use async_trait::async_trait;
12use futures::StreamExt;
13use lapin::{
14 Channel, Connection, Consumer,
15 message::Delivery as LapinDelivery,
16 options::{
17 BasicAckOptions, BasicConsumeOptions, BasicQosOptions, BasicRejectOptions,
18 QueueDeclareOptions,
19 },
20 types::{FieldTable, MAX_SHORT_STRING_LENGTH},
21};
22use queuey_core::{Backend, Delivery, DeliveryStream, Envelope, QueueConfig, Result};
23use tracing::{debug, error, info, warn};
24
25use crate::{
26 codec,
27 delivery::RabbitMqDelivery,
28 error::{RabbitMqError, amqp, short_string},
29 options::RabbitMqOptions,
30 publisher::{Hold, Publisher},
31 topology,
32};
33
34/// AMQP reply code for a normal, operator-initiated close.
35const REPLY_SUCCESS: u16 = 200;
36
37/// A [`Backend`] backed by a single RabbitMQ connection.
38///
39/// * One [`Connection`].
40/// * One publishing [`Channel`] in confirm mode, shared behind a
41/// [`tokio::sync::Mutex`]. Every publish (enqueue, retry, defer, dead-letter)
42/// is `mandatory` and waits for the broker's confirmation. The channel is
43/// reopened lazily if a channel exception closed it. Nothing is ever
44/// *declared* on it.
45/// * One long-lived [`Channel`] for the hold queue declarations every retry,
46/// delayed publish and [`defer`](Backend::defer) makes on demand. A
47/// declaration is the one thing the broker routinely refuses
48/// (`PRECONDITION_FAILED` closes the channel it ran on), so it is kept away
49/// from the publishes it would otherwise take down with it.
50/// * One fresh [`Channel`] per [`consume`](Backend::consume) call, so each
51/// consumer gets its own `basic_qos` prefetch window and a failure on one
52/// consumer cannot take down the others.
53/// * One short-lived channel per [`declare`](Backend::declare) call, so a
54/// rejected declaration (e.g. re-declaring an existing queue with different
55/// arguments, which RabbitMQ answers with `PRECONDITION_FAILED` and closes the
56/// channel) cannot poison the other channels.
57///
58/// Reconnection is out of scope: when the connection is lost, consumer streams
59/// end and subsequent operations fail.
60///
61/// See [`topology`] for the queues this creates.
62#[derive(Debug)]
63pub struct RabbitMqBackend {
64 connection: Arc<Connection>,
65 publisher: Publisher,
66 options: Arc<RabbitMqOptions>,
67}
68
69impl RabbitMqBackend {
70 /// Connect to `uri` with [`RabbitMqOptions::default`].
71 ///
72 /// ```no_run
73 /// # async fn example() -> queuey_core::Result<()> {
74 /// use queuey_rabbitmq::RabbitMqBackend;
75 ///
76 /// let backend = RabbitMqBackend::connect("amqp://guest:guest@localhost:5672/%2f").await?;
77 /// # Ok(()) }
78 /// ```
79 pub async fn connect(uri: &str) -> Result<Self> {
80 Self::with_options(uri, RabbitMqOptions::default()).await
81 }
82
83 /// Connect to `uri` with explicit options.
84 pub async fn with_options(uri: &str, options: RabbitMqOptions) -> Result<Self> {
85 let connection = Arc::new(
86 Connection::connect(uri, options.connection_properties.clone())
87 .await
88 .map_err(amqp)?,
89 );
90
91 let channel = Publisher::open_confirm_channel(&connection).await?;
92 // A second, non-confirm channel used only for the on-demand hold queue
93 // declarations: a refused declaration closes the channel it ran on, and
94 // that must never be the channel every publish shares.
95 let declare_channel = connection.create_channel().await.map_err(amqp)?;
96
97 let options = Arc::new(options);
98 info!(
99 channel = channel.id(),
100 declare_channel = declare_channel.id(),
101 "rabbitmq backend connected"
102 );
103
104 Ok(Self {
105 publisher: Publisher::new(
106 Arc::clone(&connection),
107 channel,
108 declare_channel,
109 Arc::clone(&options),
110 ),
111 connection,
112 options,
113 })
114 }
115
116 /// The options this backend was built with.
117 #[must_use]
118 pub fn options(&self) -> &RabbitMqOptions {
119 &self.options
120 }
121
122 /// The name of the dead-letter queue backing `queue`.
123 #[must_use]
124 pub fn dead_queue_name(&self, queue: &str) -> String {
125 topology::dead_queue_name(queue, &self.options.dead_suffix)
126 }
127
128 /// The name of the hold queue that `ttl_ms`-long waits of `queue` happen in.
129 ///
130 /// There is one per distinct rounded delay, shared by retries and
131 /// deferrals, and it is created on demand by whatever schedules the wait
132 /// rather than by [`declare`](Backend::declare); see [`topology`].
133 #[must_use]
134 pub fn deferred_queue_name(&self, queue: &str, ttl_ms: u32) -> String {
135 topology::deferred_queue_name(queue, &self.options.deferred_suffix, ttl_ms)
136 }
137
138 /// Declaration options for a queue that is never exclusive or auto-deleted.
139 fn declare_options(durable: bool) -> QueueDeclareOptions {
140 topology::declare_options(durable)
141 }
142
143 /// Declare `q` and (optionally) `q.dead` on `channel`.
144 ///
145 /// Hold queues are *not* declared here: their names depend on the delays
146 /// jobs actually ask for, so they are created on demand by
147 /// [`publish`](Backend::publish) with a delay, [`defer`](Backend::defer)
148 /// and the delivery's `retry` / `defer`, and deleted again by the broker
149 /// once idle.
150 async fn declare_one(&self, channel: &Channel, config: &QueueConfig) -> Result<()> {
151 channel
152 .queue_declare(
153 short_string(&config.name)?,
154 Self::declare_options(config.durable),
155 topology::queue_args(config),
156 )
157 .await
158 .map_err(amqp)?;
159
160 if self.options.declare_dead_letter_queues {
161 let dead = self.dead_queue_name(&config.name);
162 channel
163 .queue_declare(
164 // Dead-lettered jobs outlive broker restarts by design:
165 // they are the record of what went wrong.
166 short_string(&dead)?,
167 Self::declare_options(true),
168 topology::dead_queue_args(config),
169 )
170 .await
171 .map_err(amqp)?;
172 }
173
174 // Only after the declarations landed: a retry or deferral onto this
175 // queue now knows whether its hold queue has to be durable, and with how
176 // many priority levels the returning job will be ordered.
177 self.publisher.remember(config);
178
179 debug!(queue = %config.name, "topology declared");
180 Ok(())
181 }
182}
183
184#[async_trait]
185impl Backend for RabbitMqBackend {
186 async fn declare(&self, queues: &[QueueConfig]) -> Result<()> {
187 if queues.is_empty() {
188 return Ok(());
189 }
190 // Up front, before a single queue exists: a name that leaves no room for
191 // its hold queues would otherwise declare fine and then fail one retry
192 // at a time, in production, on the day someone picks a long delay.
193 // Nothing is created when this fails.
194 for config in queues {
195 check_deferrable_name(&config.name, &self.options.deferred_suffix)?;
196 }
197 let channel = self.connection.create_channel().await.map_err(amqp)?;
198 let result: Result<()> = async {
199 for config in queues {
200 self.declare_one(&channel, config).await?;
201 }
202 Ok(())
203 }
204 .await;
205
206 // Best-effort cleanup: the declaration result is what matters.
207 if channel.status().connected()
208 && let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await
209 {
210 debug!(%error, "closing the declaration channel failed");
211 }
212 result
213 }
214
215 /// Publish `envelope` to its queue, or with `delay` into the hold queue
216 /// that releases it onto its queue afterwards.
217 ///
218 /// A delayed publish is held exactly like a retry: the delay is rounded up
219 /// to [`retry_granularity`](RabbitMqOptions::retry_granularity), the job
220 /// returns at the priority the envelope carries (`0` for a fresh envelope,
221 /// so it joins the back of the queue), and the same two rules as for
222 /// [`defer`](Backend::defer) apply: the queue must have been declared
223 /// through this backend, and the delay must not exceed
224 /// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS). An undelayed publish
225 /// has neither restriction.
226 async fn publish(&self, envelope: &Envelope, delay: Option<Duration>) -> Result<()> {
227 match delay {
228 None => self.publisher.publish_envelope(envelope).await,
229 Some(delay) => {
230 self.publisher
231 .publish_held(envelope, delay, Hold::Retry)
232 .await
233 }
234 }
235 }
236
237 /// Hold `envelope` for `delay`, then put it back on its own queue.
238 ///
239 /// # The queue must have been declared through this backend
240 ///
241 /// Deferring onto a queue this backend instance never
242 /// [`declare`](Backend::declare)d is [`Error::UnknownQueue`], not a
243 /// best-effort publish. A hold queue has to know its main queue's durability
244 /// and dead-letter it back by name, and neither can be guessed: a transient
245 /// hold queue in front of a durable queue loses jobs on a restart, and a TTL
246 /// expiry into a queue that does not exist is dropped by the broker in
247 /// silence. Unlike a `mandatory` publish, nothing is returned and nothing
248 /// is reported.
249 ///
250 /// `Producer::new` and `WorkerBuilder::build` declare the whole queue set,
251 /// so anything built through them can defer. `Producer::new_undeclared`
252 /// deliberately does not, so a producer built that way can enqueue but
253 /// cannot defer, or enqueue with a delay, until something in the process
254 /// declares the queue.
255 ///
256 /// # The delay has a ceiling
257 ///
258 /// Delays are rounded up to
259 /// [`deferred_granularity`](RabbitMqOptions::deferred_granularity) and
260 /// capped at [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS) (~24.8 days);
261 /// a longer one is an error rather than a shorter wait.
262 ///
263 /// [`Error::UnknownQueue`]: queuey_core::Error::UnknownQueue
264 async fn defer(&self, envelope: &Envelope, delay: Duration) -> Result<()> {
265 self.publisher
266 .publish_held(envelope, delay, Hold::Deferral)
267 .await
268 }
269
270 async fn consume(&self, queue: &QueueConfig) -> Result<DeliveryStream> {
271 let channel = self.connection.create_channel().await.map_err(amqp)?;
272 channel
273 .basic_qos(queue.prefetch, BasicQosOptions { global: false })
274 .await
275 .map_err(amqp)?;
276
277 let tag = consumer_tag(&queue.name);
278 let consumer = channel
279 .basic_consume(
280 short_string(&queue.name)?,
281 short_string(&tag)?,
282 BasicConsumeOptions {
283 no_local: false,
284 no_ack: false,
285 exclusive: false,
286 nowait: false,
287 },
288 FieldTable::default(),
289 )
290 .await
291 .map_err(amqp)?;
292
293 info!(queue = %queue.name, prefetch = queue.prefetch, %tag, "consuming");
294
295 let stream: DeliveryStream = Box::pin(delivery_stream(ConsumeState {
296 consumer,
297 // Held purely to keep the consumer's channel open for as long as
298 // the stream lives.
299 _channel: channel,
300 publisher: self.publisher.clone(),
301 queue: queue.name.clone(),
302 }));
303 Ok(stream)
304 }
305
306 async fn close(&self) -> Result<()> {
307 if let Err(error) = self.publisher.close().await {
308 debug!(%error, "closing the publishing channel failed");
309 }
310 if self.connection.status().connected() {
311 match self.connection.close(REPLY_SUCCESS, "OK".into()).await {
312 Ok(()) => {}
313 Err(error) if is_benign_close_error(&error) => {
314 debug!(%error, "connection already closing; treating close as successful");
315 }
316 Err(error) => return Err(amqp(error)),
317 }
318 }
319 info!("rabbitmq backend closed");
320 Ok(())
321 }
322}
323
324/// Refuse a queue name whose *longest* hold queue name would not fit in an AMQP
325/// short string.
326///
327/// A queue name is valid at up to 255 bytes, but a hold queue appends the suffix
328/// and up to ten digits of TTL, so a perfectly legal `q` can have illegal hold
329/// queues. Checking the worst case,
330/// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS), the longest TTL this backend
331/// will ever produce, means the answer does not depend on which delays happen
332/// to be used, so a name that passes `declare` can always be deferred on.
333fn check_deferrable_name(queue: &str, deferred_suffix: &str) -> Result<()> {
334 let hold = topology::deferred_queue_name(queue, deferred_suffix, topology::MAX_DEFERRAL_MS);
335 if hold.len() <= MAX_SHORT_STRING_LENGTH {
336 return Ok(());
337 }
338 Err(RabbitMqError::DeferredNameTooLong {
339 queue: queue.to_owned(),
340 length: hold.len(),
341 hold,
342 limit: MAX_SHORT_STRING_LENGTH,
343 }
344 .into_core())
345}
346
347/// Whether an error from `Connection::close` only says "already closing".
348///
349/// lapin flips every channel to `Closing` as soon as a connection close starts.
350/// A consumer channel dropped just before (a finished [`DeliveryStream`]) still
351/// has its own deferred `channel.close` queued; that command then fails the
352/// channel state check and lapin reports it through the connection-close
353/// promise, even though the connection does shut down normally. Nothing is
354/// lost and nothing is left open, so this is not an error worth surfacing.
355pub(crate) fn is_benign_close_error(error: &lapin::Error) -> bool {
356 use lapin::{ChannelState, ConnectionState, ErrorKind};
357 matches!(
358 error.kind(),
359 ErrorKind::InvalidChannelState(ChannelState::Closing | ChannelState::Closed, _)
360 | ErrorKind::InvalidConnectionState(ConnectionState::Closing | ConnectionState::Closed)
361 )
362}
363
364/// Everything the consumer stream needs to keep alive between polls.
365struct ConsumeState {
366 consumer: Consumer,
367 _channel: Channel,
368 publisher: Publisher,
369 queue: String,
370}
371
372/// Turn a lapin [`Consumer`] into a core [`DeliveryStream`].
373///
374/// Bodies that do not decode as an [`Envelope`] are disposed of in place (see
375/// [`discard_malformed`]) and never surface as stream items: a poison message
376/// must not stall or kill a worker.
377fn delivery_stream(
378 state: ConsumeState,
379) -> impl futures::Stream<Item = Result<Box<dyn Delivery>>> + Send {
380 futures::stream::unfold(state, |mut state| async move {
381 loop {
382 let next = state.consumer.next().await?;
383 let delivery = match next {
384 Ok(delivery) => delivery,
385 // lapin always follows an error with end-of-stream, so the
386 // consumer terminates on the next poll.
387 Err(error) => return Some((Err(amqp(error)), state)),
388 };
389
390 match Envelope::from_bytes(&delivery.data) {
391 Ok(envelope) => {
392 let boxed: Box<dyn Delivery> = Box::new(RabbitMqDelivery::new(
393 envelope,
394 delivery.acker.clone(),
395 state.publisher.clone(),
396 ));
397 return Some((Ok(boxed), state));
398 }
399 Err(error) => {
400 warn!(
401 queue = %state.queue,
402 delivery_tag = delivery.delivery_tag,
403 bytes = delivery.data.len(),
404 %error,
405 "dropping message whose body is not a valid envelope"
406 );
407 discard_malformed(&state.publisher, &state.queue, &delivery).await;
408 }
409 }
410 }
411 })
412}
413
414/// Get an undecodable message off the queue without failing the stream.
415///
416/// The message is always settled if the broker will let us settle it, in this
417/// order:
418///
419/// 1. Copy the raw bytes to `q.dead` with an `x-death-reason` header and ack the
420/// original, so the payload survives for inspection. Skipped when
421/// [`RabbitMqOptions::declare_dead_letter_queues`] is off, because this
422/// backend then does not own `q.dead` and the `mandatory` publish would only
423/// come back unroutable.
424/// 2. `basic_reject(requeue = false)`, which discards the message (or hands it
425/// to the queue's own dead-letter exchange) rather than letting it be
426/// redelivered forever. This also runs when step 1 failed *after* the publish
427/// landed but the ack did not.
428/// 3. If even the reject fails, log at `ERROR` and move on. The message then
429/// stays unacknowledged and keeps one prefetch slot until the consumer
430/// channel closes, at which point the broker requeues it. Nothing better is
431/// available: settling it needs the very channel that just refused.
432async fn discard_malformed(publisher: &Publisher, queue: &str, delivery: &LapinDelivery) {
433 if publisher.options().declare_dead_letter_queues {
434 match publisher
435 .publish_malformed(queue, &delivery.data, codec::REASON_MALFORMED)
436 .await
437 {
438 Ok(()) => match delivery.acker.ack(BasicAckOptions::default()).await {
439 Ok(true) => return,
440 Ok(false) => {
441 warn!(
442 queue,
443 delivery_tag = delivery.delivery_tag,
444 "a malformed message was already settled; nothing left to ack"
445 );
446 return;
447 }
448 Err(error) => {
449 warn!(%error, queue, delivery_tag = delivery.delivery_tag,
450 "acking a malformed message failed; falling back to reject");
451 }
452 },
453 Err(error) => {
454 warn!(%error, queue, "forwarding a malformed message to the dead-letter queue failed");
455 }
456 }
457 }
458
459 match delivery
460 .acker
461 .reject(BasicRejectOptions { requeue: false })
462 .await
463 {
464 Ok(true) => {}
465 Ok(false) => {
466 warn!(
467 queue,
468 delivery_tag = delivery.delivery_tag,
469 "a malformed message was already settled; nothing left to reject"
470 );
471 }
472 Err(error) => {
473 error!(
474 %error, queue, delivery_tag = delivery.delivery_tag,
475 "rejecting a malformed message failed; it stays unacknowledged and holds a \
476 prefetch slot until the consumer channel closes"
477 );
478 }
479 }
480}
481
482/// A consumer tag that is unique within this process and short enough for AMQP.
483///
484/// RabbitMQ only requires uniqueness per channel, and this backend opens a fresh
485/// channel per consumer, but a readable, globally distinct tag makes the
486/// management UI far easier to reason about.
487fn consumer_tag(queue: &str) -> String {
488 static NEXT: AtomicU64 = AtomicU64::new(0);
489 let sequence = NEXT.fetch_add(1, Ordering::Relaxed);
490 let nanos = SystemTime::now()
491 .duration_since(UNIX_EPOCH)
492 .map(|since| since.as_nanos())
493 .unwrap_or_default();
494 // Leave ample room for the fixed parts inside the 255-byte AMQP limit.
495 let queue = codec::truncate_at_boundary(queue, 160);
496 format!("queuey.{queue}.{nanos:x}.{sequence}")
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn closing_state_errors_are_benign_on_close() {
505 use lapin::{ChannelState, ConnectionState, ErrorKind};
506 for kind in [
507 ErrorKind::InvalidChannelState(ChannelState::Closing, "channel.close"),
508 ErrorKind::InvalidChannelState(ChannelState::Closed, "channel.close"),
509 ErrorKind::InvalidConnectionState(ConnectionState::Closing),
510 ErrorKind::InvalidConnectionState(ConnectionState::Closed),
511 ] {
512 assert!(is_benign_close_error(&lapin::Error::from(kind)));
513 }
514 }
515
516 #[test]
517 fn other_state_errors_are_not_benign_on_close() {
518 use lapin::{ChannelState, ConnectionState, ErrorKind};
519 for kind in [
520 ErrorKind::InvalidChannelState(ChannelState::Initial, "channel.close"),
521 ErrorKind::InvalidChannelState(ChannelState::Error, "channel.close"),
522 ErrorKind::InvalidConnectionState(ConnectionState::Error),
523 ErrorKind::InvalidChannel(7),
524 ] {
525 assert!(!is_benign_close_error(&lapin::Error::from(kind)));
526 }
527 }
528
529 #[test]
530 fn consumer_tags_are_unique_and_name_the_queue() {
531 let first = consumer_tag("myapp.emails");
532 let second = consumer_tag("myapp.emails");
533 assert_ne!(first, second);
534 assert!(first.starts_with("queuey.myapp.emails."));
535 assert!(second.starts_with("queuey.myapp.emails."));
536 }
537
538 #[test]
539 fn consumer_tags_fit_in_a_short_string() {
540 let tag = consumer_tag(&"q".repeat(1000));
541 assert!(
542 tag.len() <= MAX_SHORT_STRING_LENGTH,
543 "len was {}",
544 tag.len()
545 );
546 assert!(short_string(&tag).is_ok());
547 }
548
549 #[test]
550 fn a_queue_name_with_room_for_its_hold_queues_is_accepted() {
551 assert!(check_deferrable_name("myapp.emails", topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
552 // The longest name that still fits: 255 - len(".deferred.") - 10 digits.
553 let longest = "q".repeat(MAX_SHORT_STRING_LENGTH - ".deferred.".len() - 10);
554 assert_eq!(longest.len(), 235);
555 assert!(check_deferrable_name(&longest, topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
556 }
557
558 #[test]
559 fn a_queue_name_that_leaves_no_room_for_hold_queues_is_refused_at_declare() {
560 // 250 bytes is a perfectly legal queue name (`short_string` takes it),
561 // but `{q}.deferred.2147483647` is 270 bytes, so every deferral on it
562 // would fail. Better to say so once, at declare time.
563 let name = "q".repeat(250);
564 assert!(
565 short_string(&name).is_ok(),
566 "the queue name itself is legal"
567 );
568
569 let error = check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX)
570 .expect_err("a name with no room for hold queues must be refused");
571 let text = error.to_string();
572 assert!(
573 text.contains("leaves no room for its hold queues"),
574 "{text}"
575 );
576 assert!(text.contains("270 bytes"), "{text}");
577 assert!(text.contains("255-byte"), "{text}");
578 }
579
580 #[test]
581 fn the_hold_queue_name_check_uses_the_configured_suffix() {
582 let name = "q".repeat(240);
583 // 240 + 10 (".deferred.") + 10 digits = 260: refused.
584 assert!(check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX).is_err());
585 // 240 + 2 ("-h") + 1 (".") + 10 digits = 253: accepted.
586 assert!(check_deferrable_name(&name, "-h").is_ok());
587 }
588
589 #[test]
590 fn declare_options_never_auto_delete() {
591 let durable = RabbitMqBackend::declare_options(true);
592 assert!(durable.durable);
593 assert!(!durable.auto_delete);
594 assert!(!durable.exclusive);
595 assert!(!durable.passive);
596 assert!(!durable.nowait);
597
598 let transient = RabbitMqBackend::declare_options(false);
599 assert!(!transient.durable);
600 assert!(!transient.auto_delete);
601 }
602}