Skip to main content

queuey_rabbitmq/
delivery.rs

1//! A single message in flight from RabbitMQ.
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6use lapin::{
7    Acker,
8    options::{BasicAckOptions, BasicRejectOptions},
9};
10use queuey_core::{Delivery, Envelope, Result};
11use tracing::warn;
12
13use crate::{
14    error::{RabbitMqError, amqp},
15    publisher::{Hold, Publisher},
16};
17
18/// One RabbitMQ message, decoded into an [`Envelope`].
19///
20/// Exactly one of [`ack`](Delivery::ack), [`retry`](Delivery::retry),
21/// [`defer`](Delivery::defer) or [`dead_letter`](Delivery::dead_letter) must be
22/// called; the trait consumes the delivery so the compiler enforces "at most
23/// once", and an un-acked delivery is redelivered by the broker when the
24/// consumer channel closes.
25///
26/// [`retry`](Delivery::retry), [`defer`](Delivery::defer) and
27/// [`dead_letter`](Delivery::dead_letter) publish *before* they ack, and
28/// propagate the publish error without acking, so a failure leaves the original
29/// message unacknowledged for the broker to redeliver rather than dropping the
30/// job. Because publishes are `mandatory`, a missing `q.dead` counts as a
31/// failure. A hold queue cannot be missing: `retry` and `defer` declare it
32/// themselves, immediately before publishing, but that declaration can be
33/// *refused*, and [`retry`](Delivery::retry) says what happens then.
34///
35/// When
36/// [`declare_dead_letter_queues`](crate::RabbitMqOptions::declare_dead_letter_queues)
37/// is `false` this backend does not own `q.dead`, so
38/// [`dead_letter`](Delivery::dead_letter) rejects the message (`requeue = false`)
39/// instead of publishing to it, and logs why.
40pub struct RabbitMqDelivery {
41    envelope: Envelope,
42    acker: Acker,
43    publisher: Publisher,
44}
45
46impl std::fmt::Debug for RabbitMqDelivery {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("RabbitMqDelivery")
49            .field("job_id", &self.envelope.job_id)
50            .field("job_type", &self.envelope.job_type)
51            .field("queue", &self.envelope.queue)
52            .field("attempt", &self.envelope.attempt)
53            .finish_non_exhaustive()
54    }
55}
56
57impl RabbitMqDelivery {
58    /// Pair a decoded envelope with the acker of the message it came from.
59    pub(crate) fn new(envelope: Envelope, acker: Acker, publisher: Publisher) -> Self {
60        Self {
61            envelope,
62            acker,
63            publisher,
64        }
65    }
66
67    /// Turn lapin's "the acker was already used or is poisoned" signal into an
68    /// error: the broker never recorded the outcome, so reporting success would
69    /// tell the worker a job was settled when it will in fact be redelivered.
70    fn settled(&self, settled: bool, operation: &'static str) -> Result<()> {
71        if settled {
72            return Ok(());
73        }
74        Err(RabbitMqError::AlreadySettled {
75            operation,
76            job_id: self.envelope.job_id.to_string(),
77            queue: self.envelope.queue.clone(),
78        }
79        .into_core())
80    }
81
82    /// Ack the underlying AMQP message.
83    async fn ack_original(&self) -> Result<()> {
84        let acked = self
85            .acker
86            .ack(BasicAckOptions::default())
87            .await
88            .map_err(amqp)?;
89        self.settled(acked, "ack")
90    }
91
92    /// Reject the underlying AMQP message without requeueing it.
93    ///
94    /// The broker then applies whatever the queue's own `x-dead-letter-exchange`
95    /// policy says, and drops the message if there is none.
96    async fn reject_original(&self) -> Result<()> {
97        let rejected = self
98            .acker
99            .reject(BasicRejectOptions { requeue: false })
100            .await
101            .map_err(amqp)?;
102        self.settled(rejected, "reject")
103    }
104}
105
106#[async_trait]
107impl Delivery for RabbitMqDelivery {
108    fn envelope(&self) -> &Envelope {
109        &self.envelope
110    }
111
112    async fn ack(self: Box<Self>) -> Result<()> {
113        self.ack_original().await
114    }
115
116    async fn dead_letter(self: Box<Self>, reason: &str) -> Result<()> {
117        if !self.publisher.options().declare_dead_letter_queues {
118            // `q.dead` is not this backend's to write to, and a `mandatory`
119            // publish to a queue nobody declared is an error rather than a
120            // silent drop. Hand the message back to the broker instead: its own
121            // dead-letter policy on `q` applies if the operator configured one,
122            // and otherwise the message is discarded.
123            warn!(
124                job_id = %self.envelope.job_id,
125                queue = %self.envelope.queue,
126                reason,
127                "dead-letter queues are disabled; rejecting the delivery instead of publishing \
128                 to the dead-letter queue (the broker's own dead-letter policy applies, if any, \
129                 otherwise the job is dropped)"
130            );
131            return self.reject_original().await;
132        }
133
134        self.publisher
135            .publish_dead_letter(&self.envelope, reason)
136            .await?;
137        self.ack_original().await
138    }
139
140    /// Publish first, ack second.
141    ///
142    /// `next` is written into the hold queue for `delay` (declared on the spot,
143    /// `mandatory`, waited on for a publisher confirm) and only once the broker
144    /// has taken responsibility for it is the original acked. If the publish
145    /// fails the `?` returns before the ack, so the original stays
146    /// unacknowledged and the broker redelivers it, so the job is retried
147    /// rather than silently dropped. The reverse order would lose a job on any
148    /// broker hiccup between the two.
149    ///
150    /// The delay is rounded up to
151    /// [`RabbitMqOptions::retry_granularity`](crate::RabbitMqOptions::retry_granularity),
152    /// and `next` returns to `q` at the priority it carries, `0` after
153    /// [`Envelope::next_attempt`], so it joins the back of the queue.
154    ///
155    /// Three things count as that failure, and all three leave the original
156    /// unacked (the worker counts a settle failure and the broker redelivers the
157    /// job when the consumer channel closes):
158    ///
159    /// * the hold queue exists with different arguments, so the declaration is
160    ///   refused with `PRECONDITION_FAILED`. The declaration runs on its own
161    ///   channel, so concurrent publishes are untouched;
162    /// * `next.queue` was never declared through this backend, so the hold
163    ///   queue's durability and dead-letter target are unknown
164    ///   ([`Error::UnknownQueue`](queuey_core::Error::UnknownQueue));
165    /// * `delay` is longer than
166    ///   [`MAX_DEFERRAL_MS`](crate::topology::MAX_DEFERRAL_MS) (~24.8 days),
167    ///   which is refused rather than shortened, because a hold never releases
168    ///   a job early.
169    async fn retry(self: Box<Self>, next: Envelope, delay: Duration) -> Result<()> {
170        self.publisher
171            .publish_held(&next, delay, Hold::Retry)
172            .await?;
173        self.ack_original().await
174    }
175
176    /// Publish first, ack second, the same rule and the same failure modes as
177    /// [`retry`](Delivery::retry).
178    ///
179    /// The delay is rounded up to
180    /// [`RabbitMqOptions::deferred_granularity`](crate::RabbitMqOptions::deferred_granularity)
181    /// instead, and `next` carries its queue's top priority, so it returns
182    /// ahead of the backlog.
183    ///
184    /// A caveat on "ahead of the backlog": a consumer with prefetch `N` is
185    /// already holding up to `N` messages of that backlog, and the returning
186    /// deferral cannot overtake those. It is first among what is still on the
187    /// queue.
188    async fn defer(self: Box<Self>, next: Envelope, delay: Duration) -> Result<()> {
189        self.publisher
190            .publish_held(&next, delay, Hold::Deferral)
191            .await?;
192        self.ack_original().await
193    }
194}