Skip to main content

ruststream_nats/
message.rs

1//! Delivered-message wrapper that implements [`IncomingMessage`].
2
3// Without the `testing` feature a `JetStream` delivery has one variant; see `broker.rs`.
4#![cfg_attr(
5    not(feature = "testing"),
6    allow(clippy::infallible_destructuring_match)
7)]
8
9use std::fmt::{Debug, Formatter};
10use std::sync::OnceLock;
11use std::time::Duration;
12
13use async_nats::jetstream::AckKind;
14use ruststream::{AckError, HeaderMap, IncomingMessage, Partitioned, Str};
15
16use crate::convert::headers_from_nats;
17#[cfg(feature = "testing")]
18use crate::in_process::{JetStreamDelivery, Release};
19
20/// A NATS delivery. Two flavours: core NATS (no ack) and `JetStream` (real ack/nack/redelivery).
21///
22/// Both variants are boxed to keep the enum compact; the wrapped `async_nats` messages are large.
23pub enum NatsMessage {
24    /// A core NATS subject delivery. Acknowledgement is not supported.
25    Core(Box<CoreMessage>),
26    /// A `JetStream` pull-consumer delivery with full ack support.
27    JetStream(Box<JetStreamMessage>),
28}
29
30impl Debug for NatsMessage {
31    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::Core(_) => f.debug_struct("NatsMessage::Core").finish_non_exhaustive(),
34            Self::JetStream(_) => f
35                .debug_struct("NatsMessage::JetStream")
36                .finish_non_exhaustive(),
37        }
38    }
39}
40
41/// Wrapper around an `async_nats::Message` from a core (non-JetStream) subscription.
42pub struct CoreMessage {
43    inner: async_nats::Message,
44    /// `None` where the delivery carried nothing to put in a map, so a subscription of headerless
45    /// deliveries builds none at all; [`headers`](IncomingMessage::headers) answers the crate's
46    /// one empty map instead.
47    headers: Option<HeaderMap>,
48    /// Keeps a delivery of the in-process transport counted with the test harness until it
49    /// drops. The field is there only with the `testing` feature.
50    #[cfg(feature = "testing")]
51    _release: Release,
52}
53
54impl Debug for CoreMessage {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("CoreMessage")
57            .field("subject", &self.inner.subject.as_str())
58            .field("payload_len", &self.inner.payload.len())
59            .finish_non_exhaustive()
60    }
61}
62
63impl CoreMessage {
64    pub(crate) fn new(inner: async_nats::Message) -> Self {
65        if inner.headers.is_none() && inner.reply.is_none() {
66            return Self {
67                inner,
68                headers: None,
69                #[cfg(feature = "testing")]
70                _release: Release::uncounted(),
71            };
72        }
73        let mut headers = headers_from_nats(inner.headers.as_ref());
74        // NATS carries the request inbox in the wire-level `reply` field, not in a header.
75        // Surface it as the well-known `reply-to` header so framework handlers can respond
76        // (the in-memory testing broker already exposes it this way). The wire field is
77        // authoritative: it overrides a literal `reply-to` header if both are present.
78        // JetStream deliveries are excluded on purpose - there `reply` is the ack inbox.
79        if let Some(reply) = inner.reply.as_ref() {
80            headers.insert(Str::from_static(REPLY_TO_HEADER), reply.as_str().to_owned());
81        }
82        Self {
83            inner,
84            headers: Some(headers),
85            #[cfg(feature = "testing")]
86            _release: Release::uncounted(),
87        }
88    }
89
90    /// A delivery of the in-process transport: the client's own message, counted with the test
91    /// harness until it drops.
92    #[cfg(feature = "testing")]
93    pub(crate) fn in_process(inner: async_nats::Message, release: Release) -> Self {
94        Self {
95            _release: release,
96            ..Self::new(inner)
97        }
98    }
99}
100
101/// Wrapper around an `async_nats::jetstream::Message` with ack semantics.
102pub struct JetStreamMessage {
103    inner: JetStreamInner,
104    /// `None` where the delivery carried no headers; see [`CoreMessage::headers`].
105    headers: Option<HeaderMap>,
106}
107
108/// A consumer's delivery: the client's, or, under the `testing` feature, the in-process
109/// transport's, which settles through its own consumer.
110// The size difference exists only with the `testing` feature; boxing the production variant to
111// even it out would cost every production delivery an allocation.
112#[cfg_attr(feature = "testing", allow(clippy::large_enum_variant))]
113enum JetStreamInner {
114    Nats(async_nats::jetstream::Message),
115    #[cfg(feature = "testing")]
116    InProcess(Box<JetStreamDelivery>),
117}
118
119// A production build holds the client's delivery and nothing else.
120#[cfg(not(feature = "testing"))]
121const _: () = assert!(size_of::<JetStreamInner>() == size_of::<async_nats::jetstream::Message>());
122
123/// The native `JetStream` metadata of one delivery, as the server states it.
124pub(crate) struct DeliveryMetadata<'a> {
125    pub(crate) stream: &'a str,
126    pub(crate) consumer: &'a str,
127    pub(crate) stream_sequence: u64,
128    pub(crate) consumer_sequence: u64,
129    pub(crate) delivered: i64,
130    pub(crate) pending: u64,
131}
132
133impl Debug for JetStreamMessage {
134    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("JetStreamMessage")
136            .field("subject", &self.message().subject.as_str())
137            .field("payload_len", &self.message().payload.len())
138            .finish_non_exhaustive()
139    }
140}
141
142impl JetStreamMessage {
143    pub(crate) fn new(inner: async_nats::jetstream::Message) -> Self {
144        let headers = inner
145            .message
146            .headers
147            .as_ref()
148            .map(|wire| headers_from_nats(Some(wire)));
149        Self {
150            inner: JetStreamInner::Nats(inner),
151            headers,
152        }
153    }
154
155    /// A delivery of the in-process transport.
156    #[cfg(feature = "testing")]
157    pub(crate) fn in_process(delivery: JetStreamDelivery) -> Self {
158        let headers = delivery
159            .message
160            .headers
161            .as_ref()
162            .map(|wire| headers_from_nats(Some(wire)));
163        Self {
164            inner: JetStreamInner::InProcess(Box::new(delivery)),
165            headers,
166        }
167    }
168
169    /// The message as the client read it.
170    fn message(&self) -> &async_nats::Message {
171        match &self.inner {
172            JetStreamInner::Nats(inner) => &inner.message,
173            #[cfg(feature = "testing")]
174            JetStreamInner::InProcess(delivery) => &delivery.message,
175        }
176    }
177
178    /// The native `JetStream` delivery metadata (stream/consumer name and sequences, redelivery
179    /// count, pending count), parsed from the `$JS.ACK` reply subject.
180    ///
181    /// Returns `None` when the reply subject is absent or malformed - i.e. the underlying
182    /// `async_nats` parse failed - so a caller building a context can fall back to "no metadata"
183    /// rather than surfacing an error on the per-delivery hot path.
184    pub(crate) fn metadata(&self) -> Option<DeliveryMetadata<'_>> {
185        match &self.inner {
186            JetStreamInner::Nats(inner) => inner.info().ok().map(|info| DeliveryMetadata {
187                stream: info.stream,
188                consumer: info.consumer,
189                stream_sequence: info.stream_sequence,
190                consumer_sequence: info.consumer_sequence,
191                delivered: info.delivered,
192                pending: info.pending,
193            }),
194            #[cfg(feature = "testing")]
195            JetStreamInner::InProcess(delivery) => Some(DeliveryMetadata {
196                stream: &delivery.info.stream,
197                consumer: &delivery.info.consumer,
198                stream_sequence: delivery.info.stream_sequence,
199                consumer_sequence: delivery.info.consumer_sequence,
200                delivered: i64::try_from(delivery.info.delivered).unwrap_or(i64::MAX),
201                pending: delivery.info.pending,
202            }),
203        }
204    }
205}
206
207/// The header this crate surfaces a request's wire-level reply subject under.
208///
209/// NATS carries the inbox in a protocol field rather than a header, so a handler that answers a
210/// request reads it here. The generated `AsyncAPI` document names the same header as the runtime
211/// expression a client reads a reply address from.
212///
213/// The name is static, so surfacing it on a delivery costs no allocation.
214pub(crate) const REPLY_TO_HEADER: &str = "reply-to";
215
216/// The same header, as the runtime expression an `AsyncAPI` document reports a reply address at.
217/// The specification spells the pointer out, so the header name is repeated here rather than
218/// composed.
219#[cfg(feature = "asyncapi")]
220pub(crate) const REPLY_ADDRESS_LOCATION: &str = "$message.header#/reply-to";
221
222/// The map a delivery with no headers of its own answers with.
223///
224/// The signature `headers()` has to satisfy is `-> &HeaderMap`, and building an empty map per
225/// delivery to satisfy it is the whole cost this removes.
226fn empty_headers() -> &'static HeaderMap {
227    static EMPTY: OnceLock<HeaderMap> = OnceLock::new();
228    EMPTY.get_or_init(HeaderMap::new)
229}
230
231impl IncomingMessage for NatsMessage {
232    fn payload(&self) -> &[u8] {
233        match self {
234            Self::Core(m) => &m.inner.payload,
235            Self::JetStream(m) => &m.message().payload,
236        }
237    }
238
239    fn headers(&self) -> &HeaderMap {
240        let carried = match self {
241            Self::Core(m) => m.headers.as_ref(),
242            Self::JetStream(m) => m.headers.as_ref(),
243        };
244        carried.unwrap_or_else(|| empty_headers())
245    }
246
247    /// The [`PARTITION_KEY_HEADER`] value the sender set, which the runtime's keyed worker lanes
248    /// (`workers(n, by_key)`) order by.
249    fn partition_key(&self) -> Option<&[u8]> {
250        self.headers().get(PARTITION_KEY_HEADER)
251    }
252
253    async fn ack(self) -> Result<(), AckError> {
254        match self {
255            Self::Core(_) => Err(AckError::Unsupported),
256            Self::JetStream(m) => match m.inner {
257                JetStreamInner::Nats(inner) => inner
258                    .ack()
259                    .await
260                    .map_err(|err| AckError::Broker(format_err(err))),
261                #[cfg(feature = "testing")]
262                JetStreamInner::InProcess(delivery) => delivery.ack(),
263            },
264        }
265    }
266
267    async fn nack(self, requeue: bool) -> Result<(), AckError> {
268        match self {
269            Self::Core(_) => Err(AckError::Unsupported),
270            Self::JetStream(m) => {
271                let inner = match m.inner {
272                    JetStreamInner::Nats(inner) => inner,
273                    #[cfg(feature = "testing")]
274                    JetStreamInner::InProcess(delivery) => return delivery.nack(requeue),
275                };
276                let kind = if requeue {
277                    AckKind::Nak(None)
278                } else {
279                    AckKind::Term
280                };
281                inner
282                    .ack_with(kind)
283                    .await
284                    .map_err(|err| AckError::Broker(format_err(err)))
285            }
286        }
287    }
288
289    /// How many times the server has delivered this message, counting this one.
290    ///
291    /// A `JetStream` consumer keeps that count and reports it on every delivery, so a cap
292    /// declared at the mount site counts the server's own redeliveries - an `ack_wait` that ran
293    /// out, a negative acknowledgement - and not only the copies this process published. Core
294    /// NATS neither stores a message nor redelivers one, so a core delivery has no count and the
295    /// framework's retry-count header is the whole tally.
296    fn redelivery_count(&self) -> Option<u64> {
297        match self {
298            Self::Core(_) => None,
299            Self::JetStream(m) => m.metadata().map(|info| info.delivered.unsigned_abs()),
300        }
301    }
302
303    /// Whether this delivery can honor a native delayed redelivery.
304    ///
305    /// `true` for every `JetStream` delivery: the protocol carries the delay in the negative
306    /// acknowledgement itself, so no opt-in infrastructure is needed. Core NATS has no
307    /// acknowledgement at all, so a core delivery reports `false` and the runtime applies its
308    /// broker-agnostic deferred re-publish instead.
309    fn supports_nack_after(&self) -> bool {
310        matches!(self, Self::JetStream(_))
311    }
312
313    /// Redelivers this message no sooner than `delay`, natively: `JetStream`'s negative
314    /// acknowledgement takes the delay as its argument (`-NAK {"delay": ns}`), so the server holds
315    /// the message for that long and then redelivers it on this consumer. Nothing is re-published
316    /// and no copy is made, so the delivery count, the stream sequence, and the payload all stay
317    /// the ones the message was first delivered with.
318    ///
319    /// # Errors
320    ///
321    /// Returns [`AckError::Unsupported`] on a core (non-JetStream) delivery, and
322    /// [`AckError::Broker`] when the acknowledgement cannot be sent.
323    async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
324        match self {
325            Self::Core(_) => Err(AckError::Unsupported),
326            Self::JetStream(m) => match m.inner {
327                JetStreamInner::Nats(inner) => inner
328                    .ack_with(AckKind::Nak(Some(delay)))
329                    .await
330                    .map_err(|err| AckError::Broker(format_err(err))),
331                #[cfg(feature = "testing")]
332                JetStreamInner::InProcess(delivery) => delivery.nack_after(delay),
333            },
334        }
335    }
336}
337
338/// The well-known header key for per-message routing / partitioning.
339///
340/// Set this header on outgoing messages to control key-based fan-out when the runtime is
341/// configured with `workers(N, by_key)`. The runtime hashes the value to assign a dispatch lane.
342///
343/// The value is text, because a NATS header is: a key that is not UTF-8, or that carries a
344/// newline, fails the publish rather than arriving without the header that decides its lane.
345pub const PARTITION_KEY_HEADER: &str = "nats-partition-key";
346
347/// `Partitioned` lets the `workers(N, by_key)` runtime feature assign a dispatch lane based on
348/// a well-known message header. NATS has no native partition concept, so the key travels as the
349/// [`PARTITION_KEY_HEADER`] header value and the sender is responsible for setting it.
350impl Partitioned for NatsMessage {
351    fn partition_key(&self) -> Option<&[u8]> {
352        IncomingMessage::partition_key(self)
353    }
354}
355
356fn format_err<E>(err: E) -> Box<dyn std::error::Error + Send + Sync>
357where
358    E: std::fmt::Display + Send + Sync + 'static,
359{
360    let msg = err.to_string();
361    Box::<dyn std::error::Error + Send + Sync>::from(msg)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    fn core_message(reply: Option<&str>) -> NatsMessage {
369        NatsMessage::Core(Box::new(CoreMessage::new(async_nats::Message {
370            subject: "subj".into(),
371            reply: reply.map(Into::into),
372            payload: bytes::Bytes::from_static(b"x"),
373            headers: None,
374            status: None,
375            description: None,
376            length: 1,
377        })))
378    }
379
380    /// A delivery that carries nothing to put in a map answers with the one empty map the crate
381    /// keeps, so a subscription of headerless deliveries builds none at all. Two deliveries
382    /// answering the same address is what says so.
383    #[test]
384    fn a_headerless_delivery_answers_with_the_shared_empty_map() {
385        let first = core_message(None);
386        let second = core_message(None);
387
388        assert!(
389            std::ptr::eq(first.headers(), second.headers()),
390            "a delivery with neither wire headers nor a reply inbox has nothing to build a map \
391             from, so both answer the crate's own empty one",
392        );
393    }
394
395    #[test]
396    fn core_reply_inbox_surfaces_as_reply_to_header() {
397        let msg = core_message(Some("_INBOX.42"));
398        assert_eq!(msg.headers().reply_to(), Some("_INBOX.42"));
399    }
400
401    #[test]
402    fn core_message_without_reply_has_no_reply_to() {
403        assert_eq!(core_message(None).headers().reply_to(), None);
404    }
405
406    // Core NATS has no acknowledgement, so it must not claim the native delay: the runtime reads
407    // this to decide between the native `-NAK {"delay"}` and its own deferred re-publish. The
408    // JetStream arm answers `true`, which `tests/in_process_nats.rs` and the live suite exercise.
409    #[test]
410    fn core_delivery_does_not_claim_native_delayed_redelivery() {
411        assert!(!core_message(None).supports_nack_after());
412    }
413
414    #[tokio::test]
415    async fn core_delivery_reports_nack_after_unsupported() {
416        let err = core_message(None)
417            .nack_after(Duration::from_secs(1))
418            .await
419            .expect_err("core NATS cannot honor a delayed redelivery");
420        assert!(matches!(err, AckError::Unsupported));
421    }
422}