use ruststream_kinesis::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "customer-orders")]
struct Order {
id: u64,
customer: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "customer-orders.journal")]
struct Entry {
order: u64,
customer: String,
}
#[derive(OutSlot)]
#[publishes(Entry)]
struct Journal;
#[subscriber(
KinesisStream::new("customer-orders").create_if_missing(2),
workers(4, by_key)
)]
async fn post(
order: &Order,
Out(journal): Out<impl Publisher<Options = KinesisPublishOptions>, Journal>,
) -> HandlerOutcome {
let entry = Entry {
order: order.id,
customer: order.customer.clone(),
};
if journal
.message(&entry)
.partition_key(order.customer.as_str())
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
tracing::info!(order.id, order.customer, "order posted");
HandlerOutcome::ack()
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "receipts")]
struct Receipt {
order: u64,
}
#[subscriber(KinesisStream::new("customer-orders.journal").create_if_missing(1), reply)]
async fn confirm(entry: &Entry) -> Receipt {
Receipt { order: entry.order }
}
fn service(broker: KinesisBroker) -> RustStream {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
b.include(post).out(Journal, Publish::default()).build();
b.include(confirm)
.out_reply(Publish::default())
.partition_key("receipts");
})
}
#[ruststream::app]
fn app() -> impl App {
service(
KinesisBroker::new()
.endpoint("http://localhost:4566")
.region("us-east-1")
.test_credentials(),
)
}
#[cfg(test)]
#[path = "../support/kinesis_live.rs"]
mod support;
#[cfg(test)]
mod tests {
use std::error::Error;
use ruststream::HeaderMap;
use ruststream::testing::TestApp;
use ruststream_kinesis::PARTITION_KEY_HEADER;
use super::*;
async fn place_orders(tb: &TestApp<()>) -> Result<(), Box<dyn Error>> {
for (id, customer) in [
(1, "ana"),
(2, "bo"),
(3, "cy"),
(4, "ana"),
(5, "bo"),
(6, "cy"),
] {
let mut key = HeaderMap::new();
key.insert(PARTITION_KEY_HEADER, customer);
tb.broker::<KinesisBroker>()
.message(&Order {
id,
customer: customer.to_owned(),
})
.with_headers(key)
.publish()
.await?;
}
Ok(())
}
fn orders_of(tb: &TestApp<()>, customer: &str) -> Vec<u64> {
tb.broker::<KinesisBroker>()
.subscriber("customer-orders.journal")
.received::<Entry>()
.into_iter()
.filter(|entry| entry.customer == customer)
.map(|entry| entry.order)
.collect()
}
async fn each_customer_keeps_its_order(tb: TestApp<()>) -> Result<(), Box<dyn Error>> {
place_orders(&tb).await?;
tb.broker::<KinesisBroker>()
.subscriber("customer-orders")
.assert_called(6)
.settled(HandlerOutcome::ack());
assert_eq!(orders_of(&tb, "ana"), [1, 4]);
assert_eq!(orders_of(&tb, "bo"), [2, 5]);
assert_eq!(orders_of(&tb, "cy"), [3, 6]);
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn each_customer_keeps_its_order_in_process() -> Result<(), Box<dyn Error>> {
each_customer_keeps_its_order(TestApp::start(service(KinesisBroker::new())).await?).await
}
#[tokio::test]
async fn a_journal_entry_takes_the_customer_as_its_key() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(KinesisBroker::new())).await?;
place_orders(&tb).await?;
tb.out::<Journal>()
.assert_called(6)
.with_options(&KinesisPublishOptions {
partition_key: Some("cy".to_owned()),
});
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn every_receipt_shares_one_key() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(service(KinesisBroker::new())).await?;
place_orders(&tb).await?;
tb.broker::<KinesisBroker>()
.published::<Receipt>("receipts")
.assert_called(6)
.with_header(PARTITION_KEY_HEADER, "receipts");
tb.shutdown().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn each_customer_keeps_its_order_across_two_shards() -> Result<(), Box<dyn Error>> {
let Some(endpoint) = support::endpoint() else {
return Ok(());
};
support::ensure_stream(&support::config(&endpoint).await, "receipts", 1).await;
let tb = TestApp::start_live(service(support::broker(&endpoint))).await?;
each_customer_keeps_its_order(tb).await
}
}