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::{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}