ruststream_lapin/message.rs
1//! The delivery type yielded by [`LapinSubscriber`](crate::LapinSubscriber).
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use bytes::Bytes;
7use lapin::message::Delivery;
8use lapin::options::{BasicAckOptions, BasicRejectOptions};
9use lapin::types::ShortString;
10use lapin::{Acker, BasicProperties};
11use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned};
12
13use crate::broker::AmqpConnection;
14use crate::convert;
15use crate::delay::DelayContext;
16#[cfg(feature = "testing")]
17use crate::in_process::{BusDelivery, Settlement};
18
19/// Header carrying a message's partition key, read by [`Partitioned`] for keyed worker lanes.
20///
21/// Set it on an outgoing message's [`HeaderMap`] to route deliveries that share a key to the same
22/// worker lane under [`workers(n, by_key)`](https://docs.rs/ruststream). It rides in the AMQP
23/// header table like any other header; nothing else in the broker interprets it.
24pub const PARTITION_KEY_HEADER: &str = "amqp-partition-key";
25
26/// The header a quorum queue stamps with how often it has returned this message to the queue.
27///
28/// Classic queues keep no such counter, and a quorum queue omits the header until the message has
29/// been returned at least once, so its absence says "no count", not "one delivery".
30pub(crate) const DELIVERY_COUNT_HEADER: &str = "x-delivery-count";
31
32/// One AMQP delivery, settled with the protocol's native acknowledgement frames.
33///
34/// Settlement mapping:
35///
36/// - [`ack`](IncomingMessage::ack) sends `basic.ack`.
37/// - [`nack(true)`](IncomingMessage::nack) sends `basic.reject` with `requeue = true`; the broker
38/// redelivers the message (typically to the same queue, `redelivered` set).
39/// - [`nack(false)`](IncomingMessage::nack) sends `basic.reject` with `requeue = false`; the
40/// broker drops the message, or dead-letters it when the queue has a dead-letter exchange.
41///
42/// Both settle with `basic.reject` rather than `basic.nack`. The two frames are the same
43/// operation on a single delivery, and they differ in one thing that matters: `RabbitMQ` 4.3
44/// counts a rejection as a delivery a message has spent and does not count a nack. A handler
45/// asking for its message back is a failed attempt, so it is counted, and a quorum queue's
46/// `x-delivery-limit` can then end a poison loop on the server.
47/// - [`nack_after(delay)`](IncomingMessage::nack_after) is native only when the subscription set
48/// [`RabbitQueue::delay`](crate::RabbitQueue::delay); otherwise the default reports the delay
49/// unsupported and the runtime uses its broker-agnostic fallback.
50///
51/// Replies received through [`LapinRequester`](crate::LapinRequester) arrive on a no-ack
52/// consumer; settling them is a no-op that always succeeds.
53#[derive(Debug)]
54pub struct LapinMessage {
55 payload: Bytes,
56 headers: HeaderMap,
57 exchange: String,
58 routing_key: String,
59 redelivered: bool,
60 delivery_tag: u64,
61 returns: Option<u64>,
62 acker: Option<Acker>,
63 /// The connection the delivery arrived on, read when it settles; `None` where the acker is.
64 connection: Option<Arc<AmqpConnection>>,
65 delay: Option<DelayContext>,
66 /// How a delivery of the in-process transport settles, in place of the acker a live one
67 /// carries. The field is there only with the `testing` feature.
68 #[cfg(feature = "testing")]
69 in_process: Option<Box<Settlement>>,
70}
71
72impl LapinMessage {
73 pub(crate) fn from_delivery(
74 delivery: Delivery,
75 connection: Option<Arc<AmqpConnection>>,
76 delay: Option<DelayContext>,
77 ) -> Self {
78 let headers = convert::headers_from_properties(&delivery.properties);
79 Self {
80 payload: Bytes::from(delivery.data),
81 headers,
82 exchange: delivery.exchange.to_string(),
83 routing_key: delivery.routing_key.to_string(),
84 redelivered: delivery.redelivered,
85 delivery_tag: delivery.delivery_tag,
86 returns: returns_of(&delivery.properties),
87 acker: Some(delivery.acker),
88 connection,
89 delay,
90 #[cfg(feature = "testing")]
91 in_process: None,
92 }
93 }
94
95 /// A delivery of the in-process transport, reporting what a live one reports.
96 #[cfg(feature = "testing")]
97 pub(crate) fn in_process(delivery: BusDelivery, delivery_tag: u64, settle: Settlement) -> Self {
98 Self {
99 payload: delivery.payload,
100 headers: delivery.headers,
101 exchange: delivery.exchange,
102 routing_key: delivery.routing_key,
103 redelivered: delivery.redelivered,
104 delivery_tag,
105 returns: delivery.returns,
106 acker: None,
107 connection: None,
108 delay: None,
109 in_process: Some(Box::new(settle)),
110 }
111 }
112
113 /// The delivery as the in-process transport routes it again, for a requeue or a redelivery.
114 #[cfg(feature = "testing")]
115 fn into_bus_delivery(self) -> BusDelivery {
116 BusDelivery {
117 payload: self.payload,
118 headers: self.headers,
119 exchange: self.exchange,
120 routing_key: self.routing_key,
121 redelivered: self.redelivered,
122 returns: self.returns,
123 }
124 }
125
126 /// Builds a settled-by-construction message for no-ack deliveries (request replies).
127 pub(crate) fn from_delivery_no_ack(delivery: Delivery) -> Self {
128 let mut msg = Self::from_delivery(delivery, None, None);
129 msg.acker = None;
130 msg
131 }
132
133 /// The exchange this message was published to (empty for the default exchange).
134 #[must_use]
135 pub fn exchange(&self) -> &str {
136 &self.exchange
137 }
138
139 /// The routing key the message was published with.
140 #[must_use]
141 pub fn routing_key(&self) -> &str {
142 &self.routing_key
143 }
144
145 /// Whether the broker marked this delivery as redelivered.
146 #[must_use]
147 pub fn redelivered(&self) -> bool {
148 self.redelivered
149 }
150
151 /// The channel-local delivery tag of this delivery.
152 #[must_use]
153 pub fn delivery_tag(&self) -> u64 {
154 self.delivery_tag
155 }
156
157 /// Refuses a settlement of a delivery held across the broker's shutdown.
158 ///
159 /// A lapin defect, worked around here: once its channel has closed, lapin drops a
160 /// settlement's command without answering it, and the acker never reports the channel
161 /// poisoned, so the frame's future never completes. The refusal comes before the frame is
162 /// handed to lapin.
163 fn refuse_after_shutdown(&self, what: &str) -> Result<(), AckError> {
164 if self
165 .connection
166 .as_ref()
167 .is_some_and(|connection| connection.is_closed())
168 {
169 return Err(AckError::Broker(
170 format!("{what} was not sent: the broker has shut down").into(),
171 ));
172 }
173 Ok(())
174 }
175
176 async fn settle<F, Fut>(mut self, op: F, what: &'static str) -> Result<(), AckError>
177 where
178 F: FnOnce(Acker) -> Fut,
179 Fut: Future<Output = lapin::Result<bool>>,
180 {
181 // No acker means a no-ack consumer delivered this message; nothing to settle.
182 let Some(acker) = self.acker.take() else {
183 return Ok(());
184 };
185 self.refuse_after_shutdown(what)?;
186 match op(acker).await {
187 Ok(true) => Ok(()),
188 // lapin reports `false` when the settle frame could not be sent because the channel
189 // already closed or errored; surface that instead of pretending the broker saw it.
190 Ok(false) => Err(AckError::Broker(
191 format!("{what} was not sent: the delivery channel is closed or errored").into(),
192 )),
193 Err(err) => Err(AckError::Broker(Box::new(err))),
194 }
195 }
196}
197
198impl IncomingMessage for LapinMessage {
199 fn payload(&self) -> &[u8] {
200 &self.payload
201 }
202
203 fn headers(&self) -> &HeaderMap {
204 &self.headers
205 }
206
207 /// Acknowledges the delivery with `basic.ack`.
208 ///
209 /// # Errors
210 ///
211 /// Returns [`AckError::Broker`] when the frame cannot be sent, for example because the
212 /// channel closed after the delivery arrived.
213 ///
214 /// # Cancel safety
215 ///
216 /// Not cancel safe: dropping the future after the frame was queued may still acknowledge the
217 /// message on the broker.
218 async fn ack(
219 // The binding is mutated only to take the in-process settlement out.
220 #[cfg_attr(not(feature = "testing"), allow(unused_mut))] mut self,
221 ) -> Result<(), AckError> {
222 #[cfg(feature = "testing")]
223 if let Some(settle) = self.in_process.take() {
224 return settle.ack();
225 }
226 self.settle(
227 |acker| async move { acker.ack(BasicAckOptions::default()).await },
228 "basic.ack",
229 )
230 .await
231 }
232
233 /// Settles negatively with `basic.reject`: back to the queue under `requeue = true`, away
234 /// from it under `requeue = false`.
235 ///
236 /// The frame is a rejection in both cases, never `basic.nack`. On one delivery the two are
237 /// the same operation - `basic.nack` only adds the `multiple` flag this crate never sets -
238 /// but a quorum queue counts a rejected delivery and does not count a nacked one, so a
239 /// handler's `retry()` spends an attempt the server can see and its `x-delivery-limit` ends
240 /// the loop.
241 ///
242 /// # Errors
243 ///
244 /// Returns [`AckError::Broker`] when the frame cannot be sent, for example because the
245 /// channel closed after the delivery arrived.
246 ///
247 /// # Cancel safety
248 ///
249 /// Not cancel safe: dropping the future after the frame was queued may still settle the
250 /// message on the broker.
251 async fn nack(
252 // The binding is mutated only to take the in-process settlement out.
253 #[cfg_attr(not(feature = "testing"), allow(unused_mut))] mut self,
254 requeue: bool,
255 ) -> Result<(), AckError> {
256 #[cfg(feature = "testing")]
257 if let Some(settle) = self.in_process.take() {
258 return settle.reject(self.into_bus_delivery(), requeue);
259 }
260 self.settle(
261 |acker| async move { acker.reject(BasicRejectOptions { requeue }).await },
262 "basic.reject",
263 )
264 .await
265 }
266
267 /// The partition key from the [`PARTITION_KEY_HEADER`], if set. Overridden so keyed worker
268 /// lanes see it without a `Partitioned` bound on every dispatch path.
269 fn partition_key(&self) -> Option<&[u8]> {
270 self.headers.get(PARTITION_KEY_HEADER)
271 }
272
273 /// How many times this message has been delivered, counting this delivery, from the one
274 /// counter the server keeps: a quorum queue's `x-delivery-count`.
275 ///
276 /// A quorum queue counts a delivery that failed - one whose consumer went away without
277 /// settling it, and one this crate rejected for a handler that asked for the message back.
278 ///
279 /// `None` where nothing counted: every delivery off a classic queue, which keeps no counter,
280 /// and the first delivery of any message. A classic queue's `redelivered` flag is not a count,
281 /// and the `x-death` table a dead-lettered message carries counts the queues it has left
282 /// rather than the deliveries it has spent, so neither stands in for the server's own count.
283 /// That is what leaves the framework's header in charge of a classic queue's cap, and it is
284 /// also what a delayed redelivery through [`RabbitQueue::delay`](crate::RabbitQueue::delay)
285 /// reports, because the copy the waiting queue releases is a new message the server counts
286 /// from zero.
287 fn redelivery_count(&self) -> Option<u64> {
288 self.returns.map(|returns| returns.saturating_add(1))
289 }
290
291 /// Whether this delivery can honor a native delayed redelivery.
292 ///
293 /// `true` only when the subscription set [`RabbitQueue::delay`](crate::RabbitQueue::delay);
294 /// otherwise the runtime uses its broker-agnostic deferred re-publish.
295 fn supports_nack_after(&self) -> bool {
296 #[cfg(feature = "testing")]
297 if let Some(settle) = &self.in_process {
298 return settle.delays();
299 }
300 self.delay.is_some()
301 }
302
303 /// Redelivers this message no sooner than `delay`, natively: re-publish it to the delay
304 /// waiting queue with a per-message TTL, then acknowledge the original. The waiting queue
305 /// dead-letters the copy back to the origin queue when the TTL fires.
306 ///
307 /// Duplicate-not-loss: the re-publish is sent on the same channel before the original is
308 /// acked, so a connection failure between them leaves the original unacked (redelivered), not
309 /// lost. The server drops a copy sent to a waiting queue that does not exist, so a
310 /// subscription whose broker does not declare topology checks for the waiting queue (or the
311 /// delayed exchange) when it opens, and refuses to open without it.
312 ///
313 /// # Errors
314 ///
315 /// Returns [`AckError::Unsupported`] when the subscription set no delay queue, and
316 /// [`AckError::Broker`] when the re-publish or the ack fails.
317 ///
318 /// # Cancel safety
319 ///
320 /// Not cancel safe: dropping the future may leave the delayed copy published, the original
321 /// acked, or both.
322 async fn nack_after(mut self, delay: Duration) -> Result<(), AckError> {
323 #[cfg(feature = "testing")]
324 if let Some(settle) = self.in_process.take() {
325 return settle.nack_after(self.into_bus_delivery(), delay);
326 }
327 let Some(context) = self.delay.take() else {
328 return Err(AckError::Unsupported);
329 };
330 // Before the copy, so a refused settlement leaves no copy behind either.
331 self.refuse_after_shutdown("basic.ack")?;
332 context
333 .republish(&self.payload, &self.headers, delay)
334 .await
335 .map_err(|err| AckError::Broker(Box::new(err)))?;
336
337 self.settle(
338 |acker| async move { acker.ack(BasicAckOptions::default()).await },
339 "basic.ack",
340 )
341 .await
342 }
343}
344
345/// The `x-delivery-count` a quorum queue stamps, or `None` where nothing counted.
346fn returns_of(properties: &BasicProperties) -> Option<u64> {
347 properties
348 .headers()
349 .as_ref()?
350 .inner()
351 .get(&ShortString::from(DELIVERY_COUNT_HEADER))
352 .and_then(convert::counter)
353}
354
355impl Partitioned for LapinMessage {
356 /// The partition key from the [`PARTITION_KEY_HEADER`], or `None` when unset.
357 ///
358 /// Deliveries that share a key are dispatched to the same worker lane under
359 /// [`workers(n, by_key)`](https://docs.rs/ruststream); AMQP itself does not interpret the
360 /// header, so the producer sets it.
361 fn partition_key(&self) -> Option<&[u8]> {
362 self.headers.get(PARTITION_KEY_HEADER)
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use lapin::BasicProperties;
369 use lapin::types::{AMQPValue, FieldArray, FieldTable, ShortString};
370
371 use super::{DELIVERY_COUNT_HEADER, returns_of};
372
373 /// A delivery carrying `fields` in its header table.
374 fn delivered(fields: &[(&str, AMQPValue)]) -> BasicProperties {
375 let mut table = FieldTable::default();
376 for (name, value) in fields {
377 table.insert(ShortString::from(*name), value.clone());
378 }
379 BasicProperties::default().with_headers(table)
380 }
381
382 // The queue's own counter is the count, whichever integer width the server wrote it in.
383 #[test]
384 fn the_queues_counter_is_the_count() {
385 let properties = delivered(&[(DELIVERY_COUNT_HEADER, AMQPValue::LongLongInt(4))]);
386
387 assert_eq!(returns_of(&properties), Some(4));
388 }
389
390 // The `x-death` table is a record of the queues a message has left, not of the deliveries it
391 // has spent, so a message the server carried away and brought back still reports the queue's
392 // count and nothing else.
393 #[test]
394 fn a_dead_lettered_delivery_reports_no_count_of_its_own() {
395 let mut entry = FieldTable::default();
396 entry.insert(
397 ShortString::from("reason"),
398 AMQPValue::LongString("rejected".into()),
399 );
400 entry.insert(ShortString::from("count"), AMQPValue::LongLongInt(2));
401 let properties = delivered(&[(
402 "x-death",
403 AMQPValue::FieldArray(FieldArray::from(vec![AMQPValue::FieldTable(entry)])),
404 )]);
405
406 assert_eq!(returns_of(&properties), None);
407 }
408
409 // Nothing counted: the first delivery of a message, and every delivery on a classic queue.
410 #[test]
411 fn a_delivery_with_no_counter_reports_none() {
412 assert_eq!(returns_of(&BasicProperties::default()), None);
413 assert_eq!(returns_of(&delivered(&[])), None);
414 }
415}