#![cfg(all(
feature = "testing",
feature = "memory",
feature = "json",
feature = "macros"
))]
use std::convert::Infallible;
use std::error::Error;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use ruststream::memory::{MemoryBroker, MemoryPublish, MemoryPublisher};
use ruststream::runtime::{AppInfo, HandlerOutcome, Identity, PublishExt, RustStream};
use ruststream::testing::{TestApp, TestError};
use ruststream::{Broker, Outgoing, subscriber};
use serde::{Deserialize, Serialize};
const RETRY_DELAY: Duration = Duration::from_millis(200);
#[derive(Debug, Outgoing, Serialize, Deserialize, PartialEq)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
}
#[derive(Debug, Outgoing, Serialize, Deserialize, PartialEq)]
#[outgoing(name = "receipts")]
struct Receipt {
id: u64,
}
#[derive(Default)]
struct Seen(AtomicU32);
#[subscriber("orders", reply)]
async fn accept(order: &Order, ctx: &mut Context<'_, (), Seen>) -> Result<Receipt, HandlerOutcome> {
if ctx.state().0.fetch_add(1, Ordering::SeqCst) == 0 {
return Err(HandlerOutcome::retry_after(RETRY_DELAY));
}
Ok(Receipt { id: order.id })
}
#[subscriber("receipts")]
async fn file(receipt: &Receipt) -> HandlerOutcome {
let _ = receipt.id;
HandlerOutcome::ack()
}
fn app() -> RustStream<Identity, Seen> {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Seen::default()))
.with_broker(MemoryBroker::new(), |b| {
b.include(accept).out_reply(MemoryPublish);
b.include(file);
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_delayed_redelivery_live() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start_live(app()).await?;
tb.broker::<MemoryBroker>()
.message(&Order { id: 7 })
.publish()
.await?;
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::retry_after(RETRY_DELAY));
tb.advance(RETRY_DELAY).await?;
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called(2);
tb.broker::<MemoryBroker>()
.published::<Receipt>("receipts")
.assert_called_once()
.with(&Receipt { id: 7 });
tb.broker::<MemoryBroker>()
.subscriber("receipts")
.assert_called_once()
.with(&Receipt { id: 7 });
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_live_start_refuses_a_paused_clock() {
match TestApp::start_live(app()).await {
Err(TestError::PausedClock) => {}
other => panic!(
"expected the paused clock refused, got {:?}",
other.map(|_| ())
),
}
}
#[derive(Debug, Outgoing, Serialize, Deserialize)]
struct Job {
id: u64,
}
#[subscriber("slow")]
async fn slow(job: &Job) -> HandlerOutcome {
let _ = job.id;
tokio::time::sleep(Duration::from_secs(2)).await;
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_live_settle_that_outlasts_the_deadline_says_so() {
let app = RustStream::new(AppInfo::new("slow", "0.1.0"))
.shutdown_timeout(Duration::from_millis(10))
.with_broker(MemoryBroker::new(), |b| {
b.include(slow);
});
let tb = TestApp::start_live_within(app, Duration::from_millis(100))
.await
.expect("start");
let err = tb
.broker::<MemoryBroker>()
.message(&Job { id: 1 })
.to("slow")
.publish()
.await
.expect_err("the handler outlives the deadline");
let message = err.to_string();
assert!(message.contains("did not settle within 100ms"), "{message}");
assert!(
message.contains("subscription \"slow\" handled 0 of 1 deliveries"),
"{message}"
);
}
#[subscriber("orders")]
async fn take_order_slowly(order: &Order) -> HandlerOutcome {
let _ = order.id;
tokio::time::sleep(Duration::from_millis(50)).await;
HandlerOutcome::ack()
}
#[derive(Debug, thiserror::Error)]
#[error("the announcement failed: {0}")]
struct AnnounceError(String);
fn announcing_app() -> RustStream {
let west = MemoryBroker::new().bindable();
let to_west = west.bind(MemoryPublish);
RustStream::new(AppInfo::new("announcing", "0.1.0"))
.with_broker_labeled("east", MemoryBroker::new(), |b| {
b.after_startup(MemoryPublish, async move |_east: MemoryPublisher| {
let publisher = to_west
.live()
.await
.map_err(|err| AnnounceError(err.to_string()))?;
publisher
.message(&Order { id: 0 })
.publish()
.await
.map_err(|err| AnnounceError(err.to_string()))
});
b.include(take_order_slowly);
})
.with_broker_labeled("west", west, |b| {
b.include(take_order_slowly);
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_startup_hook_publish_is_not_awaited() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start_live_within(announcing_app(), Duration::from_secs(1)).await?;
tb.settle().await?;
tb.broker_named("east")
.subscriber("orders")
.assert_not_called();
tb.broker_named("east")
.published::<Order>("orders")
.assert_not_called();
tb.broker_named("west")
.published::<Order>("orders")
.assert_not_called();
tb.shutdown().await?;
Ok(())
}
#[subscriber("orders")]
async fn take_order(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[derive(Debug, thiserror::Error)]
#[error("the readiness check never passed")]
struct NotReady;
fn failing_app(broker: MemoryBroker) -> RustStream {
RustStream::new(AppInfo::new("failing", "0.1.0"))
.after_startup(async move |_state| Err::<(), _>(NotReady))
.with_broker(broker, |b| {
b.include(take_order);
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_failed_start_shuts_its_brokers_down() {
for live in [false, true] {
let broker = MemoryBroker::new();
let app = failing_app(broker.clone());
let started = if live {
TestApp::start_live(app).await
} else {
TestApp::start(app).await
};
match started {
Err(TestError::Startup(source)) => assert_eq!(
source.to_string(),
"the readiness check never passed",
"the hook's own error is the start's (live: {live})",
),
other => panic!(
"expected the start to fail (live: {live}), got {:?}",
other.map(|_| ())
),
}
let late = broker.publisher().message(&Order { id: 1 }).publish().await;
assert!(
late.is_err(),
"the broker a failed start connected is still up (live: {live})",
);
}
}
#[subscriber("stuck")]
async fn stuck(job: &Job, ctx: &mut Context<'_, (), Arc<()>>) -> HandlerOutcome {
let _ = (job.id, ctx.state());
tokio::time::sleep(Duration::from_secs(3600)).await;
HandlerOutcome::ack()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_timed_out_shutdown_stops_the_handler_it_waited_for() -> Result<(), Box<dyn Error>> {
let state = Arc::new(());
let held = Arc::clone(&state);
let app = RustStream::new(AppInfo::new("stuck", "0.1.0"))
.shutdown_timeout(Duration::from_millis(10))
.on_startup(async move |()| Ok::<_, Infallible>(held))
.with_broker(MemoryBroker::new(), |b| {
b.include(stuck);
});
let tb = TestApp::start_live_within(app, Duration::from_millis(100)).await?;
let unsettled = tb
.broker::<MemoryBroker>()
.message(&Job { id: 1 })
.to("stuck")
.publish()
.await;
assert!(unsettled.is_err(), "the handler outlives the deadline");
tb.shutdown().await?;
assert_eq!(
Arc::strong_count(&state),
1,
"the handler the shutdown timed out on is still running",
);
Ok(())
}
#[subscriber("stuck-a")]
async fn stuck_a(job: &Job) -> HandlerOutcome {
let _ = job.id;
std::future::pending::<()>().await;
HandlerOutcome::ack()
}
#[subscriber("stuck-b")]
async fn stuck_b(job: &Job) -> HandlerOutcome {
let _ = job.id;
std::future::pending::<()>().await;
HandlerOutcome::ack()
}
#[tokio::test(start_paused = true)]
async fn one_shutdown_timeout_bounds_every_stuck_subscription() -> Result<(), Box<dyn Error>> {
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
let app = RustStream::new(AppInfo::new("stuck", "0.1.0"))
.shutdown_timeout(SHUTDOWN_TIMEOUT)
.with_broker(MemoryBroker::new(), |b| {
b.include(stuck_a);
b.include(stuck_b);
});
let tb = TestApp::start(app).await?;
for name in ["stuck-a", "stuck-b"] {
let publish = tb
.broker::<MemoryBroker>()
.message(&Job { id: 1 })
.to(name)
.publish();
assert!(
tokio::time::timeout(Duration::from_millis(1), publish)
.await
.is_err()
);
}
let started = tokio::time::Instant::now();
tb.shutdown().await?;
assert_eq!(started.elapsed(), SHUTDOWN_TIMEOUT);
Ok(())
}