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}