#![cfg_attr(not(test), allow(dead_code))]
use std::time::Duration;
use ruststream_fred::DELIVERY_COUNT_HEADER;
use ruststream_fred::stream::prelude::*;
use serde::{Deserialize, Serialize};
const MIN_IDLE: Duration = Duration::from_secs(30);
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "payments")]
struct Payment {
id: u64,
account: String,
amount_cents: u64,
}
#[subscriber(
RedisStream::claiming("payments", MIN_IDLE).group("ledger"),
workers(8, by_key)
)]
async fn post_payment(payment: &Payment, ctx: &mut Context<'_>) -> HandlerOutcome {
let earlier_deliveries = ctx.headers().get_str(DELIVERY_COUNT_HEADER).unwrap_or("0");
if payment.account.starts_with("migrating-") {
tracing::warn!(
payment.id,
earlier_deliveries,
"account locked, payment left pending"
);
return HandlerOutcome::retry();
}
tracing::info!(payment.id, earlier_deliveries, "payment posted");
HandlerOutcome::ack()
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "payments.screened")]
struct Screened {
payment_id: u64,
suspicious: bool,
}
#[subscriber(
RedisStream::new("payments")
.group("fraud")
.start_id(StreamStart::Beginning),
reply
)]
async fn screen_payment(payment: &Payment) -> Screened {
Screened {
payment_id: payment.id,
suspicious: payment.amount_cents > 1_000_000,
}
}
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(
RedisBroker::standalone("redis://localhost:6379"),
|b| {
b.include(post_payment);
b.include(screen_payment).out_reply(Publish);
},
)
}
#[subscriber(RedisStream::new("payments").group("ledger"))]
async fn post_new_payment(payment: &Payment) -> HandlerOutcome {
tracing::info!(payment.id, "payment posted");
HandlerOutcome::ack()
}
#[subscriber(RedisStream::reclaim("payments", MIN_IDLE).group("ledger"))]
async fn recover_payment(payment: &Payment, ctx: &mut Context<'_>) -> HandlerOutcome {
let deliveries = ctx.headers().get_str(DELIVERY_COUNT_HEADER).unwrap_or("1");
tracing::warn!(
payment.id,
deliveries,
"payment recovered from a stopped worker"
);
HandlerOutcome::ack()
}
fn app_before_8_4() -> impl App {
RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(
RedisBroker::standalone("redis://localhost:6379"),
|b| {
b.include(post_new_payment);
b.include(recover_payment);
b.include(screen_payment).out_reply(Publish);
},
)
}
#[cfg(test)]
mod tests {
use std::error::Error;
use ruststream::testing::TestApp;
use ruststream_fred::stream::prelude::*;
use super::{MIN_IDLE, Payment, Screened, app, app_before_8_4};
fn payment(id: u64, account: &str) -> Payment {
Payment {
id,
account: account.to_owned(),
amount_cents: 2500,
}
}
#[tokio::test]
async fn each_group_reads_every_payment() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<RedisBroker>()
.message(&payment(1, "acct-1"))
.publish()
.await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(2)
.settled(HandlerOutcome::ack());
tb.broker::<RedisBroker>()
.published::<Screened>("payments.screened")
.assert_called_once()
.with(&Screened {
payment_id: 1,
suspicious: false,
});
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_pending_payment_comes_back_after_min_idle() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<RedisBroker>()
.message(&payment(2, "migrating-7"))
.publish()
.await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(2);
tb.advance(MIN_IDLE / 2).await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(2);
tb.advance(MIN_IDLE / 2).await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(3)
.settled(HandlerOutcome::retry());
tb.broker::<RedisBroker>()
.published::<Payment>("payments")
.assert_called_once();
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn before_8_4_an_acknowledged_payment_is_not_recovered() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app_before_8_4()).await?;
tb.broker::<RedisBroker>()
.message(&payment(3, "acct-3"))
.publish()
.await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(2)
.settled(HandlerOutcome::ack());
tb.advance(MIN_IDLE * 2).await?;
tb.broker::<RedisBroker>()
.subscriber("payments")
.assert_called(2);
tb.shutdown().await?;
Ok(())
}
}