#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use async_nats::jetstream::consumer::{PullConsumer, pull::Stream as PullStream};
use futures::stream::{poll_fn, unfold};
use futures::{Stream, StreamExt, future::Either};
use ruststream::{BatchSubscriber, BufferedSubscriber, Subscriber};
use std::fmt::{Debug, Formatter};
use std::num::NonZeroUsize;
#[cfg(feature = "testing")]
use std::task::Poll;
use std::{pin::Pin, time::Duration};
use tracing::warn;
#[cfg(feature = "testing")]
use crate::in_process::{Consumer, Feed};
use crate::{
error::NatsError,
message::{CoreMessage, JetStreamMessage, NatsMessage},
};
const CORE_BATCH_MAX_WAIT: Duration = Duration::from_millis(10);
enum SubscriberKind {
Core(BufferedSubscriber<CoreSubscriber>),
JetStream(Box<JetStreamKind>),
#[cfg(feature = "testing")]
InProcessJetStream(Box<Consumer>),
}
struct JetStreamKind {
inner: Pin<Box<PullStream>>,
consumer: PullConsumer,
stream_name: String,
pull_expires: Duration,
}
struct CoreSubscriber {
inner: CoreFeed,
}
enum CoreFeed {
Nats(async_nats::Subscriber),
#[cfg(feature = "testing")]
InProcess(Feed),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<CoreFeed>() == size_of::<async_nats::Subscriber>());
impl Subscriber for CoreSubscriber {
type Message = NatsMessage;
type Error = NatsError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
poll_fn(move |cx| match &mut self.inner {
CoreFeed::Nats(inner) => Pin::new(inner)
.poll_next(cx)
.map(|msg| msg.map(core_message)),
#[cfg(feature = "testing")]
CoreFeed::InProcess(feed) => feed.poll_next(cx).map(|delivery| {
delivery.map(|delivery| {
NatsMessage::Core(Box::new(CoreMessage::in_process(
delivery.message,
delivery.release,
)))
})
}),
})
.map(Ok)
}
}
pub struct NatsSubscriber {
subject: String,
kind: SubscriberKind,
}
impl Debug for NatsSubscriber {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("NatsSubscriber");
s.field("subject", &self.subject);
match &self.kind {
SubscriberKind::Core(_) => {
s.field("kind", &"core");
}
SubscriberKind::JetStream(js) => {
s.field("kind", &"jetstream")
.field("stream", &js.stream_name);
}
#[cfg(feature = "testing")]
SubscriberKind::InProcessJetStream(consumer) => {
s.field("kind", &"jetstream")
.field("stream", &consumer.stream());
}
}
s.finish_non_exhaustive()
}
}
impl NatsSubscriber {
pub(crate) fn from_core(subject: String, inner: async_nats::Subscriber) -> Self {
Self {
subject,
kind: SubscriberKind::Core(
BufferedSubscriber::new(CoreSubscriber {
inner: CoreFeed::Nats(inner),
})
.max_wait(CORE_BATCH_MAX_WAIT),
),
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process_core(subject: String, feed: Feed) -> Self {
Self {
subject,
kind: SubscriberKind::Core(
BufferedSubscriber::new(CoreSubscriber {
inner: CoreFeed::InProcess(feed),
})
.max_wait(CORE_BATCH_MAX_WAIT),
),
}
}
#[cfg(feature = "testing")]
pub(crate) fn in_process_jetstream(subject: String, consumer: Consumer) -> Self {
Self {
subject,
kind: SubscriberKind::InProcessJetStream(Box::new(consumer)),
}
}
pub(crate) fn from_jetstream(
subject: String,
stream_name: String,
inner: PullStream,
consumer: PullConsumer,
pull_expires: Duration,
) -> Self {
Self {
subject,
kind: SubscriberKind::JetStream(Box::new(JetStreamKind {
inner: Box::pin(inner),
consumer,
stream_name,
pull_expires,
})),
}
}
}
fn core_message(msg: async_nats::Message) -> NatsMessage {
NatsMessage::Core(Box::new(CoreMessage::new(msg)))
}
fn jetstream_message(msg: async_nats::jetstream::Message) -> NatsMessage {
NatsMessage::JetStream(Box::new(JetStreamMessage::new(msg)))
}
impl Subscriber for NatsSubscriber {
type Message = NatsMessage;
type Error = NatsError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
match &mut self.kind {
SubscriberKind::Core(core) => Either::Left(core.stream()),
SubscriberKind::JetStream(js) => {
let stream =
poll_fn(move |cx| js.inner.as_mut().poll_next(cx)).map(|item| match item {
Ok(msg) => Ok(jetstream_message(msg)),
Err(err) => {
warn!(target: "ruststream::nats", error = %err, "jetstream fetch error");
Err(NatsError::JetStream(Box::new(err)))
}
});
#[cfg(feature = "testing")]
let stream = Either::Left(stream);
Either::Right(stream)
}
#[cfg(feature = "testing")]
SubscriberKind::InProcessJetStream(consumer) => Either::Right(Either::Right(
poll_fn(move |cx| consumer.poll_next(cx)).map(|delivery| {
Ok(NatsMessage::JetStream(Box::new(
JetStreamMessage::in_process(delivery),
)))
}),
)),
}
}
}
impl BatchSubscriber for NatsSubscriber {
type Batch = Vec<NatsMessage>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
match &mut self.kind {
SubscriberKind::Core(core) => Either::Left(core.batches(size)),
#[cfg(feature = "testing")]
SubscriberKind::InProcessJetStream(consumer) => {
let limit = size.get();
Either::Right(Either::Right(poll_fn(move |cx| {
let first = match consumer.poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(delivery)) => delivery,
};
let mut batch = vec![NatsMessage::JetStream(Box::new(
JetStreamMessage::in_process(first),
))];
while batch.len() < limit {
match consumer.poll_next(cx) {
Poll::Ready(Some(delivery)) => batch.push(NatsMessage::JetStream(
Box::new(JetStreamMessage::in_process(delivery)),
)),
Poll::Ready(None) | Poll::Pending => break,
}
}
Poll::Ready(Some(Ok(batch)))
})))
}
SubscriberKind::JetStream(js) => {
let max = size.get();
let expires = js.pull_expires;
let batches = unfold(&mut js.consumer, move |consumer| async move {
loop {
let fetch = consumer
.fetch()
.max_messages(max)
.expires(expires)
.messages()
.await;
let mut messages = match fetch {
Ok(messages) => messages,
Err(err) => {
return Some((Err(NatsError::JetStream(Box::new(err))), consumer));
}
};
let mut batch = Vec::new();
while let Some(item) = messages.next().await {
match item {
Ok(msg) => batch.push(jetstream_message(msg)),
Err(err) => {
if batch.is_empty() {
return Some((Err(NatsError::JetStream(err)), consumer));
}
warn!(
target: "ruststream::nats",
error = %err,
"jetstream fetch error mid-batch; delivering the partial batch",
);
break;
}
}
}
if !batch.is_empty() {
return Some((Ok(batch), consumer));
}
}
});
#[cfg(feature = "testing")]
let batches = Either::Left(batches);
Either::Right(batches)
}
}
}
}