#![cfg(all(
feature = "macros",
feature = "memory",
feature = "json",
feature = "testing"
))]
mod common;
use common::{Order, Wire};
use ruststream::memory::prelude::*;
use ruststream::runtime::RustStreamError;
use ruststream::testing::{Outcome, TestApp};
use serde::{Deserialize, Serialize};
#[subscriber("boom")]
async fn boom(order: &Order) -> HandlerOutcome {
assert_eq!(order.id, u32::MAX, "handler exploded");
HandlerOutcome::ack()
}
#[subscriber("dropping", on_failure(panic = drop))]
async fn dropping(order: &Order) -> HandlerOutcome {
assert!(order.id != 0, "poison order must panic");
HandlerOutcome::ack()
}
#[subscriber("decodeff", on_failure(decode = fail_fast))]
async fn decode_ff(_order: &Order) -> HandlerOutcome {
HandlerOutcome::ack()
}
#[subscriber("skipping", on_failure(decode = skip))]
async fn skipping(_order: &Order) -> HandlerOutcome {
HandlerOutcome::ack()
}
#[subscriber("batchboom", on_failure(panic = fail_fast))]
async fn batch_boom(orders: &[Order]) -> HandlerOutcome {
assert!(orders.is_empty(), "batch handler exploded");
HandlerOutcome::ack()
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Outgoing)]
struct Acked(u32);
#[subscriber("rpcd", publish("rpcd.out"))]
async fn rpcd(order: &Order) -> Acked {
Acked(order.id)
}
#[subscriber("bd")]
async fn bd(orders: &[Order]) -> HandlerOutcome {
let _ = orders;
HandlerOutcome::ack()
}
#[subscriber("bpd", publish("bpd.out"))]
async fn bpd(orders: &[Order]) -> Vec<Acked> {
orders.iter().map(|o| Acked(o.id)).collect()
}
async fn drive_good_bad_good<S: Send + Sync + 'static>(tb: &TestApp<S>, topic: &str) {
tb.message(&Order { id: 1 })
.to(topic)
.publish()
.await
.expect("publish");
tb.message(&Wire::of(b"not json"))
.to(topic)
.publish()
.await
.expect("publish");
tb.message(&Order { id: 2 })
.to(topic)
.publish()
.await
.expect("publish");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn handler_panic_fails_fast_and_run_returns_err() {
let app =
RustStream::new(AppInfo::new("boom", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(boom);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 1 })
.to("boom")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("boom")
.assert_called_once()
.panicked();
tb.assert_shut_down();
let result = tb.shutdown().await;
assert!(
matches!(result, Err(RustStreamError::Dispatch(_))),
"a fail-fast panic must make run() return a dispatch error, got {result:?}",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn panic_drop_keeps_the_subscriber_consuming() {
let app =
RustStream::new(AppInfo::new("dropping", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(dropping);
});
let tb = TestApp::start(app).await.expect("startup failed");
for id in [7u32, 0, 9] {
tb.message(&Order { id })
.to("dropping")
.publish()
.await
.expect("publish");
}
assert_eq!(
tb.broker::<MemoryBroker>()
.subscriber("dropping")
.outcomes(),
[Outcome::Ack, Outcome::Panicked, Outcome::Ack],
);
tb.assert_running();
let result = tb.shutdown().await;
assert!(
result.is_ok(),
"a dropped panic must not error the run: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn decode_fail_fast_returns_err() {
let app =
RustStream::new(AppInfo::new("decodeff", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(decode_ff);
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Wire::of(b"not json"))
.to("decodeff")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("decodeff")
.assert_called_once()
.assert_last_failed_to_decode();
tb.assert_shut_down();
let result = tb.shutdown().await;
assert!(
matches!(result, Err(RustStreamError::Dispatch(_))),
"a fail-fast decode failure must make run() return a dispatch error, got {result:?}",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn decode_skip_acks_past_bad_input_and_continues() {
let app =
RustStream::new(AppInfo::new("skipping", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(skipping);
});
let tb = TestApp::start(app).await.expect("startup failed");
drive_good_bad_good(&tb, "skipping").await;
assert_eq!(
tb.broker::<MemoryBroker>()
.subscriber("skipping")
.outcomes(),
[Outcome::Ack, Outcome::DecodeFailed, Outcome::Ack],
);
tb.assert_running();
let result = tb.shutdown().await;
assert!(
result.is_ok(),
"a skipped decode failure must not error the run: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn publishing_decode_failure_is_dropped_and_continues() {
let router = Router::<MemoryBroker>::new()
.include(rpcd)
.out(Reply, Publish)
.build();
let app = RustStream::new(AppInfo::new("rpcd", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include_router(router));
let tb = TestApp::start(app).await.expect("startup failed");
drive_good_bad_good(&tb, "rpcd").await;
assert_eq!(
tb.broker::<MemoryBroker>().subscriber("rpcd").outcomes(),
[Outcome::Ack, Outcome::DecodeFailed, Outcome::Ack],
);
assert_eq!(
tb.broker::<MemoryBroker>()
.published::<Acked>("rpcd.out")
.decoded(),
vec![Acked(1), Acked(2)],
);
tb.assert_running();
let result = tb.shutdown().await;
assert!(
result.is_ok(),
"a dropped decode failure must not error the run: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_decode_failure_drops_the_bad_element() {
let app = RustStream::new(AppInfo::new("bd", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(bd.batch(nonzero!(64)));
});
let tb = TestApp::start(app).await.expect("startup failed");
drive_good_bad_good(&tb, "bd").await;
let seen: Vec<Order> = tb.broker::<MemoryBroker>().subscriber("bd").received();
assert_eq!(seen.iter().map(|o| o.id).collect::<Vec<_>>(), [1, 2]);
tb.broker::<MemoryBroker>()
.subscriber("bd")
.assert_called(2)
.settled(HandlerOutcome::ack());
tb.assert_running();
let result = tb.shutdown().await;
assert!(
result.is_ok(),
"a dropped batch element must not error the run: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_publishing_decode_failure_is_dropped() {
let router = Router::<MemoryBroker>::new()
.include(bpd.batch(nonzero!(64)))
.out(Reply, Publish)
.build();
let app = RustStream::new(AppInfo::new("bpd", "0.1.0"))
.with_broker(MemoryBroker::new(), |b| b.include_router(router));
let tb = TestApp::start(app).await.expect("startup failed");
drive_good_bad_good(&tb, "bpd").await;
let seen: Vec<Order> = tb.broker::<MemoryBroker>().subscriber("bpd").received();
assert_eq!(seen.iter().map(|o| o.id).collect::<Vec<_>>(), [1, 2]);
assert_eq!(
tb.broker::<MemoryBroker>()
.published::<Acked>("bpd.out")
.decoded(),
vec![Acked(1), Acked(2)],
);
tb.assert_running();
let result = tb.shutdown().await;
assert!(
result.is_ok(),
"a dropped batch reply element must not error the run: {result:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn batch_handler_panic_fails_fast() {
let app =
RustStream::new(AppInfo::new("batchboom", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(batch_boom.batch(nonzero!(64)));
});
let tb = TestApp::start(app).await.expect("startup failed");
tb.message(&Order { id: 1 })
.to("batchboom")
.publish()
.await
.expect("publish");
tb.broker::<MemoryBroker>()
.subscriber("batchboom")
.assert_called_once()
.panicked();
tb.assert_shut_down();
let result = tb.shutdown().await;
assert!(
matches!(result, Err(RustStreamError::Dispatch(_))),
"a fail-fast batch panic must make run() return a dispatch error, got {result:?}",
);
}