Skip to main content

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