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::{BasicAckOptions, BasicConsumeOptions, BasicQosOptions, BasicRejectOptions},
17    types::{FieldTable, MAX_SHORT_STRING_LENGTH},
18};
19use queuey_core::{Backend, Delivery, DeliveryStream, Envelope, QueueConfig, Result};
20use tracing::{debug, error, info, warn};
21
22use crate::{
23    codec,
24    connection::{ConnectionHandle, REPLY_SUCCESS, declare_topology},
25    delivery::RabbitMqDelivery,
26    error::{RabbitMqError, amqp, short_string},
27    options::RabbitMqOptions,
28    publisher::{Hold, Publisher},
29    reconnect::{Attempt, Rebuilding},
30    topology,
31};
32
33/// A [`Backend`] backed by a single RabbitMQ connection.
34///
35/// * One [`Connection`].
36/// * One publishing [`Channel`] in confirm mode, shared behind a
37///   [`tokio::sync::Mutex`]. Every publish (enqueue, retry, defer, dead-letter)
38///   is `mandatory` and waits for the broker's confirmation. The channel is
39///   reopened lazily if a channel exception closed it. Nothing is ever
40///   *declared* on it.
41/// * One long-lived [`Channel`] for the hold queue declarations every retry,
42///   delayed publish and [`defer`](Backend::defer) makes on demand. A
43///   declaration is the one thing the broker routinely refuses
44///   (`PRECONDITION_FAILED` closes the channel it ran on), so it is kept away
45///   from the publishes it would otherwise take down with it.
46/// * One fresh [`Channel`] per [`consume`](Backend::consume) call, so each
47///   consumer gets its own `basic_qos` prefetch window and a failure on one
48///   consumer cannot take down the others.
49/// * One short-lived channel per [`declare`](Backend::declare) call, so a
50///   rejected declaration (e.g. re-declaring an existing queue with different
51///   arguments, which RabbitMQ answers with `PRECONDITION_FAILED` and closes the
52///   channel) cannot poison the other channels.
53///
54/// # Reconnection
55///
56/// The connection above is not one socket but a slot. When it drops, the first
57/// operation to notice dials a replacement, the others queue behind it, every
58/// queue this backend declared is re-declared on it, and the consumer streams
59/// resubscribe and carry on yielding. Nothing above the backend sees an error:
60/// a publish issued during the outage waits, and
61/// [`Worker::run`](queuey_core::Worker::run) keeps running.
62///
63/// What does *not* survive is anything already in flight. The broker requeues
64/// every unacknowledged delivery when a connection drops, so a job whose handler
65/// was mid-run is delivered again on the new connection, and when the first run
66/// finishes and tries to settle, that settle fails (the worker counts it in
67/// [`WorkerHandle::settle_failures`](queuey_core::WorkerHandle::settle_failures)).
68/// Handlers were already required to tolerate this — the contract is
69/// at-least-once — but an outage is when it stops being theoretical.
70///
71/// See [`RabbitMqOptions::reconnect`] to bound the attempts or turn it off, and
72/// [`topology`] for the queues this creates.
73#[derive(Debug)]
74pub struct RabbitMqBackend {
75    connection: Arc<ConnectionHandle>,
76    publisher: Publisher,
77    options: Arc<RabbitMqOptions>,
78}
79
80impl RabbitMqBackend {
81    /// Connect to `uri` with [`RabbitMqOptions::default`].
82    ///
83    /// ```no_run
84    /// # async fn example() -> queuey_core::Result<()> {
85    /// use queuey_rabbitmq::RabbitMqBackend;
86    ///
87    /// let backend = RabbitMqBackend::connect("amqp://guest:guest@localhost:5672/%2f").await?;
88    /// # Ok(()) }
89    /// ```
90    pub async fn connect(uri: &str) -> Result<Self> {
91        Self::with_options(uri, RabbitMqOptions::default()).await
92    }
93
94    /// Connect to `uri` with explicit options.
95    ///
96    /// Only the *first* connection is made here, and it is not retried: a
97    /// process that cannot reach its broker at startup should fail loudly rather
98    /// than block its caller in a backoff loop. Every connection after this one
99    /// is [`RabbitMqOptions::reconnect`]'s business.
100    pub async fn with_options(uri: &str, options: RabbitMqOptions) -> Result<Self> {
101        let options = Arc::new(options);
102        let connection = ConnectionHandle::connect(uri, Arc::clone(&options)).await?;
103        let live = connection.ensure_connected().await?;
104
105        let channel = Publisher::open_confirm_channel(&live).await?;
106        // A second, non-confirm channel used only for the on-demand hold queue
107        // declarations: a refused declaration closes the channel it ran on, and
108        // that must never be the channel every publish shares.
109        let declare_channel = live.create_channel().await.map_err(amqp)?;
110
111        info!(
112            channel = channel.id(),
113            declare_channel = declare_channel.id(),
114            reconnects = connection.reconnects(),
115            "rabbitmq backend connected"
116        );
117
118        Ok(Self {
119            publisher: Publisher::new(
120                Arc::clone(&connection),
121                channel,
122                declare_channel,
123                Arc::clone(&options),
124            ),
125            connection,
126            options,
127        })
128    }
129
130    /// Whether the backend currently holds a live connection.
131    ///
132    /// A `false` does not mean the backend is broken: with reconnection on (the
133    /// default) the next operation waits for a replacement. It is here for
134    /// health endpoints and dashboards that want to report the gap rather than
135    /// cause one, and it never itself triggers a reconnect.
136    #[must_use]
137    pub fn is_connected(&self) -> bool {
138        self.connection.is_connected()
139    }
140
141    /// The options this backend was built with.
142    #[must_use]
143    pub fn options(&self) -> &RabbitMqOptions {
144        &self.options
145    }
146
147    /// The name of the dead-letter queue backing `queue`.
148    #[must_use]
149    pub fn dead_queue_name(&self, queue: &str) -> String {
150        topology::dead_queue_name(queue, &self.options.dead_suffix)
151    }
152
153    /// The name of the hold queue that `ttl_ms`-long waits of `queue` happen in.
154    ///
155    /// There is one per distinct rounded delay, shared by retries and
156    /// deferrals, and it is created on demand by whatever schedules the wait
157    /// rather than by [`declare`](Backend::declare); see [`topology`].
158    #[must_use]
159    pub fn deferred_queue_name(&self, queue: &str, ttl_ms: u32) -> String {
160        topology::deferred_queue_name(queue, &self.options.deferred_suffix, ttl_ms)
161    }
162
163    /// Declare `q` and (optionally) `q.dead` on `channel`, and remember the
164    /// config.
165    ///
166    /// Hold queues are *not* declared here: their names depend on the delays
167    /// jobs actually ask for, so they are created on demand by
168    /// [`publish`](Backend::publish) with a delay, [`defer`](Backend::defer)
169    /// and the delivery's `retry` / `defer`, and deleted again by the broker
170    /// once idle.
171    ///
172    /// The declaration itself is [`declare_topology`], shared with the replay a
173    /// reconnect performs, so what comes back after an outage is exactly what
174    /// was created before it.
175    async fn declare_one(&self, channel: &Channel, config: &QueueConfig) -> Result<()> {
176        declare_topology(channel, config, &self.options).await?;
177
178        // Only after the declarations landed: a retry or deferral onto this
179        // queue now knows whether its hold queue has to be durable, and with how
180        // many priority levels the returning job will be ordered. This is also
181        // the record a reconnect replays.
182        self.connection.remember(config);
183
184        debug!(queue = %config.name, "topology declared");
185        Ok(())
186    }
187}
188
189#[async_trait]
190impl Backend for RabbitMqBackend {
191    async fn declare(&self, queues: &[QueueConfig]) -> Result<()> {
192        if queues.is_empty() {
193            return Ok(());
194        }
195        // Up front, before a single queue exists: a name that leaves no room for
196        // its hold queues would otherwise declare fine and then fail one retry
197        // at a time, in production, on the day someone picks a long delay.
198        // Nothing is created when this fails.
199        for config in queues {
200            check_deferrable_name(&config.name, &self.options.deferred_suffix)?;
201        }
202        let channel = self
203            .connection
204            .ensure_connected()
205            .await?
206            .create_channel()
207            .await
208            .map_err(amqp)?;
209        let result: Result<()> = async {
210            for config in queues {
211                self.declare_one(&channel, config).await?;
212            }
213            Ok(())
214        }
215        .await;
216
217        // Best-effort cleanup: the declaration result is what matters.
218        if channel.status().connected()
219            && let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await
220        {
221            debug!(%error, "closing the declaration channel failed");
222        }
223        result
224    }
225
226    /// Publish `envelope` to its queue, or with `delay` into the hold queue
227    /// that releases it onto its queue afterwards.
228    ///
229    /// A delayed publish is held exactly like a retry: the delay is rounded up
230    /// to [`retry_granularity`](RabbitMqOptions::retry_granularity), the job
231    /// returns at the priority the envelope carries (`0` for a fresh envelope,
232    /// so it joins the back of the queue), and the same two rules as for
233    /// [`defer`](Backend::defer) apply: the queue must have been declared
234    /// through this backend, and the delay must not exceed
235    /// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS). An undelayed publish
236    /// has neither restriction.
237    async fn publish(&self, envelope: &Envelope, delay: Option<Duration>) -> Result<()> {
238        match delay {
239            None => self.publisher.publish_envelope(envelope).await,
240            Some(delay) => {
241                self.publisher
242                    .publish_held(envelope, delay, Hold::Retry)
243                    .await
244            }
245        }
246    }
247
248    /// Hold `envelope` for `delay`, then put it back on its own queue.
249    ///
250    /// # The queue must have been declared through this backend
251    ///
252    /// Deferring onto a queue this backend instance never
253    /// [`declare`](Backend::declare)d is [`Error::UnknownQueue`], not a
254    /// best-effort publish. A hold queue has to know its main queue's durability
255    /// and dead-letter it back by name, and neither can be guessed: a transient
256    /// hold queue in front of a durable queue loses jobs on a restart, and a TTL
257    /// expiry into a queue that does not exist is dropped by the broker in
258    /// silence. Unlike a `mandatory` publish, nothing is returned and nothing
259    /// is reported.
260    ///
261    /// `Producer::new` and `WorkerBuilder::build` declare the whole queue set,
262    /// so anything built through them can defer. `Producer::new_undeclared`
263    /// deliberately does not, so a producer built that way can enqueue but
264    /// cannot defer, or enqueue with a delay, until something in the process
265    /// declares the queue.
266    ///
267    /// # The delay has a ceiling
268    ///
269    /// Delays are rounded up to
270    /// [`deferred_granularity`](RabbitMqOptions::deferred_granularity) and
271    /// capped at [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS) (~24.8 days);
272    /// a longer one is an error rather than a shorter wait.
273    ///
274    /// [`Error::UnknownQueue`]: queuey_core::Error::UnknownQueue
275    async fn defer(&self, envelope: &Envelope, delay: Duration) -> Result<()> {
276        self.publisher
277            .publish_held(envelope, delay, Hold::Deferral)
278            .await
279    }
280
281    /// Subscribe to `queue` and stream its deliveries.
282    ///
283    /// The stream outlives the connection it started on: if the connection
284    /// drops, it waits for a reconnect and resubscribes rather than ending, so
285    /// [`Worker::run`](queuey_core::Worker::run) keeps going across a broker
286    /// restart. It ends only when the backend is [`close`](Backend::close)d, or
287    /// when reconnection is disabled or exhausted, which is what
288    /// [`Error::ConsumerStopped`](queuey_core::Error::ConsumerStopped) is for.
289    ///
290    /// Deliveries the worker was still holding when the connection dropped are
291    /// requeued by the broker and delivered again here; settling them on the old
292    /// connection fails. See the type-level docs.
293    async fn consume(&self, queue: &QueueConfig) -> Result<DeliveryStream> {
294        let connection = self.connection.ensure_connected().await?;
295        let generation = self.connection.generation();
296        let (channel, consumer) = subscribe(&connection, queue).await?;
297
298        let stream: DeliveryStream = Box::pin(delivery_stream(ConsumeState {
299            consumer,
300            // Held purely to keep the consumer's channel open for as long as
301            // the stream lives, and replaced wholesale on a resubscribe.
302            channel,
303            connection: Arc::clone(&self.connection),
304            publisher: self.publisher.clone(),
305            config: queue.clone(),
306            generation,
307        }));
308        Ok(stream)
309    }
310
311    /// Close the connection and every channel on it, for good.
312    ///
313    /// The handle is marked closing *first*, before a single channel goes down,
314    /// so that the consumers watching for a dropped connection see a deliberate
315    /// shutdown instead of an outage and end their streams rather than racing to
316    /// reconnect. Nothing reopens the connection afterwards.
317    async fn close(&self) -> Result<()> {
318        self.connection.mark_closing();
319        if let Err(error) = self.publisher.close().await {
320            debug!(%error, "closing the publishing channel failed");
321        }
322        self.connection.close().await?;
323        info!("rabbitmq backend closed");
324        Ok(())
325    }
326}
327
328/// Refuse a queue name whose *longest* hold queue name would not fit in an AMQP
329/// short string.
330///
331/// A queue name is valid at up to 255 bytes, but a hold queue appends the suffix
332/// and up to ten digits of TTL, so a perfectly legal `q` can have illegal hold
333/// queues. Checking the worst case,
334/// [`MAX_DEFERRAL_MS`](topology::MAX_DEFERRAL_MS), the longest TTL this backend
335/// will ever produce, means the answer does not depend on which delays happen
336/// to be used, so a name that passes `declare` can always be deferred on.
337fn check_deferrable_name(queue: &str, deferred_suffix: &str) -> Result<()> {
338    let hold = topology::deferred_queue_name(queue, deferred_suffix, topology::MAX_DEFERRAL_MS);
339    if hold.len() <= MAX_SHORT_STRING_LENGTH {
340        return Ok(());
341    }
342    Err(RabbitMqError::DeferredNameTooLong {
343        queue: queue.to_owned(),
344        length: hold.len(),
345        hold,
346        limit: MAX_SHORT_STRING_LENGTH,
347    }
348    .into_core())
349}
350
351/// Whether an error from `Connection::close` only says "already closing".
352///
353/// lapin flips every channel to `Closing` as soon as a connection close starts.
354/// A consumer channel dropped just before (a finished [`DeliveryStream`]) still
355/// has its own deferred `channel.close` queued; that command then fails the
356/// channel state check and lapin reports it through the connection-close
357/// promise, even though the connection does shut down normally. Nothing is
358/// lost and nothing is left open, so this is not an error worth surfacing.
359pub(crate) fn is_benign_close_error(error: &lapin::Error) -> bool {
360    use lapin::{ChannelState, ConnectionState, ErrorKind};
361    matches!(
362        error.kind(),
363        ErrorKind::InvalidChannelState(ChannelState::Closing | ChannelState::Closed, _)
364            | ErrorKind::InvalidConnectionState(ConnectionState::Closing | ConnectionState::Closed)
365    )
366}
367
368/// Open a channel on `connection` and start consuming `config`'s queue on it.
369///
370/// One channel per consumer: the `basic_qos` prefetch window is per channel, and
371/// a failure on one consumer must not take down the others. Shared by
372/// [`consume`](Backend::consume) and by the resubscribe a dropped connection
373/// triggers, so a recovered consumer is configured exactly like a fresh one.
374async fn subscribe(connection: &Connection, config: &QueueConfig) -> Result<(Channel, Consumer)> {
375    let channel = connection.create_channel().await.map_err(amqp)?;
376    channel
377        .basic_qos(config.prefetch, BasicQosOptions { global: false })
378        .await
379        .map_err(amqp)?;
380
381    let tag = consumer_tag(&config.name);
382    let consumer = channel
383        .basic_consume(
384            short_string(&config.name)?,
385            short_string(&tag)?,
386            BasicConsumeOptions {
387                no_local: false,
388                no_ack: false,
389                exclusive: false,
390                nowait: false,
391            },
392            FieldTable::default(),
393        )
394        .await
395        .map_err(amqp)?;
396
397    info!(queue = %config.name, prefetch = config.prefetch, %tag, "consuming");
398    Ok((channel, consumer))
399}
400
401/// Everything the consumer stream needs to keep alive between polls.
402struct ConsumeState {
403    consumer: Consumer,
404    /// Kept alive so the consumer's channel outlives the call that opened it,
405    /// and replaced on every resubscribe.
406    channel: Channel,
407    connection: Arc<ConnectionHandle>,
408    publisher: Publisher,
409    /// The full config, not just the name: a resubscribe needs the prefetch too.
410    config: QueueConfig,
411    /// Connection generation this subscription was made on, so a consumer can
412    /// tell "my connection died" from "somebody already replaced it".
413    generation: u64,
414}
415
416/// What a consumer should do after its subscription stopped producing.
417enum Resubscribed {
418    /// Back on a live connection; keep polling.
419    Yes,
420    /// The stream is over on purpose: the backend is closing, or reconnection is
421    /// switched off.
422    Stop,
423}
424
425/// Turn a lapin [`Consumer`] into a core [`DeliveryStream`].
426///
427/// Bodies that do not decode as an [`Envelope`] are disposed of in place (see
428/// [`discard_malformed`]) and never surface as stream items: a poison message
429/// must not stall or kill a worker.
430///
431/// A subscription that stops producing, whether it errors or simply ends, is
432/// rebuilt rather than propagated (see [`recover`]), so a dropped connection is
433/// a pause in this stream instead of the end of it. The stream still terminates
434/// on a deliberate close, and still yields an error when reconnection is off or
435/// has given up, which is what makes
436/// [`Error::ConsumerStopped`](queuey_core::Error::ConsumerStopped) reachable.
437fn delivery_stream(
438    state: ConsumeState,
439) -> impl futures::Stream<Item = Result<Box<dyn Delivery>>> + Send {
440    futures::stream::unfold(state, |mut state| async move {
441        loop {
442            let next = match state.consumer.next().await {
443                Some(Ok(delivery)) => Some(delivery),
444                // lapin always follows an error with end-of-stream, so the
445                // consumer is finished either way; both cases are the same
446                // question, which is whether this consumer can be rebuilt.
447                Some(Err(error)) => {
448                    warn!(
449                        queue = %state.config.name,
450                        %error,
451                        "consumer failed"
452                    );
453                    None
454                }
455                None => None,
456            };
457
458            let Some(delivery) = next else {
459                match recover(&mut state).await {
460                    Ok(Resubscribed::Yes) => continue,
461                    Ok(Resubscribed::Stop) => return None,
462                    Err(error) => return Some((Err(error), state)),
463                }
464            };
465
466            match Envelope::from_bytes(&delivery.data) {
467                Ok(envelope) => {
468                    let boxed: Box<dyn Delivery> = Box::new(RabbitMqDelivery::new(
469                        envelope,
470                        delivery.acker.clone(),
471                        state.publisher.clone(),
472                    ));
473                    return Some((Ok(boxed), state));
474                }
475                Err(error) => {
476                    warn!(
477                        queue = %state.config.name,
478                        delivery_tag = delivery.delivery_tag,
479                        bytes = delivery.data.len(),
480                        %error,
481                        "dropping message whose body is not a valid envelope"
482                    );
483                    discard_malformed(&state.publisher, &state.config.name, &delivery).await;
484                }
485            }
486        }
487    })
488}
489
490/// Put a stopped consumer back on a live connection.
491///
492/// Returns [`Resubscribed::Stop`] when the stream is meant to end (the backend
493/// is closing, or reconnection is disabled), and an error when reconnection was
494/// tried and gave up. Both end the stream; the worker distinguishes them,
495/// because only one of the two is a failure.
496///
497/// The retry loop here is not the same one as
498/// [`ConnectionHandle::ensure_connected`]'s, and it is needed as well as that
499/// one: the connection can be perfectly healthy while `basic_consume` still
500/// fails, most obviously when the queue itself is gone (deleted by an operator,
501/// or lost to a broker restart and not restored because the redeclare after the
502/// reconnect was refused). Without a delay of its own that case would spin as
503/// fast as the broker can say no, so failures here are paced by the same
504/// [`ReconnectPolicy`](crate::ReconnectPolicy) and counted against the same
505/// attempt limit.
506async fn recover(state: &mut ConsumeState) -> Result<Resubscribed> {
507    if state.connection.is_closing() {
508        debug!(queue = %state.config.name, "consumer stopped; the backend is closing");
509        return Ok(Resubscribed::Stop);
510    }
511    let Some(policy) = state.connection.policy() else {
512        info!(
513            queue = %state.config.name,
514            "consumer stopped and reconnection is disabled; ending the stream"
515        );
516        return Ok(Resubscribed::Stop);
517    };
518
519    // The old channel is usually dead already, but a broker-side `basic.cancel`
520    // (a deleted queue) leaves it open with no consumer on it.
521    close_consumer_channel(&state.channel).await;
522
523    let mut failures: u32 = 0;
524    loop {
525        // `ensure_connected` enforces the same limit on the connection itself
526        // and blocks here until the broker is back.
527        let connection = state.connection.ensure_connected().await?;
528        let generation = state.connection.generation();
529
530        match subscribe(&connection, &state.config).await {
531            Ok((channel, consumer)) => {
532                info!(
533                    queue = %state.config.name,
534                    generation,
535                    previous_generation = state.generation,
536                    attempts = failures + 1,
537                    "consumer resubscribed"
538                );
539                state.channel = channel;
540                state.consumer = consumer;
541                state.generation = generation;
542                return Ok(Resubscribed::Yes);
543            }
544            Err(error) => {
545                failures += 1;
546                let Some(delay) =
547                    policy.next_delay(Attempt::after(Rebuilding::Consumer, failures, &error))
548                else {
549                    error!(
550                        queue = %state.config.name,
551                        %error,
552                        attempts = failures,
553                        "giving up on resubscribing the consumer; the policy declined another \
554                         attempt"
555                    );
556                    return Err(error);
557                };
558                warn!(
559                    queue = %state.config.name,
560                    %error,
561                    failures,
562                    ?delay,
563                    "resubscribing the consumer failed; will try again"
564                );
565                state.connection.sleep_unless_closing(delay).await?;
566            }
567        }
568    }
569}
570
571/// Close a consumer's channel before it is replaced, best-effort.
572///
573/// Only reachable for a channel the broker left open (a `basic.cancel` after its
574/// queue was deleted); after a connection drop the channel is already gone and
575/// this is a no-op. Leaking it would hold a channel per outage on a
576/// long-running worker.
577async fn close_consumer_channel(channel: &Channel) {
578    if !channel.status().connected() {
579        return;
580    }
581    if let Err(error) = channel.close(REPLY_SUCCESS, "OK".into()).await {
582        debug!(%error, channel = channel.id(), "closing a replaced consumer channel failed");
583    }
584}
585
586/// Get an undecodable message off the queue without failing the stream.
587///
588/// The message is always settled if the broker will let us settle it, in this
589/// order:
590///
591/// 1. Copy the raw bytes to `q.dead` with an `x-death-reason` header and ack the
592///    original, so the payload survives for inspection. Skipped when
593///    [`RabbitMqOptions::declare_dead_letter_queues`] is off, because this
594///    backend then does not own `q.dead` and the `mandatory` publish would only
595///    come back unroutable.
596/// 2. `basic_reject(requeue = false)`, which discards the message (or hands it
597///    to the queue's own dead-letter exchange) rather than letting it be
598///    redelivered forever. This also runs when step 1 failed *after* the publish
599///    landed but the ack did not.
600/// 3. If even the reject fails, log at `ERROR` and move on. The message then
601///    stays unacknowledged and keeps one prefetch slot until the consumer
602///    channel closes, at which point the broker requeues it. Nothing better is
603///    available: settling it needs the very channel that just refused.
604async fn discard_malformed(publisher: &Publisher, queue: &str, delivery: &LapinDelivery) {
605    if publisher.options().declare_dead_letter_queues {
606        match publisher
607            .publish_malformed(queue, &delivery.data, codec::REASON_MALFORMED)
608            .await
609        {
610            Ok(()) => match delivery.acker.ack(BasicAckOptions::default()).await {
611                Ok(true) => return,
612                Ok(false) => {
613                    warn!(
614                        queue,
615                        delivery_tag = delivery.delivery_tag,
616                        "a malformed message was already settled; nothing left to ack"
617                    );
618                    return;
619                }
620                Err(error) => {
621                    warn!(%error, queue, delivery_tag = delivery.delivery_tag,
622                        "acking a malformed message failed; falling back to reject");
623                }
624            },
625            Err(error) => {
626                warn!(%error, queue, "forwarding a malformed message to the dead-letter queue failed");
627            }
628        }
629    }
630
631    match delivery
632        .acker
633        .reject(BasicRejectOptions { requeue: false })
634        .await
635    {
636        Ok(true) => {}
637        Ok(false) => {
638            warn!(
639                queue,
640                delivery_tag = delivery.delivery_tag,
641                "a malformed message was already settled; nothing left to reject"
642            );
643        }
644        Err(error) => {
645            error!(
646                %error, queue, delivery_tag = delivery.delivery_tag,
647                "rejecting a malformed message failed; it stays unacknowledged and holds a \
648                 prefetch slot until the consumer channel closes"
649            );
650        }
651    }
652}
653
654/// A consumer tag that is unique within this process and short enough for AMQP.
655///
656/// RabbitMQ only requires uniqueness per channel, and this backend opens a fresh
657/// channel per consumer, but a readable, globally distinct tag makes the
658/// management UI far easier to reason about.
659fn consumer_tag(queue: &str) -> String {
660    static NEXT: AtomicU64 = AtomicU64::new(0);
661    let sequence = NEXT.fetch_add(1, Ordering::Relaxed);
662    let nanos = SystemTime::now()
663        .duration_since(UNIX_EPOCH)
664        .map(|since| since.as_nanos())
665        .unwrap_or_default();
666    // Leave ample room for the fixed parts inside the 255-byte AMQP limit.
667    let queue = codec::truncate_at_boundary(queue, 160);
668    format!("queuey.{queue}.{nanos:x}.{sequence}")
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    #[test]
676    fn closing_state_errors_are_benign_on_close() {
677        use lapin::{ChannelState, ConnectionState, ErrorKind};
678        for kind in [
679            ErrorKind::InvalidChannelState(ChannelState::Closing, "channel.close"),
680            ErrorKind::InvalidChannelState(ChannelState::Closed, "channel.close"),
681            ErrorKind::InvalidConnectionState(ConnectionState::Closing),
682            ErrorKind::InvalidConnectionState(ConnectionState::Closed),
683        ] {
684            assert!(is_benign_close_error(&lapin::Error::from(kind)));
685        }
686    }
687
688    #[test]
689    fn other_state_errors_are_not_benign_on_close() {
690        use lapin::{ChannelState, ConnectionState, ErrorKind};
691        for kind in [
692            ErrorKind::InvalidChannelState(ChannelState::Initial, "channel.close"),
693            ErrorKind::InvalidChannelState(ChannelState::Error, "channel.close"),
694            ErrorKind::InvalidConnectionState(ConnectionState::Error),
695            ErrorKind::InvalidChannel(7),
696        ] {
697            assert!(!is_benign_close_error(&lapin::Error::from(kind)));
698        }
699    }
700
701    #[test]
702    fn consumer_tags_are_unique_and_name_the_queue() {
703        let first = consumer_tag("myapp.emails");
704        let second = consumer_tag("myapp.emails");
705        assert_ne!(first, second);
706        assert!(first.starts_with("queuey.myapp.emails."));
707        assert!(second.starts_with("queuey.myapp.emails."));
708    }
709
710    #[test]
711    fn consumer_tags_fit_in_a_short_string() {
712        let tag = consumer_tag(&"q".repeat(1000));
713        assert!(
714            tag.len() <= MAX_SHORT_STRING_LENGTH,
715            "len was {}",
716            tag.len()
717        );
718        assert!(short_string(&tag).is_ok());
719    }
720
721    #[test]
722    fn a_queue_name_with_room_for_its_hold_queues_is_accepted() {
723        assert!(check_deferrable_name("myapp.emails", topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
724        // The longest name that still fits: 255 - len(".deferred.") - 10 digits.
725        let longest = "q".repeat(MAX_SHORT_STRING_LENGTH - ".deferred.".len() - 10);
726        assert_eq!(longest.len(), 235);
727        assert!(check_deferrable_name(&longest, topology::DEFAULT_DEFERRED_SUFFIX).is_ok());
728    }
729
730    #[test]
731    fn a_queue_name_that_leaves_no_room_for_hold_queues_is_refused_at_declare() {
732        // 250 bytes is a perfectly legal queue name (`short_string` takes it),
733        // but `{q}.deferred.2147483647` is 270 bytes, so every deferral on it
734        // would fail. Better to say so once, at declare time.
735        let name = "q".repeat(250);
736        assert!(
737            short_string(&name).is_ok(),
738            "the queue name itself is legal"
739        );
740
741        let error = check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX)
742            .expect_err("a name with no room for hold queues must be refused");
743        let text = error.to_string();
744        assert!(
745            text.contains("leaves no room for its hold queues"),
746            "{text}"
747        );
748        assert!(text.contains("270 bytes"), "{text}");
749        assert!(text.contains("255-byte"), "{text}");
750    }
751
752    #[test]
753    fn the_hold_queue_name_check_uses_the_configured_suffix() {
754        let name = "q".repeat(240);
755        // 240 + 10 (".deferred.") + 10 digits = 260: refused.
756        assert!(check_deferrable_name(&name, topology::DEFAULT_DEFERRED_SUFFIX).is_err());
757        // 240 + 2 ("-h") + 1 (".") + 10 digits = 253: accepted.
758        assert!(check_deferrable_name(&name, "-h").is_ok());
759    }
760
761    #[test]
762    fn declare_options_never_auto_delete() {
763        let durable = topology::declare_options(true);
764        assert!(durable.durable);
765        assert!(!durable.auto_delete);
766        assert!(!durable.exclusive);
767        assert!(!durable.passive);
768        assert!(!durable.nowait);
769
770        let transient = topology::declare_options(false);
771        assert!(!transient.durable);
772        assert!(!transient.auto_delete);
773    }
774}