Skip to main content

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}