#![cfg(all(
feature = "macros",
feature = "memory",
feature = "json",
feature = "testing"
))]
mod common;
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use common::{Order, Wire};
use futures::Stream;
use ruststream::memory::prelude::*;
use ruststream::memory::{ConnectedMemoryBroker, MemorySubscriber};
use ruststream::testing::{Outcome, TestApp};
use ruststream::{
BatchSubscriber, Buffered, BufferedSubscriber, Seekable, Subscribe, Subscriber,
SubscriptionSource,
};
use serde::{Deserialize, Serialize};
#[subscriber("orders")]
async fn bill(orders: &[Order]) -> HandlerOutcome {
let _ = orders;
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_macro_def_receives_batches() {
let app = RustStream::new(AppInfo::new("billing", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include(bill.batch(nonzero!(64))));
let tb = TestApp::start(app).await.expect("startup failed");
for id in 0..3u32 {
tb.message(&Order { id })
.to("orders")
.publish()
.await
.expect("publish failed");
}
let batches: Vec<Vec<Order>> = tb.broker::<MemoryBroker>().subscriber("orders").batches();
let flattened: Vec<u32> = batches.iter().flatten().map(|o| o.id).collect();
assert_eq!(flattened, vec![0, 1, 2], "deliveries out of publish order");
assert!(
batches.iter().all(|batch| !batch.is_empty()),
"batches must not be empty",
);
}
#[subscriber("mixed")]
async fn sift(orders: &[Order]) -> HandlerOutcome {
let _ = orders;
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn undecodable_elements_never_reach_the_handler() {
let app = RustStream::new(AppInfo::new("billing", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include(sift.batch(nonzero!(64))));
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 1 })
.to("mixed")
.publish()
.await
.expect("publish failed");
tb.message(&Wire::of(b"not json"))
.to("mixed")
.publish()
.await
.expect("publish failed");
tb.message(&Order { id: 2 })
.to("mixed")
.publish()
.await
.expect("publish failed");
let received: Vec<Order> = tb.broker::<MemoryBroker>().subscriber("mixed").received();
let ids: Vec<u32> = received.iter().map(|o| o.id).collect();
assert_eq!(ids, vec![1, 2], "unexpected ids reached the handler");
tb.broker::<MemoryBroker>()
.subscriber("mixed")
.assert_called(2)
.settled(HandlerOutcome::ack());
}
#[subscriber(Buffered::<Name>::new(Name::new("events")))]
async fn drain(events: &[Order]) -> HandlerOutcome {
let _ = events;
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn buffered_adapter_batches_plain_subscribers_via_router() {
let router = Router::<MemoryBroker>::new().include(drain.batch(nonzero!(2)));
let app = RustStream::new(AppInfo::new("events", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include_router(router));
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 7 })
.to("events")
.publish()
.await
.expect("publish failed");
tb.broker::<MemoryBroker>()
.subscriber("events")
.assert_called_once()
.with(&Order { id: 7 })
.settled(HandlerOutcome::ack());
}
struct TrickleSubscriber(BufferedSubscriber<MemorySubscriber>);
impl TrickleSubscriber {
fn new(inner: MemorySubscriber) -> Self {
Self(BufferedSubscriber::new(inner).max_wait(Duration::from_millis(5)))
}
}
impl Subscriber for TrickleSubscriber {
type Message = <MemorySubscriber as Subscriber>::Message;
type Error = <MemorySubscriber as Subscriber>::Error;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
self.0.stream()
}
}
impl BatchSubscriber for TrickleSubscriber {
type Batch = Vec<<MemorySubscriber as Subscriber>::Message>;
fn batches(
&mut self,
size: NonZeroUsize,
) -> impl Stream<Item = Result<Self::Batch, Self::Error>> + Send + '_ {
self.0.batches(size)
}
}
impl Seekable for TrickleSubscriber {
type Seeker = <MemorySubscriber as Seekable>::Seeker;
fn seeker(&self) -> Self::Seeker {
self.0.seeker()
}
}
#[derive(Clone)]
struct Trickle {
name: &'static str,
}
impl SubscriptionSource<ConnectedMemoryBroker> for Trickle {
type Subscriber = TrickleSubscriber;
fn name(&self) -> &str {
self.name
}
async fn subscribe(
self,
connected: &ConnectedMemoryBroker,
) -> Result<TrickleSubscriber, MemoryError> {
Ok(TrickleSubscriber::new(
Subscribe::subscribe(connected, self.name).await?,
))
}
}
#[subscriber(Trickle { name: "trickle" })]
async fn sip(orders: &[Order]) -> HandlerOutcome {
let _ = orders.len();
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_delegating_broker_honours_the_batch_size() {
let broker = MemoryBroker::new();
let publisher = broker.publisher();
for id in 0..3u32 {
publisher
.message(&Order { id })
.to("trickle")
.publish()
.await
.expect("publish failed");
}
let app = RustStream::new(AppInfo::new("trickle", "0.1.0")).with_broker(broker, |b| {
b.include(sip.batch(nonzero!(2)).start_at(MemoryPosition::start()));
});
let tb = TestApp::start(app).await.expect("harness start");
tb.settle().await.expect("the replayed batches settle");
tb.broker::<MemoryBroker>()
.subscriber("trickle")
.assert_batch_sizes(&[2, 1])
.settled(HandlerOutcome::ack());
tb.shutdown().await.expect("shutdown failed");
}
struct Attempts {
retried_once: Arc<AtomicBool>,
}
#[subscriber("batches")]
async fn reconcile(orders: &[Order], ctx: &mut Context<'_, (), Attempts>) -> Vec<HandlerOutcome> {
let retried_once = Arc::clone(&ctx.state().retried_once);
orders
.iter()
.map(|o| {
if o.id == 11 && !retried_once.swap(true, Ordering::SeqCst) {
HandlerOutcome::retry()
} else {
HandlerOutcome::ack()
}
})
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn per_element_outcomes_retry_individually() {
let retried_once = Arc::new(AtomicBool::new(false));
let state_flag = Arc::clone(&retried_once);
let app = RustStream::new(AppInfo::new("batches", "0.1.0"))
.on_startup(move |()| {
let retried_once = state_flag;
async move { Ok::<_, std::convert::Infallible>(Attempts { retried_once }) }
})
.with_broker(MemoryBroker::new(), |b| {
b.include(reconcile.batch(nonzero!(64)));
});
let tb = TestApp::start(app).await.expect("startup failed");
for id in [10u32, 11, 12] {
tb.message(&Order { id })
.to("batches")
.publish()
.await
.expect("publish failed");
}
assert!(retried_once.load(Ordering::SeqCst));
let batches: Vec<Vec<Order>> = tb.broker::<MemoryBroker>().subscriber("batches").batches();
let seen: Vec<Vec<u32>> = batches
.iter()
.map(|batch| batch.iter().map(|o| o.id).collect())
.collect();
assert_eq!(seen, vec![vec![10], vec![11], vec![11], vec![12]]);
assert_eq!(
tb.broker::<MemoryBroker>().subscriber("batches").outcomes(),
[Outcome::Ack, Outcome::Nack, Outcome::Ack, Outcome::Ack],
);
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
struct Confirmation {
id: u32,
accepted: bool,
}
#[subscriber("requests", publish("confirmations"))]
async fn confirm(orders: &[Order]) -> Result<Vec<Confirmation>, HandlerOutcome> {
Ok(orders
.iter()
.map(|o| Confirmation {
id: o.id,
accepted: true,
})
.collect())
}
#[subscriber("requests", publish("audit"))]
async fn audit(orders: &[Order]) -> Vec<Confirmation> {
orders
.iter()
.map(|o| Confirmation {
id: o.id,
accepted: true,
})
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_replies_publish_transactionally() {
let app = RustStream::new(AppInfo::new("confirmations", "0.1.0")).with_broker(
MemoryBroker::new(),
|b| {
b.include(confirm.batch(nonzero!(64)))
.out(Reply, TransactionalPublish)
.transactional();
},
);
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 7 })
.to("requests")
.publish()
.await
.expect("publish failed");
tb.broker::<MemoryBroker>()
.published::<Confirmation>("confirmations")
.assert_called_once()
.with(&Confirmation {
id: 7,
accepted: true,
});
}
#[test]
fn batch_publishing_def_records_metadata() {
let broker = MemoryBroker::new();
let app = RustStream::new(AppInfo::new("audit", "0.1.0")).with_broker(broker, |b| {
b.include(audit.batch(nonzero!(64))).out(Reply, Publish);
});
assert_eq!(app.handlers().len(), 1);
assert_eq!(app.handlers()[0].name, "requests");
assert!(
app.handlers()[0]
.output_type
.is_some_and(|t| t.contains("Confirmation")),
);
}
#[test]
fn batch_def_records_metadata() {
let broker = MemoryBroker::new();
let app = RustStream::new(AppInfo::new("billing", "0.1.0"))
.with_broker(broker, |b| b.include(bill.batch(nonzero!(64))));
assert_eq!(app.handlers().len(), 1);
assert_eq!(app.handlers()[0].name, "orders");
assert_eq!(
app.handlers()[0].description.as_deref(),
Some("Settles a whole batch of orders at once."),
);
}
#[derive(Clone, Copy)]
struct Tally {
multiplier: u32,
}
#[subscriber("scale", publish("scaled"))]
async fn scale(orders: &[Order], ctx: &mut Context<'_, (), Tally>) -> Vec<Order> {
let multiplier = ctx.state().multiplier;
orders
.iter()
.map(|o| Order {
id: o.id * multiplier,
})
.collect()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_handler_reads_typed_state() {
let app = RustStream::new(AppInfo::new("billing", "0.1.0"))
.on_startup(async move |()| Ok::<_, std::convert::Infallible>(Tally { multiplier: 10 }))
.with_broker(MemoryBroker::new(), |b| {
b.include(scale.batch(nonzero!(64))).out(Reply, Publish);
});
let tb = TestApp::start(app).await.expect("startup failed");
for id in 1..4u32 {
tb.message(&Order { id })
.to("scale")
.publish()
.await
.expect("publish failed");
}
let scaled: Vec<Order> = tb
.broker::<MemoryBroker>()
.published::<Order>("scaled")
.decoded();
assert_eq!(
scaled.iter().map(|o| o.id).collect::<Vec<_>>(),
vec![10, 20, 30],
);
}