#![cfg(all(
feature = "macros",
feature = "memory",
feature = "json",
feature = "testing"
))]
mod common;
use std::future::{Future, ready};
use std::num::NonZeroUsize;
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::TestApp;
use ruststream::{
AddressedCopies, BatchSubscriber, BufferedSubscriber, RedeliveryAddress, RedeliveryAddressed,
Seekable, Subscribe, Subscriber, SubscriptionSource,
};
use serde::{Deserialize, Serialize};
struct TrickleSubscriber(BufferedSubscriber<MemorySubscriber<Retaining>>);
impl TrickleSubscriber {
fn new(inner: MemorySubscriber<Retaining>) -> Self {
Self(BufferedSubscriber::new(inner).max_wait(Duration::from_millis(5)))
}
}
impl Subscriber for TrickleSubscriber {
type Message = <MemorySubscriber<Retaining> as Subscriber>::Message;
type Error = <MemorySubscriber<Retaining> 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<Retaining> 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<Retaining> as Seekable>::Seeker;
fn seeker(&self) -> Self::Seeker {
self.0.seeker()
}
}
#[derive(Clone)]
struct Trickle {
name: &'static str,
}
impl SubscriptionSource<ConnectedMemoryBroker<Retaining>> for Trickle {
type Subscriber = TrickleSubscriber;
type Copies = AddressedCopies;
fn name(&self) -> &str {
self.name
}
async fn subscribe(
self,
connected: &ConnectedMemoryBroker<Retaining>,
) -> Result<TrickleSubscriber, MemoryError> {
Ok(TrickleSubscriber::new(
Subscribe::subscribe(connected, self.name).await?,
))
}
}
impl RedeliveryAddressed<ConnectedMemoryBroker<Retaining>> for Trickle {
fn redelivery_address(
&self,
_connected: &ConnectedMemoryBroker<Retaining>,
) -> impl Future<Output = Result<RedeliveryAddress, MemoryError>> + Send {
ready(Ok(RedeliveryAddress::new(self.name)))
}
}
#[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::retaining(Retention::Messages(nonzero!(32)));
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<Retaining>>()
.subscriber("trickle")
.assert_batch_sizes(&[2, 1])
.settled(HandlerOutcome::ack());
tb.shutdown().await.expect("shutdown failed");
}
struct Attempts {
retried_once: AtomicBool,
}
#[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("batches")]
async fn reconcile(orders: &[Order], ctx: &mut Context<'_, (), Attempts>) -> Vec<HandlerOutcome> {
let retried_once = &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 broker = MemoryBroker::retaining(Retention::Messages(nonzero!(32)));
let publisher = broker.publisher();
for id in [10u32, 11, 12] {
publisher
.message(&Order { id })
.to("batches")
.publish()
.await
.expect("publish failed");
}
let app = RustStream::new(AppInfo::new("batches", "0.1.0"))
.on_startup(async move |()| {
Ok::<_, std::convert::Infallible>(Attempts {
retried_once: AtomicBool::new(false),
})
})
.with_broker(broker, |b| {
b.include(
reconcile
.batch(nonzero!(8))
.start_at(MemoryPosition::start()),
);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.settle()
.await
.expect("the replayed batch and its redelivery settle");
let subscriber = tb.broker::<MemoryBroker<Retaining>>();
let subscriber = subscriber.subscriber("batches");
let seen: Vec<Vec<u32>> = subscriber
.batches::<Order>()
.iter()
.map(|batch| batch.iter().map(|o| o.id).collect())
.collect();
assert_eq!(seen, vec![vec![10, 11, 12], vec![11]]);
subscriber.settled(HandlerOutcome::ack());
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
struct Confirmation {
id: u32,
accepted: bool,
}
#[subscriber("requests", reply("audit"))]
async fn audit(orders: &[Order]) -> Vec<Confirmation> {
orders
.iter()
.map(|o| Confirmation {
id: o.id,
accepted: true,
})
.collect()
}
#[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")),
);
}
#[subscriber("orders")]
async fn bill(orders: &[Order]) -> HandlerOutcome {
let _ = orders;
HandlerOutcome::ack()
}
#[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", reply("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],
);
}