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::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.retry` / `q.dead`
31/// counts as a failure. A hold queue cannot be missing: `defer` declares it
32/// itself, immediately before publishing, but that declaration can be
33/// *refused*, and [`defer`](Delivery::defer) 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 async fn retry(self: Box<Self>, next: Envelope, delay: Duration) -> Result<()> {
141 self.publisher.publish_envelope(&next, Some(delay)).await?;
142 self.ack_original().await
143 }
144
145 /// Publish first, ack second, the same rule as [`retry`](Delivery::retry).
146 ///
147 /// `next` is written into a hold queue (declared on the spot, `mandatory`,
148 /// waited on for a publisher confirm) and only once the broker has taken
149 /// responsibility for it is the original acked. If the publish fails the `?`
150 /// returns before the ack, so the original stays unacknowledged and the
151 /// broker redelivers it, so the job is retried rather than silently dropped.
152 /// The reverse order would lose a job on any broker hiccup between the two.
153 ///
154 /// Three things count as that failure, and all three leave the original
155 /// unacked (the worker counts a settle failure and the broker redelivers the
156 /// job when the consumer channel closes):
157 ///
158 /// * the hold queue exists with different arguments, so the declaration is
159 /// refused with `PRECONDITION_FAILED`. The declaration runs on its own
160 /// channel, so concurrent publishes are untouched;
161 /// * `next.queue` was never declared through this backend, so the hold
162 /// queue's durability and dead-letter target are unknown
163 /// ([`Error::UnknownQueue`](queuey_core::Error::UnknownQueue));
164 /// * `delay` is longer than
165 /// [`MAX_DEFERRAL_MS`](crate::topology::MAX_DEFERRAL_MS) (~24.8 days),
166 /// which is refused rather than shortened, because a deferral is never released
167 /// early.
168 ///
169 /// A caveat on "ahead of the backlog": a consumer with prefetch `N` is
170 /// already holding up to `N` messages of that backlog, and the returning
171 /// deferral cannot overtake those. It is first among what is still on the
172 /// queue.
173 async fn defer(self: Box<Self>, next: Envelope, delay: Duration) -> Result<()> {
174 self.publisher.publish_deferred(&next, delay).await?;
175 self.ack_original().await
176 }
177}