ruststream_lapin/message.rs
1//! The delivery type yielded by [`LapinSubscriber`](crate::LapinSubscriber).
2
3use std::time::Duration;
4
5use bytes::Bytes;
6use lapin::Acker;
7use lapin::message::Delivery;
8use lapin::options::{BasicAckOptions, BasicNackOptions, BasicRejectOptions};
9use ruststream::{AckError, Headers, IncomingMessage, Partitioned};
10
11use crate::convert;
12use crate::delay::DelayContext;
13
14/// Header carrying a message's partition key, read by [`Partitioned`] for keyed worker lanes.
15///
16/// Set it on an outgoing message's [`Headers`] to route deliveries that share a key to the same
17/// worker lane under [`workers(n, by_key)`](https://docs.rs/ruststream). It rides in the AMQP
18/// header table like any other header; nothing else in the broker interprets it.
19pub const PARTITION_KEY_HEADER: &str = "amqp-partition-key";
20
21/// One AMQP delivery, settled with the protocol's native acknowledgement frames.
22///
23/// Settlement mapping:
24///
25/// - [`ack`](IncomingMessage::ack) sends `basic.ack`.
26/// - [`nack(true)`](IncomingMessage::nack) sends `basic.nack` with `requeue = true`; the broker
27/// redelivers the message (typically to the same queue, `redelivered` set).
28/// - [`nack(false)`](IncomingMessage::nack) sends `basic.reject` with `requeue = false`; the
29/// broker drops the message, or dead-letters it when the queue has a dead-letter exchange.
30/// - [`nack_after(delay)`](IncomingMessage::nack_after) is native only when the subscription set
31/// [`RabbitQueue::delay`](crate::RabbitQueue::delay); otherwise the default reports the delay
32/// unsupported and the runtime uses its broker-agnostic fallback.
33///
34/// Replies received through [`LapinRequester`](crate::LapinRequester) arrive on a no-ack
35/// consumer; settling them is a no-op that always succeeds.
36#[derive(Debug)]
37pub struct LapinMessage {
38 payload: Bytes,
39 headers: Headers,
40 exchange: String,
41 routing_key: String,
42 redelivered: bool,
43 delivery_tag: u64,
44 acker: Option<Acker>,
45 delay: Option<DelayContext>,
46}
47
48impl LapinMessage {
49 pub(crate) fn from_delivery(delivery: Delivery, delay: Option<DelayContext>) -> Self {
50 let headers = convert::headers_from_properties(&delivery.properties);
51 Self {
52 payload: Bytes::from(delivery.data),
53 headers,
54 exchange: delivery.exchange.to_string(),
55 routing_key: delivery.routing_key.to_string(),
56 redelivered: delivery.redelivered,
57 delivery_tag: delivery.delivery_tag,
58 acker: Some(delivery.acker),
59 delay,
60 }
61 }
62
63 /// Builds a settled-by-construction message for no-ack deliveries (request replies).
64 pub(crate) fn from_delivery_no_ack(delivery: Delivery) -> Self {
65 let mut msg = Self::from_delivery(delivery, None);
66 msg.acker = None;
67 msg
68 }
69
70 /// The exchange this message was published to (empty for the default exchange).
71 #[must_use]
72 pub fn exchange(&self) -> &str {
73 &self.exchange
74 }
75
76 /// The routing key the message was published with.
77 #[must_use]
78 pub fn routing_key(&self) -> &str {
79 &self.routing_key
80 }
81
82 /// Whether the broker marked this delivery as redelivered.
83 #[must_use]
84 pub fn redelivered(&self) -> bool {
85 self.redelivered
86 }
87
88 /// The channel-local delivery tag of this delivery.
89 #[must_use]
90 pub fn delivery_tag(&self) -> u64 {
91 self.delivery_tag
92 }
93
94 async fn settle<F, Fut>(mut self, op: F, what: &'static str) -> Result<(), AckError>
95 where
96 F: FnOnce(Acker) -> Fut,
97 Fut: Future<Output = lapin::Result<bool>>,
98 {
99 // No acker means a no-ack consumer delivered this message; nothing to settle.
100 let Some(acker) = self.acker.take() else {
101 return Ok(());
102 };
103 match op(acker).await {
104 Ok(true) => Ok(()),
105 // lapin reports `false` when the settle frame could not be sent because the channel
106 // already closed or errored; surface that instead of pretending the broker saw it.
107 Ok(false) => Err(AckError::Broker(
108 format!("{what} was not sent: the delivery channel is closed or errored").into(),
109 )),
110 Err(err) => Err(AckError::Broker(Box::new(err))),
111 }
112 }
113}
114
115impl IncomingMessage for LapinMessage {
116 fn payload(&self) -> &[u8] {
117 &self.payload
118 }
119
120 fn headers(&self) -> &Headers {
121 &self.headers
122 }
123
124 /// Acknowledges the delivery with `basic.ack`.
125 ///
126 /// # Errors
127 ///
128 /// Returns [`AckError::Broker`] when the frame cannot be sent, for example because the
129 /// channel closed after the delivery arrived.
130 ///
131 /// # Cancel safety
132 ///
133 /// Not cancel safe: dropping the future after the frame was queued may still acknowledge the
134 /// message on the broker.
135 async fn ack(self) -> Result<(), AckError> {
136 self.settle(
137 |acker| async move { acker.ack(BasicAckOptions::default()).await },
138 "basic.ack",
139 )
140 .await
141 }
142
143 /// Settles negatively: `basic.nack(requeue = true)` or `basic.reject(requeue = false)`.
144 ///
145 /// # Errors
146 ///
147 /// Returns [`AckError::Broker`] when the frame cannot be sent, for example because the
148 /// channel closed after the delivery arrived.
149 ///
150 /// # Cancel safety
151 ///
152 /// Not cancel safe: dropping the future after the frame was queued may still settle the
153 /// message on the broker.
154 async fn nack(self, requeue: bool) -> Result<(), AckError> {
155 if requeue {
156 self.settle(
157 |acker| async move {
158 acker
159 .nack(BasicNackOptions {
160 multiple: false,
161 requeue: true,
162 })
163 .await
164 },
165 "basic.nack",
166 )
167 .await
168 } else {
169 self.settle(
170 |acker| async move { acker.reject(BasicRejectOptions { requeue: false }).await },
171 "basic.reject",
172 )
173 .await
174 }
175 }
176
177 /// The partition key from the [`PARTITION_KEY_HEADER`], if set. Overridden so keyed worker
178 /// lanes see it without a `Partitioned` bound on every dispatch path.
179 fn partition_key(&self) -> Option<&[u8]> {
180 self.headers.get(PARTITION_KEY_HEADER)
181 }
182
183 /// Whether this delivery can honor a native delayed redelivery.
184 ///
185 /// `true` only when the subscription set [`RabbitQueue::delay`](crate::RabbitQueue::delay);
186 /// otherwise the runtime uses its broker-agnostic deferred re-publish.
187 fn supports_nack_after(&self) -> bool {
188 self.delay.is_some()
189 }
190
191 /// Redelivers this message no sooner than `delay`, natively: re-publish it to the delay
192 /// waiting queue with a per-message TTL, then acknowledge the original. The waiting queue
193 /// dead-letters the copy back to the origin queue when the TTL fires.
194 ///
195 /// Duplicate-not-loss: the re-publish is sent on the same channel before the original is
196 /// acked, so a connection failure between them leaves the original unacked (redelivered), not
197 /// lost. The one loss window is a missing waiting queue - an unroutable publish to the default
198 /// exchange is silently dropped - which is why the waiting queue is the user's declared
199 /// infrastructure.
200 ///
201 /// # Errors
202 ///
203 /// Returns [`AckError::Unsupported`] when the subscription set no delay queue, and
204 /// [`AckError::Broker`] when the re-publish or the ack fails.
205 ///
206 /// # Cancel safety
207 ///
208 /// Not cancel safe: dropping the future may leave the delayed copy published, the original
209 /// acked, or both.
210 async fn nack_after(mut self, delay: Duration) -> Result<(), AckError> {
211 let Some(context) = self.delay.take() else {
212 return Err(AckError::Unsupported);
213 };
214 context
215 .republish(&self.payload, &self.headers, delay)
216 .await
217 .map_err(|err| AckError::Broker(Box::new(err)))?;
218
219 self.settle(
220 |acker| async move { acker.ack(BasicAckOptions::default()).await },
221 "basic.ack",
222 )
223 .await
224 }
225}
226
227impl Partitioned for LapinMessage {
228 /// The partition key from the [`PARTITION_KEY_HEADER`], or `None` when unset.
229 ///
230 /// Deliveries that share a key are dispatched to the same worker lane under
231 /// [`workers(n, by_key)`](https://docs.rs/ruststream); AMQP itself does not interpret the
232 /// header, so the producer sets it.
233 fn partition_key(&self) -> Option<&[u8]> {
234 self.headers.get(PARTITION_KEY_HEADER)
235 }
236}