#![cfg(feature = "testing")]
use std::time::Duration;
use ruststream::testing::TestApp;
use ruststream_fred::stream::prelude::*;
use ruststream_fred::{DELIVERY_COUNT_HEADER, IDLE_MS_HEADER};
use serde::{Deserialize, Serialize};
const URL: &str = "redis://localhost:6379";
const MIN_IDLE: Duration = Duration::from_secs(30);
const MAX_DELIVERIES: u64 = 2;
#[derive(Debug, Deserialize, Outgoing, PartialEq, Serialize)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
}
#[derive(Debug, Deserialize, Outgoing, PartialEq, Serialize)]
#[outgoing(name = "flaky")]
struct FlakyOrder {
id: u64,
}
#[derive(Debug, Deserialize, Outgoing, PartialEq, Serialize)]
#[outgoing(name = "orders.seen")]
struct Seen {
id: u64,
delivery_count: u64,
idle_ms: u64,
}
#[derive(OutSlot)]
#[publishes(Seen)]
struct Audit;
#[subscriber(RedisStream::claiming("orders", MIN_IDLE).group("workers"))]
async fn handle_order(
order: &Order,
ctx: &mut Context<'_>,
Out(audit): Out<impl Publisher, Audit>,
) -> HandlerOutcome {
let seen = Seen {
id: order.id,
delivery_count: counter(ctx, DELIVERY_COUNT_HEADER),
idle_ms: counter(ctx, IDLE_MS_HEADER),
};
let first = seen.delivery_count == 0;
if audit.message(&seen).publish().await.is_err() {
return HandlerOutcome::drop();
}
if first {
HandlerOutcome::retry()
} else {
HandlerOutcome::ack()
}
}
#[subscriber(RedisStream::claiming("flaky", MIN_IDLE).group("workers"))]
async fn handle_flaky(
order: &FlakyOrder,
ctx: &mut Context<'_>,
Out(audit): Out<impl Publisher, Audit>,
) -> HandlerOutcome {
let seen = Seen {
id: order.id,
delivery_count: counter(ctx, DELIVERY_COUNT_HEADER),
idle_ms: counter(ctx, IDLE_MS_HEADER),
};
let give_up = seen.delivery_count >= MAX_DELIVERIES;
if audit.message(&seen).publish().await.is_err() {
return HandlerOutcome::drop();
}
if give_up {
HandlerOutcome::drop()
} else {
HandlerOutcome::retry()
}
}
fn counter<Kind, AppState>(ctx: &Context<'_, Kind, AppState>, name: &str) -> u64 {
ctx.headers()
.get_str(name)
.unwrap_or_else(|| panic!("a claiming delivery must carry the {name} header"))
.parse()
.unwrap_or_else(|err| panic!("the {name} header must be a number: {err}"))
}
fn app() -> impl App<State = ()> {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
RedisBroker::standalone(URL),
|b| {
b.include(handle_order).out(Audit, Publish).build();
b.include(handle_flaky).out(Audit, Publish).build();
b.include(handle_impatient);
},
)
}
async fn start() -> TestApp<()> {
TestApp::start(app()).await.expect("start")
}
#[tokio::test(start_paused = true)]
async fn a_fresh_entry_reports_no_idle_time_and_no_earlier_delivery() {
let tb = start().await;
tb.broker::<RedisBroker>()
.message(&Order { id: 7 })
.publish()
.await
.expect("publish");
tb.broker::<RedisBroker>()
.subscriber("orders")
.assert_called_once();
tb.broker::<RedisBroker>()
.published::<Seen>("orders.seen")
.with(&Seen {
id: 7,
delivery_count: 0,
idle_ms: 0,
});
tb.shutdown().await.expect("shutdown");
}
#[tokio::test(start_paused = true)]
async fn an_entry_left_pending_comes_back_with_its_delivery_count_raised() {
let tb = start().await;
tb.broker::<RedisBroker>()
.message(&Order { id: 7 })
.publish()
.await
.expect("publish");
tb.broker::<RedisBroker>()
.subscriber("orders")
.assert_called_once()
.settled(HandlerOutcome::retry());
tb.advance(MIN_IDLE / 2).await.expect("advance");
tb.broker::<RedisBroker>()
.subscriber("orders")
.assert_called(1);
tb.advance(MIN_IDLE / 2).await.expect("advance");
tb.broker::<RedisBroker>()
.subscriber("orders")
.assert_called(2);
tb.broker::<RedisBroker>()
.published::<Seen>("orders.seen")
.with(&Seen {
id: 7,
delivery_count: 1,
idle_ms: 30_000,
});
tb.shutdown().await.expect("shutdown");
}
#[tokio::test(start_paused = true)]
async fn a_handler_stops_retrying_at_the_delivery_count_it_caps_on() {
let tb = start().await;
tb.broker::<RedisBroker>()
.message(&FlakyOrder { id: 3 })
.publish()
.await
.expect("publish");
for _ in 0..3 {
tb.advance(MIN_IDLE).await.expect("advance");
}
tb.broker::<RedisBroker>()
.subscriber("flaky")
.assert_called(3);
assert_eq!(
tb.broker::<RedisBroker>()
.published::<Seen>("orders.seen")
.decoded()
.iter()
.map(|seen| seen.delivery_count)
.collect::<Vec<_>>(),
[0, 1, 2],
"each claim raises the count the handler caps on",
);
tb.shutdown().await.expect("shutdown");
}
#[derive(Debug, Deserialize, Outgoing, PartialEq, Serialize)]
#[outgoing(name = "impatient")]
struct ImpatientOrder {
id: u64,
}
#[subscriber(RedisStream::claiming("impatient", MIN_IDLE).group("workers"))]
async fn handle_impatient(order: &ImpatientOrder) -> HandlerOutcome {
let _ = order;
HandlerOutcome::retry_after(SHORT_DELAY)
}
const SHORT_DELAY: Duration = Duration::from_secs(2);
#[tokio::test(start_paused = true)]
async fn a_delay_shorter_than_min_idle_rounds_up_to_it() {
let tb = start().await;
tb.broker::<RedisBroker>()
.message(&ImpatientOrder { id: 4 })
.publish()
.await
.expect("publish");
tb.broker::<RedisBroker>()
.subscriber("impatient")
.assert_called_once();
tb.advance(SHORT_DELAY).await.expect("advance");
tb.broker::<RedisBroker>()
.subscriber("impatient")
.assert_called(1);
tb.advance(MIN_IDLE).await.expect("advance");
tb.broker::<RedisBroker>()
.subscriber("impatient")
.assert_called(2);
tb.broker::<RedisBroker>()
.published::<ImpatientOrder>("impatient")
.assert_called_once();
tb.shutdown().await.expect("shutdown");
}