Skip to main content

ruststream_nats/
subscriber.rs

1//! Unified NATS subscriber wrapping either a Core or a `JetStream` pull consumer.
2
3// Without the `testing` feature a Core feed has one variant; see `broker.rs`.
4#![cfg_attr(
5    not(feature = "testing"),
6    allow(clippy::infallible_destructuring_match)
7)]
8
9use async_nats::jetstream::consumer::{PullConsumer, pull::Stream as PullStream};
10use futures::stream::{poll_fn, unfold};
11use futures::{Stream, StreamExt, future::Either};
12use ruststream::{BatchSubscriber, BufferedSubscriber, Subscriber};
13use std::fmt::{Debug, Formatter};
14use std::num::NonZeroUsize;
15#[cfg(feature = "testing")]
16use std::task::Poll;
17use std::{pin::Pin, time::Duration};
18use tracing::warn;
19
20#[cfg(feature = "testing")]
21use crate::in_process::{Consumer, Feed};
22use crate::{
23    error::NatsError,
24    message::{CoreMessage, JetStreamMessage, NatsMessage},
25};
26
27/// How long a partial Core NATS batch waits for further deliveries after its first one.
28///
29/// Core NATS has no wire-level batch, so its batches are assembled on the client and the deadline
30/// that closes a partial one is this crate's own choice (the batch *size* is the registration's).
31/// The window is short enough to add no meaningful latency to a batch that will not fill, and long
32/// enough to gather a burst already in flight.
33const CORE_BATCH_MAX_WAIT: Duration = Duration::from_millis(10);
34
35enum SubscriberKind {
36    // Core deliveries arrive one at a time, so the capability comes from the core adapter rather
37    // than from the wire; everything else reaches through it unchanged.
38    Core(BufferedSubscriber<CoreSubscriber>),
39    // Box the JetStream variant: PullConsumer is large (~1400 bytes) and the enum would otherwise
40    // penalise the Core path with the same footprint.
41    JetStream(Box<JetStreamKind>),
42    /// A consumer of the in-process transport, under the `testing` feature only.
43    #[cfg(feature = "testing")]
44    InProcessJetStream(Box<Consumer>),
45}
46
47struct JetStreamKind {
48    inner: Pin<Box<PullStream>>,
49    consumer: PullConsumer,
50    stream_name: String,
51    pull_expires: Duration,
52}
53
54/// The plain Core NATS subscription: one delivery per stream item, which is what
55/// [`BufferedSubscriber`] batches on the client.
56struct CoreSubscriber {
57    inner: CoreFeed,
58}
59
60/// Where Core deliveries come from: the client's subscription, or, under the `testing` feature,
61/// the in-process transport. Both hand over the client's own message, so batching and the
62/// delivery type are the production ones either way.
63enum CoreFeed {
64    Nats(async_nats::Subscriber),
65    #[cfg(feature = "testing")]
66    InProcess(Feed),
67}
68
69// A production build holds the client's subscription and nothing else.
70#[cfg(not(feature = "testing"))]
71const _: () = assert!(size_of::<CoreFeed>() == size_of::<async_nats::Subscriber>());
72
73impl Subscriber for CoreSubscriber {
74    type Message = NatsMessage;
75    type Error = NatsError;
76
77    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
78        // Poll the inner subscription in place rather than moving it into the returned stream,
79        // so `stream` can be called again after the returned stream is dropped (the runtime and
80        // the conformance helpers re-enter it per call).
81        poll_fn(move |cx| match &mut self.inner {
82            CoreFeed::Nats(inner) => Pin::new(inner)
83                .poll_next(cx)
84                .map(|msg| msg.map(core_message)),
85            #[cfg(feature = "testing")]
86            CoreFeed::InProcess(feed) => feed.poll_next(cx).map(|delivery| {
87                delivery.map(|delivery| {
88                    NatsMessage::Core(Box::new(CoreMessage::in_process(
89                        delivery.message,
90                        delivery.release,
91                    )))
92                })
93            }),
94        })
95        .map(Ok)
96    }
97}
98
99/// A NATS subscription.
100///
101/// Backed transparently by either a Core subscription (no ack) or a `JetStream` pull consumer
102/// (full ack/nack/term). Construct via [`ConnectedNatsBroker::subscribe_with`] with
103/// [`CoreSubject`] or [`JetStreamSubject`], or let the runtime resolve either source at startup.
104///
105/// [`ConnectedNatsBroker::subscribe_with`]: crate::ConnectedNatsBroker::subscribe_with
106/// [`CoreSubject`]: crate::CoreSubject
107/// [`JetStreamSubject`]: crate::JetStreamSubject
108pub struct NatsSubscriber {
109    subject: String,
110    kind: SubscriberKind,
111}
112
113impl Debug for NatsSubscriber {
114    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
115        let mut s = f.debug_struct("NatsSubscriber");
116        s.field("subject", &self.subject);
117        match &self.kind {
118            SubscriberKind::Core(_) => {
119                s.field("kind", &"core");
120            }
121            SubscriberKind::JetStream(js) => {
122                s.field("kind", &"jetstream")
123                    .field("stream", &js.stream_name);
124            }
125            #[cfg(feature = "testing")]
126            SubscriberKind::InProcessJetStream(consumer) => {
127                s.field("kind", &"jetstream")
128                    .field("stream", &consumer.stream());
129            }
130        }
131        s.finish_non_exhaustive()
132    }
133}
134
135impl NatsSubscriber {
136    pub(crate) fn from_core(subject: String, inner: async_nats::Subscriber) -> Self {
137        Self {
138            subject,
139            kind: SubscriberKind::Core(
140                BufferedSubscriber::new(CoreSubscriber {
141                    inner: CoreFeed::Nats(inner),
142                })
143                .max_wait(CORE_BATCH_MAX_WAIT),
144            ),
145        }
146    }
147
148    /// A Core subscription of the in-process transport, batched on the client exactly as a live
149    /// one is.
150    #[cfg(feature = "testing")]
151    pub(crate) fn in_process_core(subject: String, feed: Feed) -> Self {
152        Self {
153            subject,
154            kind: SubscriberKind::Core(
155                BufferedSubscriber::new(CoreSubscriber {
156                    inner: CoreFeed::InProcess(feed),
157                })
158                .max_wait(CORE_BATCH_MAX_WAIT),
159            ),
160        }
161    }
162
163    /// A subscription reading a consumer of the in-process transport.
164    #[cfg(feature = "testing")]
165    pub(crate) fn in_process_jetstream(subject: String, consumer: Consumer) -> Self {
166        Self {
167            subject,
168            kind: SubscriberKind::InProcessJetStream(Box::new(consumer)),
169        }
170    }
171
172    pub(crate) fn from_jetstream(
173        subject: String,
174        stream_name: String,
175        inner: PullStream,
176        consumer: PullConsumer,
177        pull_expires: Duration,
178    ) -> Self {
179        Self {
180            subject,
181            kind: SubscriberKind::JetStream(Box::new(JetStreamKind {
182                inner: Box::pin(inner),
183                consumer,
184                stream_name,
185                pull_expires,
186            })),
187        }
188    }
189}
190
191fn core_message(msg: async_nats::Message) -> NatsMessage {
192    NatsMessage::Core(Box::new(CoreMessage::new(msg)))
193}
194
195fn jetstream_message(msg: async_nats::jetstream::Message) -> NatsMessage {
196    NatsMessage::JetStream(Box::new(JetStreamMessage::new(msg)))
197}
198
199impl Subscriber for NatsSubscriber {
200    type Message = NatsMessage;
201    type Error = NatsError;
202
203    fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
204        // Poll the inner subscription in place rather than moving it into the returned stream,
205        // so `stream` can be called again after the returned stream is dropped (the runtime and
206        // the conformance helpers re-enter it per call).
207        match &mut self.kind {
208            // Batching does not change the per-message path: the adapter forwards it untouched.
209            SubscriberKind::Core(core) => Either::Left(core.stream()),
210            SubscriberKind::JetStream(js) => {
211                let stream =
212                    poll_fn(move |cx| js.inner.as_mut().poll_next(cx)).map(|item| match item {
213                        Ok(msg) => Ok(jetstream_message(msg)),
214                        Err(err) => {
215                            warn!(target: "ruststream::nats", error = %err, "jetstream fetch error");
216                            Err(NatsError::JetStream(Box::new(err)))
217                        }
218                    });
219                // The in-process arm is a second stream type, so the production one is nested
220                // beside it only when the feature brings it.
221                #[cfg(feature = "testing")]
222                let stream = Either::Left(stream);
223                Either::Right(stream)
224            }
225            #[cfg(feature = "testing")]
226            SubscriberKind::InProcessJetStream(consumer) => Either::Right(Either::Right(
227                poll_fn(move |cx| consumer.poll_next(cx)).map(|delivery| {
228                    Ok(NatsMessage::JetStream(Box::new(
229                        JetStreamMessage::in_process(delivery),
230                    )))
231                }),
232            )),
233        }
234    }
235}
236
237impl BatchSubscriber for NatsSubscriber {
238    type Batch = Vec<NatsMessage>;
239
240    /// Returns a stream of batches of at most `size` messages.
241    ///
242    /// `size` is the registration's batch size, and each transport spends it in its own currency.
243    /// `JetStream` batches on the wire: one stream item is one pull `fetch` of up to `size`
244    /// messages, waiting at most [`pull_expires`](crate::JetStreamSubject::pull_expires) before
245    /// delivering a partial batch (an empty fetch is retried, so the stream never yields empty
246    /// batches). Core NATS has no wire-level batching, so its batches are assembled on the client
247    /// by the framework's [`BufferedSubscriber`]: a batch closes at `size` deliveries or 10 ms
248    /// after its first one, whichever comes first.
249    ///
250    /// Drive a subscriber through either [`Subscriber::stream`] or `batches`, not both at once:
251    /// on `JetStream` each issues its own pull requests, so deliveries would be split between
252    /// them.
253    ///
254    /// # Cancel safety
255    ///
256    /// Dropping the returned stream between items is allowed. On `JetStream`, dropping it
257    /// mid-fetch can leave already-fetched, undelivered messages to be redelivered after the
258    /// consumer's `ack_wait`.
259    fn batches(
260        &mut self,
261        size: NonZeroUsize,
262    ) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
263        match &mut self.kind {
264            SubscriberKind::Core(core) => Either::Left(core.batches(size)),
265            // A fetch hands over what the consumer has, up to `size`, and waits only for the
266            // first message: so does this.
267            #[cfg(feature = "testing")]
268            SubscriberKind::InProcessJetStream(consumer) => {
269                let limit = size.get();
270                Either::Right(Either::Right(poll_fn(move |cx| {
271                    let first = match consumer.poll_next(cx) {
272                        Poll::Pending => return Poll::Pending,
273                        Poll::Ready(None) => return Poll::Ready(None),
274                        Poll::Ready(Some(delivery)) => delivery,
275                    };
276                    let mut batch = vec![NatsMessage::JetStream(Box::new(
277                        JetStreamMessage::in_process(first),
278                    ))];
279                    while batch.len() < limit {
280                        match consumer.poll_next(cx) {
281                            Poll::Ready(Some(delivery)) => batch.push(NatsMessage::JetStream(
282                                Box::new(JetStreamMessage::in_process(delivery)),
283                            )),
284                            Poll::Ready(None) | Poll::Pending => break,
285                        }
286                    }
287                    Poll::Ready(Some(Ok(batch)))
288                })))
289            }
290            SubscriberKind::JetStream(js) => {
291                let max = size.get();
292                let expires = js.pull_expires;
293                let batches = unfold(&mut js.consumer, move |consumer| async move {
294                    loop {
295                        let fetch = consumer
296                            .fetch()
297                            .max_messages(max)
298                            .expires(expires)
299                            .messages()
300                            .await;
301                        let mut messages = match fetch {
302                            Ok(messages) => messages,
303                            // BatchError is a concrete sized type; wrap it.
304                            Err(err) => {
305                                return Some((Err(NatsError::JetStream(Box::new(err))), consumer));
306                            }
307                        };
308                        let mut batch = Vec::new();
309                        while let Some(item) = messages.next().await {
310                            match item {
311                                Ok(msg) => batch.push(jetstream_message(msg)),
312                                // crate::Error is already Box<dyn StdError + ...>; use directly.
313                                Err(err) => {
314                                    if batch.is_empty() {
315                                        return Some((Err(NatsError::JetStream(err)), consumer));
316                                    }
317                                    warn!(
318                                        target: "ruststream::nats",
319                                        error = %err,
320                                        "jetstream fetch error mid-batch; delivering the partial batch",
321                                    );
322                                    break;
323                                }
324                            }
325                        }
326                        if !batch.is_empty() {
327                            return Some((Ok(batch), consumer));
328                        }
329                        // An empty fetch only means `expires` elapsed with nothing pending.
330                    }
331                });
332                #[cfg(feature = "testing")]
333                let batches = Either::Left(batches);
334                Either::Right(batches)
335            }
336        }
337    }
338}