use std::{borrow::Cow, fmt, future::Future, pin::Pin, sync::Arc};
use bytes::BytesMut;
use serde::Serialize;
use thiserror::Error;
use tracing::warn;
use super::lifecycle::BoxError;
use crate::codec::{Codec, CodecError};
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
use crate::codec::DefaultCodec;
use crate::runtime::publish::sealed::Sealed;
use crate::{
ConnectedBroker, Headers, OutgoingMessage, OwnedTransactions, PairError, PublishPolicy,
Publisher, Transaction, TransactionalPublisher,
};
type PublishFut<'a> = Pin<Box<dyn Future<Output = Result<(), BoxError>> + Send + 'a>>;
#[derive(Debug, Clone)]
pub struct Outgoing<'a> {
name: Cow<'a, str>,
payload: BytesMut,
headers: Headers,
}
impl<'a> Outgoing<'a> {
#[must_use]
pub fn new(name: impl Into<Cow<'a, str>>, payload: impl Into<BytesMut>) -> Self {
Self {
name: name.into(),
payload: payload.into(),
headers: Headers::new(),
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: impl Into<Cow<'a, str>>) {
self.name = name.into();
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn payload_mut(&mut self) -> &mut BytesMut {
&mut self.payload
}
pub fn set_payload(&mut self, payload: impl Into<BytesMut>) {
self.payload = payload.into();
}
#[must_use]
pub fn headers(&self) -> &Headers {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
}
pub trait PublishPipeline: Send + Sync {
fn run<'a, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
send: &'a P,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'a;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PublishIdentity;
impl PublishPipeline for PublishIdentity {
async fn run<'a, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
send: &'a P,
) -> Result<(), BoxError> {
let msg =
OutgoingMessage::new(out.name(), out.payload()).with_headers(out.headers().clone());
send.publish(msg).await.map_err(|e| Box::new(e) as BoxError)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PublishStack<Head, Tail> {
head: Head,
tail: Tail,
}
impl<Head, Tail> PublishStack<Head, Tail> {
pub(crate) const fn new(head: Head, tail: Tail) -> Self {
Self { head, tail }
}
}
impl<Head: PublishLayer, Tail: PublishPipeline> PublishPipeline for PublishStack<Head, Tail> {
fn run<'a, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
send: &'a P,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'a {
self.head.on_publish(
out,
PublishNext {
tail: &self.tail,
send,
},
)
}
}
pub trait PublishLayer: Send + Sync {
fn on_publish<'a, N: PublishPipeline, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
next: PublishNext<'a, N, P>,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'a;
}
pub struct PublishNext<'a, N, P> {
tail: &'a N,
send: &'a P,
}
impl<'a, N: PublishPipeline, P: Publisher> PublishNext<'a, N, P> {
pub fn run(
self,
out: &'a mut Outgoing<'a>,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'a {
self.tail.run(out, self.send)
}
}
impl<N, P> fmt::Debug for PublishNext<'_, N, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PublishNext").finish_non_exhaustive()
}
}
pub trait PublishDynLayer: Send + Sync {
fn on_publish<'a>(
&'a self,
out: &'a mut Outgoing<'a>,
next: PublishDynNext<'a>,
) -> PublishFut<'a>;
}
pub struct PublishDynNext<'a> {
rest: &'a [Arc<dyn PublishDynLayer>],
tail: Box<dyn FnOnce(&'a mut Outgoing<'a>) -> PublishFut<'a> + Send + 'a>,
}
impl<'a> PublishDynNext<'a> {
#[must_use]
pub fn run(self, out: &'a mut Outgoing<'a>) -> PublishFut<'a> {
match self.rest.split_first() {
Some((middleware, rest)) => middleware.on_publish(
out,
PublishDynNext {
rest,
tail: self.tail,
},
),
None => (self.tail)(out),
}
}
}
impl fmt::Debug for PublishDynNext<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PublishDynNext")
.field("remaining", &self.rest.len())
.finish_non_exhaustive()
}
}
pub struct PublishDynStack(Arc<[Arc<dyn PublishDynLayer>]>);
impl Clone for PublishDynStack {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl PublishDynStack {
#[must_use]
pub fn new(middleware: impl IntoIterator<Item = Arc<dyn PublishDynLayer>>) -> Self {
Self(middleware.into_iter().collect())
}
}
impl fmt::Debug for PublishDynStack {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PublishDynStack")
.field("middleware", &self.0.len())
.finish_non_exhaustive()
}
}
impl PublishLayer for PublishDynStack {
fn on_publish<'a, N: PublishPipeline, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
next: PublishNext<'a, N, P>,
) -> impl Future<Output = Result<(), BoxError>> + Send + 'a {
PublishDynNext {
rest: &self.0,
tail: Box::new(move |out| Box::pin(next.run(out)) as PublishFut<'a>),
}
.run(out)
}
}
pub struct PublishContext<'a, C = ()> {
name: &'a str,
headers: &'a Headers,
cx: &'a C,
}
impl<'a, C> PublishContext<'a, C> {
pub(crate) fn new(name: &'a str, headers: &'a Headers, cx: &'a C) -> Self {
Self { name, headers, cx }
}
#[must_use]
pub fn name(&self) -> &str {
self.name
}
#[must_use]
pub fn headers(&self) -> &Headers {
self.headers
}
pub fn context<K: crate::Field<C>>(&self, key: K) -> K::Value<'_> {
key.get(self.cx)
}
}
impl<C> fmt::Debug for PublishContext<'_, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PublishContext")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
pub trait PublishTransform<C = ()>: Send + Sync {
fn apply(&self, out: &mut Outgoing<'_>, cx: &PublishContext<'_, C>);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PublishTransformIdentity;
impl<C> PublishTransform<C> for PublishTransformIdentity {
fn apply(&self, _out: &mut Outgoing<'_>, _cx: &PublishContext<'_, C>) {}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PublishTransformStack<Inner, Outer> {
inner: Inner,
outer: Outer,
}
impl<C, Inner: PublishTransform<C>, Outer: PublishTransform<C>> PublishTransform<C>
for PublishTransformStack<Inner, Outer>
{
fn apply(&self, out: &mut Outgoing<'_>, cx: &PublishContext<'_, C>) {
self.inner.apply(out, cx);
self.outer.apply(out, cx);
}
}
pub trait BatchPublishTransform<C = ()>: Send + Sync {
fn apply(&self, out: &mut Outgoing<'_>, cx: &PublishContext<'_, C>);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BatchTransformIdentity;
impl<C> BatchPublishTransform<C> for BatchTransformIdentity {
fn apply(&self, _out: &mut Outgoing<'_>, _cx: &PublishContext<'_, C>) {}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BatchPublishTransformStack<Inner, Outer> {
inner: Inner,
outer: Outer,
}
impl<C, Inner: BatchPublishTransform<C>, Outer: BatchPublishTransform<C>> BatchPublishTransform<C>
for BatchPublishTransformStack<Inner, Outer>
{
fn apply(&self, out: &mut Outgoing<'_>, cx: &PublishContext<'_, C>) {
self.inner.apply(out, cx);
self.outer.apply(out, cx);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ForBatch<L>(L);
impl<C, L: PublishTransform<C>> BatchPublishTransform<C> for ForBatch<L> {
fn apply(&self, out: &mut Outgoing<'_>, cx: &PublishContext<'_, C>) {
self.0.apply(out, cx);
}
}
#[must_use]
pub fn for_batch<L>(transform: L) -> ForBatch<L> {
ForBatch(transform)
}
pub struct TypedPublisher<P, C, PL = PublishTransformIdentity, BL = BatchTransformIdentity> {
publisher: P,
codec: C,
layers: PL,
batch_layers: BL,
}
impl<P, C> TypedPublisher<P, C, PublishTransformIdentity, BatchTransformIdentity> {
#[must_use]
pub fn with_codec(publisher: P, codec: C) -> Self {
Self {
publisher,
codec,
layers: PublishTransformIdentity,
batch_layers: BatchTransformIdentity,
}
}
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
impl<P> TypedPublisher<P, DefaultCodec, PublishTransformIdentity, BatchTransformIdentity> {
#[must_use]
pub fn new(publisher: P) -> Self {
Self::with_codec(publisher, DefaultCodec::default())
}
}
impl<P, C, PL, BL> TypedPublisher<P, C, PL, BL> {
pub(crate) const fn codec(&self) -> &C {
&self.codec
}
#[must_use]
pub fn transform<N>(
self,
transform: N,
) -> TypedPublisher<P, C, PublishTransformStack<PL, N>, BL> {
TypedPublisher {
publisher: self.publisher,
codec: self.codec,
layers: PublishTransformStack {
inner: self.layers,
outer: transform,
},
batch_layers: self.batch_layers,
}
}
#[must_use]
pub fn batch_transform<N>(
self,
transform: N,
) -> TypedPublisher<P, C, PL, BatchPublishTransformStack<BL, N>> {
TypedPublisher {
publisher: self.publisher,
codec: self.codec,
layers: self.layers,
batch_layers: BatchPublishTransformStack {
inner: self.batch_layers,
outer: transform,
},
}
}
#[must_use]
pub fn transactional(self) -> Transactional<P, C, PL, BL> {
Transactional { inner: self }
}
}
impl<P: Publisher, C: Codec, PL, BL> TypedPublisher<P, C, PL, BL> {
pub(crate) async fn publish<T: Serialize + Sync, Cx, PP>(
&self,
name: &str,
value: &T,
pipeline: &PP,
cx: &PublishContext<'_, Cx>,
) -> Result<(), BoxError>
where
PL: PublishTransform<Cx>,
BL: Sync,
Cx: Sync,
PP: PublishPipeline,
{
let payload = self
.codec
.encode(value)
.map_err(|e| Box::new(e) as BoxError)?;
let mut out = Outgoing::new(name, payload);
self.layers.apply(&mut out, cx);
pipeline.run(&mut out, &self.publisher).await
}
pub(crate) async fn publish_batched<T: Serialize + Sync, Cx, PP>(
&self,
name: &str,
value: &T,
pipeline: &PP,
cx: &PublishContext<'_, Cx>,
) -> Result<(), BoxError>
where
PL: Sync,
BL: BatchPublishTransform<Cx>,
Cx: Sync,
PP: PublishPipeline,
{
let payload = self
.codec
.encode(value)
.map_err(|e| Box::new(e) as BoxError)?;
let mut out = Outgoing::new(name, payload);
self.batch_layers.apply(&mut out, cx);
pipeline.run(&mut out, &self.publisher).await
}
}
impl<P, C, PL, BL> fmt::Debug for TypedPublisher<P, C, PL, BL> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypedPublisher").finish_non_exhaustive()
}
}
impl<CB, P, C, PL, BL> PublishPolicy<CB> for TypedPublisher<P, C, PL, BL>
where
CB: ConnectedBroker,
P: PublishPolicy<CB> + Send,
C: Send,
PL: Send,
BL: Send,
{
type Live = TypedPublisher<P::Live, C, PL, BL>;
async fn pair(self, connected: &CB) -> Result<Self::Live, PairError> {
Ok(TypedPublisher {
publisher: self.publisher.pair(connected).await?,
codec: self.codec,
layers: self.layers,
batch_layers: self.batch_layers,
})
}
}
pub struct Transactional<P, C, PL = PublishTransformIdentity, BL = BatchTransformIdentity> {
inner: TypedPublisher<P, C, PL, BL>,
}
impl<P, C, PL, BL> fmt::Debug for Transactional<P, C, PL, BL> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Transactional").finish_non_exhaustive()
}
}
impl<CB, P, C, PL, BL> PublishPolicy<CB> for Transactional<P, C, PL, BL>
where
CB: ConnectedBroker,
P: PublishPolicy<CB> + Send,
P::Live: TransactionalPublisher,
C: Send,
PL: Send,
BL: Send,
{
type Live = Transactional<P::Live, C, PL, BL>;
async fn pair(self, connected: &CB) -> Result<Self::Live, PairError> {
Ok(Transactional {
inner: self.inner.pair(connected).await?,
})
}
}
mod sealed {
pub trait Sealed {}
impl<P, C, PL, BL> Sealed for super::TypedPublisher<P, C, PL, BL> {}
impl<P, C, PL, BL> Sealed for super::Transactional<P, C, PL, BL> {}
}
pub trait ReplyWiring: Sealed {
type Codec: Codec + Clone;
#[doc(hidden)]
fn decode_codec(&self) -> &Self::Codec;
}
impl<P, C: Codec + Clone, PL, BL> ReplyWiring for TypedPublisher<P, C, PL, BL> {
type Codec = C;
fn decode_codec(&self) -> &C {
self.codec()
}
}
impl<P, C: Codec + Clone, PL, BL> ReplyWiring for Transactional<P, C, PL, BL> {
type Codec = C;
fn decode_codec(&self) -> &C {
self.inner.codec()
}
}
pub trait ReplyPublisher<Cx = ()>: Sealed + Send + Sync {
type Codec: Codec;
#[doc(hidden)]
fn reply_codec(&self) -> &Self::Codec;
#[doc(hidden)]
fn publish_batch<'a, T, PP>(
&'a self,
name: &'a str,
replies: &'a [T],
pipeline: &'a PP,
cx: &'a PublishContext<'a, Cx>,
) -> impl Future<Output = Result<(), BoxError>> + Send
where
T: Serialize + Sync,
PP: PublishPipeline;
}
impl<P, C, PL, BL, Cx> ReplyPublisher<Cx> for TypedPublisher<P, C, PL, BL>
where
P: Publisher,
C: Codec,
PL: Send + Sync,
BL: BatchPublishTransform<Cx>,
Cx: Sync,
{
type Codec = C;
fn reply_codec(&self) -> &C {
self.codec()
}
async fn publish_batch<'a, T, PP>(
&'a self,
name: &'a str,
replies: &'a [T],
pipeline: &'a PP,
cx: &'a PublishContext<'a, Cx>,
) -> Result<(), BoxError>
where
T: Serialize + Sync,
PP: PublishPipeline,
{
for reply in replies {
self.publish_batched(name, reply, pipeline, cx).await?;
}
Ok(())
}
}
impl<P, C, PL, BL, Cx> ReplyPublisher<Cx> for Transactional<P, C, PL, BL>
where
P: TransactionalPublisher,
C: Codec,
PL: Send + Sync,
BL: BatchPublishTransform<Cx>,
Cx: Sync,
{
type Codec = C;
fn reply_codec(&self) -> &C {
self.inner.codec()
}
async fn publish_batch<'a, T, PP>(
&'a self,
name: &'a str,
replies: &'a [T],
pipeline: &'a PP,
cx: &'a PublishContext<'a, Cx>,
) -> Result<(), BoxError>
where
T: Serialize + Sync,
PP: PublishPipeline,
{
let publisher = &self.inner.publisher;
publisher
.begin_transaction()
.await
.map_err(|e| Box::new(e) as BoxError)?;
for reply in replies {
if let Err(err) = self.inner.publish_batched(name, reply, pipeline, cx).await {
abort_quietly(publisher).await;
return Err(err);
}
}
publisher
.commit()
.await
.map_err(|err| Box::new(err) as BoxError)
}
}
async fn abort_quietly<P: TransactionalPublisher>(publisher: &P) {
if let Err(err) = publisher.abort().await {
warn!(target: "ruststream::dispatch", error = %err, "transaction abort failed");
}
}
impl<P, C, PL, BL> Transactional<P, C, PL, BL>
where
P: TransactionalPublisher,
C: Sync,
PL: Sync,
BL: Sync,
{
pub async fn begin(&self) -> Result<TransactionScope<'_, P, C>, P::Error> {
self.inner.publisher.begin_transaction().await?;
Ok(TransactionScope {
publisher: &self.inner.publisher,
codec: &self.inner.codec,
open: true,
})
}
}
#[must_use = "a transaction scope must be settled with commit() or abort()"]
pub struct TransactionScope<'a, P, C> {
publisher: &'a P,
codec: &'a C,
open: bool,
}
impl<P, C> TransactionScope<'_, P, C>
where
P: TransactionalPublisher,
C: Codec,
{
pub async fn publish<T: Serialize + Sync>(
&mut self,
name: &str,
value: &T,
) -> Result<(), TransactionPublishError<P::Error>> {
let payload = self
.codec
.encode(value)
.map_err(TransactionPublishError::Encode)?;
self.publisher
.publish(OutgoingMessage::new(name, &payload))
.await
.map_err(TransactionPublishError::Publish)
}
pub async fn commit(mut self) -> Result<(), P::Error> {
let result = self.publisher.commit().await;
self.open = false;
result
}
pub async fn abort(mut self) -> Result<(), P::Error> {
let result = self.publisher.abort().await;
self.open = false;
result
}
}
impl<P, C> Drop for TransactionScope<'_, P, C> {
fn drop(&mut self) {
if self.open {
warn!(
target: "ruststream::dispatch",
"transaction scope dropped without commit or abort; the broker transaction stays \
open on this publisher handle until it is settled or the handle is dropped"
);
}
}
}
impl<P, C> fmt::Debug for TransactionScope<'_, P, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TransactionScope")
.field("open", &self.open)
.finish_non_exhaustive()
}
}
impl<P, C, PL, BL> TypedPublisher<P, C, PL, BL>
where
P: OwnedTransactions,
C: Sync,
PL: Sync,
BL: Sync,
{
pub async fn transaction(&self) -> Result<TypedTransaction<'_, P::Transaction, C>, P::Error> {
Ok(TypedTransaction {
txn: self.publisher.transaction().await?,
codec: &self.codec,
})
}
}
#[must_use = "a transaction does nothing until settled with commit() or abort()"]
pub struct TypedTransaction<'a, Txn, C> {
txn: Txn,
codec: &'a C,
}
impl<Txn, C> TypedTransaction<'_, Txn, C>
where
Txn: Transaction,
C: Codec,
{
pub async fn publish<T>(
&mut self,
name: &str,
value: &T,
) -> Result<(), TransactionPublishError<Txn::Error>>
where
T: Serialize + Sync,
{
let payload = self
.codec
.encode(value)
.map_err(TransactionPublishError::Encode)?;
self.txn
.publish(OutgoingMessage::new(name, &payload))
.await
.map_err(TransactionPublishError::Publish)
}
pub async fn commit(self) -> Result<(), Txn::Error> {
self.txn.commit().await
}
pub async fn abort(self) -> Result<(), Txn::Error> {
self.txn.abort().await
}
}
impl<Txn, C> fmt::Debug for TypedTransaction<'_, Txn, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TypedTransaction").finish_non_exhaustive()
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TransactionPublishError<E> {
#[error("failed to encode the value for a transactional publish")]
Encode(#[source] CodecError),
#[error("failed to publish inside the transaction")]
Publish(#[source] E),
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "json", feature = "logging"))]
#[tokio::test]
async fn cancelled_commit_keeps_the_unsettled_drop_warning() {
use std::sync::{Arc, Mutex};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::{Context as LayerContext, SubscriberExt as _};
use crate::{OutgoingMessage, Publisher, TransactionalPublisher};
struct PendingCommit;
impl Publisher for PendingCommit {
type Error = std::convert::Infallible;
async fn publish(&self, _msg: OutgoingMessage<'_>) -> Result<(), Self::Error> {
Ok(())
}
}
impl TransactionalPublisher for PendingCommit {
async fn begin_transaction(&self) -> Result<(), Self::Error> {
Ok(())
}
async fn commit(&self) -> Result<(), Self::Error> {
std::future::pending().await
}
async fn abort(&self) -> Result<(), Self::Error> {
Ok(())
}
}
struct Capture(Arc<Mutex<Vec<String>>>);
impl<S: tracing::Subscriber> Layer<S> for Capture {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: LayerContext<'_, S>) {
struct Grab(Option<String>);
impl tracing::field::Visit for Grab {
fn record_debug(
&mut self,
field: &tracing::field::Field,
value: &dyn fmt::Debug,
) {
if field.name() == "message" {
self.0 = Some(format!("{value:?}"));
}
}
}
let mut grab = Grab(None);
event.record(&mut grab);
if let Some(message) = grab.0 {
self.0.lock().unwrap().push(message);
}
}
}
let events = Arc::new(Mutex::new(Vec::new()));
let guard = tracing::subscriber::set_default(
tracing_subscriber::registry().with(Capture(Arc::clone(&events))),
);
let wrapper = TypedPublisher::new(PendingCommit).transactional();
let scope = wrapper.begin().await.expect("begin failed");
{
let mut commit = std::pin::pin!(scope.commit());
assert!(
futures::poll!(commit.as_mut()).is_pending(),
"the stub commit must hold the cancellation window open",
);
}
drop(guard);
let warned = {
let captured = events.lock().unwrap();
captured
.iter()
.any(|message| message.contains("transaction scope dropped without commit"))
};
assert!(
warned,
"a commit cancelled mid-flight leaves the broker transaction unsettled and must \
keep the drop warning",
);
}
#[cfg(all(feature = "memory", feature = "json"))]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dyn_stack_walks_its_layers_then_the_static_tail() {
use std::sync::atomic::{AtomicUsize, Ordering};
use futures::StreamExt;
use crate::codec::JsonCodec;
use crate::memory::MemoryBroker;
use crate::{IncomingMessage, Subscriber};
struct Mark(Arc<AtomicUsize>, usize);
impl PublishDynLayer for Mark {
fn on_publish<'a>(
&'a self,
out: &'a mut Outgoing<'a>,
next: PublishDynNext<'a>,
) -> PublishFut<'a> {
self.0.fetch_add(self.1, Ordering::SeqCst);
next.run(out)
}
}
let hits = Arc::new(AtomicUsize::new(0));
let stack = PublishDynStack::new([
Arc::new(Mark(Arc::clone(&hits), 1)) as Arc<dyn PublishDynLayer>,
Arc::new(Mark(Arc::clone(&hits), 10)),
]);
assert!(format!("{stack:?}").contains("middleware"));
let pipeline = PublishStack::new(stack.clone(), PublishIdentity);
let broker = MemoryBroker::new();
let mut subscriber = broker.subscribe("dyn");
let publisher = TypedPublisher::with_codec(broker.publisher(), JsonCodec);
let headers = Headers::new();
let cx = PublishContext::new("dyn", &headers, &());
publisher
.publish("dyn", &5_u32, &pipeline, &cx)
.await
.expect("publish through the dynamic stack failed");
assert_eq!(hits.load(Ordering::SeqCst), 11, "each layer must run once");
let mut stream = std::pin::pin!(subscriber.stream());
let msg = stream
.next()
.await
.expect("delivery missing")
.expect("memory subscriber never errors");
assert_eq!(msg.payload(), b"5", "the static tail must still send");
msg.ack().await.expect("ack failed");
}
#[test]
fn borrowed_name_is_not_owned() {
let out = Outgoing::new("orders.created", b"payload".as_slice());
assert!(matches!(out.name, Cow::Borrowed(_)));
assert_eq!(out.name(), "orders.created");
assert_eq!(out.payload(), b"payload");
}
#[test]
fn owned_name_moves_in() {
let computed = format!("orders.{}", 42);
let out = Outgoing::new(computed, BytesMut::from(&b"x"[..]));
assert!(matches!(out.name, Cow::Owned(_)));
assert_eq!(out.name(), "orders.42");
}
#[test]
fn payload_mutates_in_place() {
let mut out = Outgoing::new("t", BytesMut::from(&b"body"[..]));
out.payload_mut().extend_from_slice(b"!");
assert_eq!(out.payload(), b"body!");
out.set_payload(b"fresh".as_slice());
assert_eq!(out.payload(), b"fresh");
}
#[test]
fn set_name_and_headers() {
let mut out = Outgoing::new("a", b"".as_slice());
out.set_name("b");
out.headers_mut().insert("k", "v");
assert_eq!(out.name(), "b");
assert_eq!(out.headers().get_str("k"), Some("v"));
}
}