#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt::{Debug, Formatter};
use std::future::Future;
#[cfg(feature = "testing")]
use std::future::ready;
use std::panic::resume_unwind;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use async_nats::jetstream;
use async_nats::jetstream::consumer::{PullConsumer, pull::Config as ConsumerConfig};
use async_nats::{Client, ConnectOptions};
#[cfg(feature = "testing")]
use bytes::Bytes;
#[cfg(feature = "testing")]
use ruststream::testing::{Coordinator, InProcess, TestableBroker};
use ruststream::{
AddressedCopies, Broker, ConnectedBroker, DefaultPublish, DescribeServer, ServerSpec, Subscribe,
};
#[cfg(feature = "testing")]
use ruststream::{OutgoingMessage, RawMessage};
use tokio::runtime::Handle;
#[cfg(feature = "testing")]
use crate::in_process::{self, Bus, Origin, PublishMode, RouteBook};
use crate::{
error::NatsError,
publisher::{NatsPublish, NatsPublishPolicy},
subject::{CoreSubject, NatsSubscription, SubscriptionPlan},
subscriber::NatsSubscriber,
};
pub(crate) struct NatsConnection {
client: Client,
closed: AtomicBool,
runtime: Option<Handle>,
}
impl Debug for NatsConnection {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NatsConnection")
.field("closed", &self.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl NatsConnection {
fn new(client: Client, runtime: Option<Handle>) -> Arc<Self> {
Arc::new(Self {
client,
closed: AtomicBool::new(false),
runtime,
})
}
pub(crate) fn jetstream(&self) -> jetstream::Context {
let _entered = self.runtime.as_ref().map(Handle::enter);
jetstream::new(self.client.clone())
}
async fn on_runtime<Output: Send + 'static>(
&self,
work: impl Future<Output = Output> + Send + 'static,
) -> Result<Output, NatsError> {
let Some(runtime) = &self.runtime else {
return Ok(work.await);
};
runtime.spawn(work).await.map_err(|err| {
if err.is_panic() {
resume_unwind(err.into_panic());
}
NatsError::JetStream(Box::new(err))
})
}
pub(crate) fn live_client(&self, subject: &str) -> Result<&Client, NatsError> {
if self.closed.load(Ordering::Acquire) {
return Err(NatsError::Closed {
subject: subject.to_owned(),
});
}
Ok(&self.client)
}
pub(crate) const fn client(&self) -> &Client {
&self.client
}
}
#[derive(Debug, Clone)]
pub(crate) enum Link {
Nats(Arc<NatsConnection>),
#[cfg(feature = "testing")]
InProcess(Arc<Bus>),
}
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<Link>() == size_of::<Arc<NatsConnection>>());
#[cfg(not(feature = "testing"))]
const _: () = assert!(size_of::<ConnectedNatsBroker>() == size_of::<Arc<NatsConnection>>());
#[derive(Debug, Clone)]
#[must_use]
pub struct NatsBroker {
addrs: String,
options: ConnectOptions,
}
impl NatsBroker {
pub fn new(addrs: impl Into<String>) -> Self {
Self {
addrs: addrs.into(),
options: ConnectOptions::default(),
}
}
pub fn with_options(mut self, options: ConnectOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn addrs(&self) -> &str {
&self.addrs
}
}
impl Broker for NatsBroker {
type Error = NatsError;
type Connected = ConnectedNatsBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
#[cfg(feature = "testing")]
let routes = RouteBook::new(in_process::settings(&self.options)?.no_echo);
let client = self
.options
.connect(self.addrs.as_str())
.await
.map_err(|err| NatsError::Connect(Box::new(err)))?;
Ok(ConnectedNatsBroker {
link: Link::Nats(NatsConnection::new(client, Some(Handle::current()))),
#[cfg(feature = "testing")]
routes,
})
}
}
#[cfg(feature = "testing")]
impl InProcess for NatsBroker {
fn connect_in_process(
self,
) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send {
ready(
in_process::connect(&self.addrs, &self.options).map(|bus| ConnectedNatsBroker {
routes: RouteBook::new(bus.no_echo()),
link: Link::InProcess(bus),
}),
)
}
}
#[cfg(feature = "testing")]
ruststream::register_testable_broker!(NatsBroker);
impl DescribeServer for NatsBroker {
fn describe_server(&self) -> ServerSpec {
let hosts = self
.addrs
.split(',')
.map(|addr| ServerSpec::host_from_url(addr.trim()))
.filter(|host| !host.is_empty())
.collect::<Vec<_>>()
.join(",");
ServerSpec::new(hosts, "nats")
}
}
#[derive(Debug)]
pub struct ConnectedNatsBroker {
link: Link,
#[cfg(feature = "testing")]
routes: RouteBook,
}
impl ConnectedNatsBroker {
#[must_use]
pub fn from_client(client: Client) -> Self {
Self {
link: Link::Nats(NatsConnection::new(client, Handle::try_current().ok())),
#[cfg(feature = "testing")]
routes: RouteBook::new(false),
}
}
#[must_use]
pub fn publisher<P: NatsPublishPolicy>(&self, policy: P) -> P::Live {
policy.bind(self)
}
#[must_use]
pub fn client(&self) -> Client {
self.connection().client().clone()
}
fn connection(&self) -> &Arc<NatsConnection> {
match &self.link {
Link::Nats(connection) => connection,
#[cfg(feature = "testing")]
Link::InProcess(_) => panic!(
"a broker connected in process has no async-nats client: `client` and \
`jetstream` are for a broker connected with `connect`"
),
}
}
#[must_use]
pub fn server_spec(&self) -> ServerSpec {
let connection = match &self.link {
Link::Nats(connection) => connection,
#[cfg(feature = "testing")]
Link::InProcess(_) => return ServerSpec::in_process("nats"),
};
let info = connection.client().server_info();
ServerSpec::new(format!("{}:{}", info.host, info.port), "nats")
.protocol_version(info.proto.to_string())
}
#[must_use]
pub fn jetstream(&self) -> jetstream::Context {
self.connection().jetstream()
}
pub(crate) const fn link(&self) -> &Link {
&self.link
}
pub async fn subscribe_with<S: NatsSubscription>(
&self,
source: S,
) -> Result<NatsSubscriber, NatsError> {
source.ensure_subject()?;
#[cfg(feature = "testing")]
if let Link::InProcess(bus) = &self.link {
return in_process::subscribe(bus, &self.routes, &source);
}
#[cfg(feature = "testing")]
let route = in_process::route(&source);
let subscriber = self.subscribe_live(source).await?;
#[cfg(feature = "testing")]
if let Some(route) = route {
self.routes.record(route);
}
Ok(subscriber)
}
async fn subscribe_live<S: NatsSubscription>(
&self,
source: S,
) -> Result<NatsSubscriber, NatsError> {
let subject = source.subject();
match source.plan() {
SubscriptionPlan::Core { queue_group } => {
self.subscribe_core(subject, queue_group).await
}
SubscriptionPlan::JetStream {
stream,
durable,
filter_subject,
ack_wait,
max_ack_pending,
deliver_policy,
pull_expires,
} => {
let consumer_cfg = ConsumerConfig {
durable_name: durable.map(str::to_owned),
filter_subject: filter_subject.to_owned(),
max_ack_pending,
ack_wait,
deliver_policy,
..Default::default()
};
self.subscribe_jetstream(subject, stream, consumer_cfg, pull_expires)
.await
}
}
}
async fn subscribe_core(
&self,
subject: &str,
queue_group: Option<&str>,
) -> Result<NatsSubscriber, NatsError> {
let client = self.connection().live_client(subject)?;
let subject = subject.to_owned();
let inner = if let Some(queue) = queue_group {
client
.queue_subscribe(subject.clone(), queue.to_owned())
.await
.map_err(|err| NatsError::Subscribe(Box::new(err)))?
} else {
client
.subscribe(subject.clone())
.await
.map_err(|err| NatsError::Subscribe(Box::new(err)))?
};
client
.flush()
.await
.map_err(|err| NatsError::Subscribe(Box::new(err)))?;
Ok(NatsSubscriber::from_core(subject, inner))
}
async fn subscribe_jetstream(
&self,
subject: &str,
stream_name: &str,
consumer_cfg: ConsumerConfig,
pull_expires: Duration,
) -> Result<NatsSubscriber, NatsError> {
let connection = self.connection();
connection.live_client(subject)?;
let ctx = connection.jetstream();
let stream = ctx
.get_stream(stream_name)
.await
.map_err(|err| NatsError::JetStream(Box::new(err)))?;
let consumer: PullConsumer = stream
.create_consumer(consumer_cfg)
.await
.map_err(|err| NatsError::JetStream(Box::new(err)))?;
let pulling = consumer.clone();
let messages = connection
.on_runtime(async move { pulling.messages().await })
.await?
.map_err(|err| NatsError::JetStream(Box::new(err)))?;
Ok(NatsSubscriber::from_jetstream(
subject.to_owned(),
stream_name.to_owned(),
messages,
consumer,
pull_expires,
))
}
}
impl ConnectedBroker for ConnectedNatsBroker {
type Error = NatsError;
type Closed = ClosedNatsBroker;
async fn shutdown(self) -> Result<Self::Closed, Self::Error> {
let connection = match self.link {
Link::Nats(connection) => connection,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
let (messages_sent, messages_received) = bus.close();
return Ok(ClosedNatsBroker {
messages_sent,
messages_received,
connects: 1,
});
}
};
connection.closed.store(true, Ordering::Release);
let client = connection.client();
let stats = client.statistics();
client
.drain()
.await
.map_err(|err| NatsError::Shutdown(Box::new(err)))?;
Ok(ClosedNatsBroker {
messages_sent: stats.out_messages.load(Ordering::Relaxed),
messages_received: stats.in_messages.load(Ordering::Relaxed),
connects: stats.connects.load(Ordering::Relaxed),
})
}
}
impl Subscribe for ConnectedNatsBroker {
type Subscriber = NatsSubscriber;
type Copies = AddressedCopies;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.subscribe_with(CoreSubject::new(name)).await
}
}
impl DefaultPublish for ConnectedNatsBroker {
type Policy = NatsPublish;
}
#[cfg(feature = "testing")]
impl TestableBroker for ConnectedNatsBroker {
fn install_coordinator(&self, coordinator: Coordinator) {
if let Link::InProcess(bus) = &self.link {
bus.install(coordinator);
}
}
fn inject(&self, message: OutgoingMessage<'_>) {
let bus = self.bus("inject");
if let Err(err) = bus.publish(
message.name(),
Bytes::copy_from_slice(message.payload()),
message.headers(),
None,
PublishMode::Core,
Origin::External,
) {
panic!(
"the injected message to {:?} is not one the server takes: {err}",
message.name()
);
}
}
fn published(&self, name: &str) -> Vec<RawMessage> {
self.bus("published").published(name)
}
fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
self.routes.routes(destination, subscriptions)
}
}
#[cfg(feature = "testing")]
impl ConnectedNatsBroker {
fn bus(&self, what: &str) -> &Arc<Bus> {
match &self.link {
Link::InProcess(bus) => bus,
Link::Nats(_) => panic!(
"TestableBroker::{what} reached a broker connected with `connect`; the harness \
drives the connection `connect_in_process` produces"
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClosedNatsBroker {
messages_sent: u64,
messages_received: u64,
connects: u64,
}
impl ClosedNatsBroker {
#[must_use]
pub const fn messages_sent(&self) -> u64 {
self.messages_sent
}
#[must_use]
pub const fn messages_received(&self) -> u64 {
self.messages_received
}
#[must_use]
pub const fn connects(&self) -> u64 {
self.connects
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_performs_no_io_and_describes_the_configured_address() {
let broker = NatsBroker::new("nats://127.0.0.1:4222");
let spec = broker.describe_server();
assert_eq!(spec.protocol, "nats");
assert_eq!(spec.host.as_deref(), Some("127.0.0.1:4222"));
}
#[test]
fn the_host_survives_every_address_shape_the_client_accepts() {
for (addr, expected) in [
("nats://127.0.0.1:4222", "127.0.0.1:4222"),
("tls://nats.example.com:4222", "nats.example.com:4222"),
("nats.example.com:4222", "nats.example.com:4222"),
("nats://nats.example.com", "nats.example.com"),
(
"nats://alice:s3cret@nats.example.com:4222",
"nats.example.com:4222",
),
(
"nats://s3cret-token@nats.example.com:4222",
"nats.example.com:4222",
),
(
"nats://alice:p@ss@nats.example.com:4222",
"nats.example.com:4222",
),
("nats://nats.example.com:4222/a@b", "nats.example.com:4222"),
(
"nats://nats.example.com:4222/?token=a@b",
"nats.example.com:4222",
),
("nats://nats.example.com:4222#a@b", "nats.example.com:4222"),
] {
let spec = NatsBroker::new(addr).describe_server();
assert_eq!(spec.host.as_deref(), Some(expected), "parsing {addr}");
}
}
#[test]
fn a_list_of_addresses_describes_every_host_and_no_credentials() {
let spec = NatsBroker::new(
"nats://alice:s3cret@one.example.com:4222, nats://tok@two.example.com:4223",
)
.describe_server();
let host = spec.host.expect("a networked broker states its host");
assert_eq!(host, "one.example.com:4222,two.example.com:4223");
assert!(
!host.contains('@'),
"the userinfo separator is gone: {host}"
);
}
#[test]
fn an_address_carrying_credentials_describes_a_server_without_them() {
for addr in [
"nats://alice:s3cret@nats.example.com:4222",
"nats://s3cret@nats.example.com:4222",
] {
let host = NatsBroker::new(addr)
.describe_server()
.host
.expect("a networked broker states its host");
assert_eq!(host, "nats.example.com:4222");
assert!(!host.contains("s3cret"), "{addr} leaked {host:?}");
assert!(!host.contains("alice"), "{addr} leaked {host:?}");
assert!(!host.contains('@'), "{addr} leaked {host:?}");
}
}
}