#![cfg_attr(not(test), allow(dead_code, unused_imports))]
use std::collections::HashSet;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ruststream::memory::prelude::*;
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "payments")]
struct Payment {
id: u64,
}
#[derive(Default)]
struct Ledger {
cleared: Mutex<HashSet<u64>>,
}
impl Ledger {
fn clear(&self, id: u64) {
if let Ok(mut cleared) = self.cleared.lock() {
cleared.insert(id);
}
}
fn is_cleared(&self, id: u64) -> bool {
self.cleared
.lock()
.is_ok_and(|cleared| cleared.contains(&id))
}
}
mod retry_after {
use std::convert::Infallible;
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::{Ledger, Payment};
const RECHECK: Duration = Duration::from_secs(5);
#[subscriber("payments")]
async fn reconcile(
payment: &Payment,
ctx: &mut Context<'_, (), Arc<Ledger>>,
) -> HandlerOutcome {
if !ctx.state().is_cleared(payment.id) {
return HandlerOutcome::retry_after(RECHECK);
}
tracing::info!(payment.id, "reconciled");
HandlerOutcome::ack()
}
fn app() -> impl App<State = Arc<Ledger>> {
RustStream::new(AppInfo::new("payments", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Arc::new(Ledger::default())))
.with_broker(MemoryBroker::new(), |b| {
b.include(reconcile);
})
}
#[tokio::test(start_paused = true)]
async fn a_pending_payment_comes_back_after_the_delay() -> Result<(), Box<dyn Error>> {
let ledger = Arc::new(Ledger::default());
let tb = TestApp::with_state(app(), |_| Arc::clone(&ledger)).await?;
tb.message(&Payment { id: 7 }).publish().await?;
tb.broker::<MemoryBroker>()
.subscriber("payments")
.assert_called_once()
.settled(HandlerOutcome::retry_after(RECHECK));
ledger.clear(7);
tb.advance(RECHECK).await?;
tb.broker::<MemoryBroker>()
.subscriber("payments")
.assert_called(2)
.settled(HandlerOutcome::ack());
Ok(())
}
}
mod batch_retry_after {
use std::convert::Infallible;
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::{Ledger, Payment};
const RECHECK: Duration = Duration::from_secs(30);
#[subscriber("payments")]
async fn reconcile(
payments: &[Payment],
ctx: &mut Context<'_, (), Arc<Ledger>>,
) -> Vec<HandlerOutcome> {
payments
.iter()
.map(|payment| {
if ctx.state().is_cleared(payment.id) {
HandlerOutcome::ack()
} else {
HandlerOutcome::retry_after(RECHECK)
}
})
.collect()
}
fn app(broker: MemoryBroker<Retaining>) -> impl App<State = Arc<Ledger>> {
RustStream::new(AppInfo::new("payments", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Arc::new(Ledger::default())))
.with_broker(broker, |b| {
b.include(
reconcile
.batch(nonzero!(2))
.start_at(MemoryPosition::start()),
);
})
}
#[tokio::test(start_paused = true)]
async fn only_the_pending_payment_comes_back() -> Result<(), Box<dyn Error>> {
let broker = MemoryBroker::retaining(Retention::Messages(nonzero!(16)));
let publisher = broker.publisher();
for id in [1, 2] {
publisher.message(&Payment { id }).publish().await?;
}
let ledger = Arc::new(Ledger::default());
ledger.clear(1);
let tb = TestApp::with_state(app(broker), |_| Arc::clone(&ledger)).await?;
tb.settle().await?;
tb.advance(RECHECK).await?;
let batches = tb
.broker::<MemoryBroker<Retaining>>()
.subscriber("payments")
.assert_called(2)
.batches::<Payment>();
assert_eq!(batches[1], [Payment { id: 2 }]);
Ok(())
}
}
mod declaration {
use std::error::Error;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use super::Payment;
#[subscriber("payments")]
async fn charge(payment: &Payment) -> HandlerOutcome {
tracing::warn!(payment.id, "gateway refused the charge");
HandlerOutcome::retry()
}
fn app() -> impl App {
RustStream::new(AppInfo::new("payments", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(charge)
.max_attempts(nonzero!(3u32))
.dead_letter("payments.dead");
})
}
#[tokio::test]
async fn the_third_failure_goes_to_the_dead_letter_subject() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.message(&Payment { id: 7 }).publish().await?;
let broker = tb.broker::<MemoryBroker>();
broker.subscriber("payments").assert_called(3);
broker
.published::<Payment>("payments.dead")
.assert_called_once()
.with(&Payment { id: 7 });
Ok(())
}
}
fn main() {}