#![cfg(all(
feature = "memory",
feature = "json",
feature = "macros",
feature = "testing"
))]
mod common;
use common::{Order, Receipt};
use std::convert::Infallible;
use std::future::{Future, ready};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use futures::{Stream, StreamExt};
use ruststream::memory::{MemoryBroker, MemoryMessage, MemoryPublish, MemorySubscriber};
use ruststream::runtime::{
AppInfo, Context, ForReply, Handle, HandlerOutcome, IntoSource, Outgoing, PublishContext,
PublishTransform, Reads, RustStream, subscriber,
};
use ruststream::testing::TestApp;
use ruststream::{
AckError, AddressedCopies, Deserialized, HeaderMap, IncomingMessage, RedeliveryAddress,
RedeliveryAddressed, Subscribe, Subscriber, SubscriptionSource,
};
struct CountingMessage {
inner: MemoryMessage,
reads: Arc<AtomicUsize>,
}
impl IncomingMessage for CountingMessage {
fn payload(&self) -> &[u8] {
self.inner.payload()
}
fn headers(&self) -> &HeaderMap {
self.reads.fetch_add(1, Ordering::Relaxed);
self.inner.headers()
}
async fn ack(self) -> Result<(), AckError> {
self.inner.ack().await
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
self.inner.nack(requeue).await
}
}
struct CountingSubscriber {
inner: MemorySubscriber,
reads: Arc<AtomicUsize>,
}
impl Subscriber for CountingSubscriber {
type Message = CountingMessage;
type Error = Infallible;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
let reads = Arc::clone(&self.reads);
self.inner.stream().map(move |item| {
let reads = Arc::clone(&reads);
item.map(|inner| CountingMessage { inner, reads })
})
}
}
macro_rules! header_reads {
($handler:expr) => {{
let reads = Arc::new(AtomicUsize::new(0));
let counted = Counted {
name: "orders",
reads: Arc::clone(&reads),
};
let app =
RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(subscriber(counted, $handler).build());
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 7 })
.to("orders")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(1)
.settled(HandlerOutcome::ack());
reads.load(Ordering::Relaxed)
}};
}
struct ReadsNothing;
impl Handle<Order> for ReadsNothing {
fn handle(
&self,
_order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
ready(Ok(()))
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_decoded_handler_never_asks_the_delivery_for_its_headers() {
let reads = header_reads!(ReadsNothing);
assert_eq!(
reads, 0,
"a handler over a decoded payload reads no headers, so none are materialized"
);
}
#[derive(Deserialized)]
struct Frame<'a>(&'a [u8]);
struct ReadsBytes;
impl<'p> Handle<Frame<'p>> for ReadsBytes {
fn handle(
&self,
frame: &Frame<'p>,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
let _ = frame.0;
ready(Ok(()))
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_byte_lane_handler_never_asks_the_delivery_for_its_headers() {
let reads = header_reads!(ReadsBytes);
assert_eq!(
reads, 0,
"the byte lane hands the payload over untouched and never reaches the header map"
);
}
struct ReadsTwice;
impl Handle<Order> for ReadsTwice {
fn handle(
&self,
_order: &Order,
_outs: &(),
ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
let reads = [ctx.headers().is_empty(), ctx.headers().is_empty()];
ready(if reads == [true, true] {
Ok(())
} else {
Err(HandlerOutcome::drop())
})
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_handler_that_reads_the_headers_materializes_them_once() {
let reads = header_reads!(ReadsTwice);
assert_eq!(
reads, 1,
"the delivery is asked once however often the handler reads the map"
);
}
struct Enriches;
impl Handle<Order> for Enriches {
fn handle(
&self,
_order: &Order,
_outs: &(),
ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
ctx.headers_mut().insert("x-seen", "yes");
ready(if ctx.headers().get("x-seen").is_some() {
Ok(())
} else {
Err(HandlerOutcome::drop())
})
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_handler_that_enriches_the_headers_asks_the_delivery_once() {
let reads = header_reads!(Enriches);
assert_eq!(
reads, 1,
"the working copy is cloned from the delivery's map, which is resolved once"
);
}
#[derive(Clone)]
struct Counted {
name: &'static str,
reads: Arc<AtomicUsize>,
}
impl<Connected> SubscriptionSource<Connected> for Counted
where
Connected: Subscribe<Subscriber = MemorySubscriber>,
{
type Subscriber = CountingSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(
self,
connected: &Connected,
) -> Result<CountingSubscriber, Connected::Error> {
Ok(CountingSubscriber {
inner: connected.subscribe(self.name).await?,
reads: self.reads,
})
}
}
impl<Connected> RedeliveryAddressed<Connected> for Counted
where
Connected: Subscribe<Subscriber = MemorySubscriber>,
{
fn redelivery_address(
&self,
_connected: &Connected,
) -> impl Future<Output = Result<RedeliveryAddress, Connected::Error>> + Send {
ready(Ok(RedeliveryAddress::new(self.name)))
}
}
impl IntoSource for Counted {
type Source = Self;
fn into_source(self) -> Self {
self
}
}
struct Confirm;
impl Handle<Order, Receipt> for Confirm {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<Receipt, HandlerOutcome>> {
ready(Ok(Receipt { id: order.id }))
}
}
struct CarryTenant;
impl<C, Options> PublishTransform<ForReply<C>, Options> for CarryTenant {
type Destination = Reads;
fn apply(
&self,
out: &mut Outgoing<'_>,
_options: &mut Option<Options>,
cx: &PublishContext<'_, C>,
) {
if let Some(tenant) = cx.headers().get_shared("x-tenant") {
out.headers_mut().insert("x-tenant", tenant);
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_reply_no_transform_reads_never_asks_the_delivery_for_its_headers() {
let reads = Arc::new(AtomicUsize::new(0));
let counted = Counted {
name: "orders",
reads: Arc::clone(&reads),
};
let app = RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(subscriber(counted, Confirm).reply().to("receipts").build())
.out_reply(MemoryPublish);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 7 })
.to("orders")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("receipts")
.assert_called_once()
.with(&Receipt { id: 7 });
assert_eq!(
reads.load(Ordering::Relaxed),
0,
"a reply whose transforms read no header leaves the delivery's map unasked"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_reply_transform_that_reads_the_headers_asks_the_delivery_once() {
let reads = Arc::new(AtomicUsize::new(0));
let counted = Counted {
name: "orders",
reads: Arc::clone(&reads),
};
let app = RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(subscriber(counted, Confirm).reply().to("receipts").build())
.out_reply(MemoryPublish)
.transform(CarryTenant);
});
let tb = TestApp::start(app).await.expect("startup failed");
let mut tenant = HeaderMap::new();
tenant.insert("x-tenant", "acme");
tb.message(&Order { id: 7 })
.to("orders")
.with_headers(tenant)
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.published::<Receipt>("receipts")
.assert_called_once()
.with_header("x-tenant", "acme");
assert_eq!(
reads.load(Ordering::Relaxed),
1,
"the transform reads the delivery's map, which is asked for once"
);
}