#![allow(clippy::print_stdout, clippy::print_stderr)]
use spate::backpressure::InflightBudget;
use spate::checkpoint::{AckRef, Checkpointer};
use spate::deser::{Deserializer, EmitRecord, Owned, RecFamily};
use spate::error::{DeserError, ErrorPolicy};
use spate::ops::{ChunkConfig, PushOutcome, chain_owned};
use spate::record::{PartitionId, RawPayload, Record};
use spate::sink::{KeyHashRouter, shard_queues};
use spate::source::{LaneId, Source, SourceCtx, SourceEvent, SourceLane};
use spate_test::{TestDeserializer, TestEncoder, memory_source};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug)]
struct OrderHeader<'buf> {
order_id: &'buf str,
customer_id: &'buf str,
}
struct OrderHeaderF;
impl RecFamily for OrderHeaderF {
type Rec<'buf> = OrderHeader<'buf>;
}
struct OrderHeaderDeser;
impl Deserializer<OrderHeaderF> for OrderHeaderDeser {
fn deserialize<'buf>(
&mut self,
raw: &RawPayload<'buf>,
ack: &AckRef,
out: &mut dyn EmitRecord<'buf, OrderHeader<'buf>>,
) -> Result<(), DeserError> {
let text = std::str::from_utf8(raw.bytes).map_err(|e| DeserError::Malformed {
reason: e.to_string(),
})?;
let (order_id, customer_id) = text.split_once('|').ok_or(DeserError::Malformed {
reason: "order header has no customer field".to_string(),
})?;
let _ = out.emit(Record {
payload: OrderHeader {
order_id,
customer_id,
},
meta: raw.meta(),
ack: ack.clone(),
});
Ok(())
}
}
fn billing_key(order: OrderHeader<'_>) -> Vec<u8> {
format!("{}/{}", order.customer_id, order.order_id).into_bytes()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
spate::telemetry::init(spate::telemetry::LogFormat::Pretty, "info");
let (queues, mut receivers) = shard_queues(1, 16);
let budget = Arc::new(InflightBudget::new());
let mut seen: HashSet<Vec<u8>> = HashSet::new();
let mut chain = chain_owned::<Vec<u8>, _>(TestDeserializer::passthrough())
.flat_map::<Owned<Vec<u8>>, _>(move |word, out| {
if seen.insert(word.clone()) {
out.emit(word);
}
})
.try_map(
|word: Vec<u8>| {
if word.iter().all(u8::is_ascii_alphabetic) {
Ok(word)
} else {
Err("non-alphabetic word")
}
},
ErrorPolicy::Skip,
)
.map(|word: Vec<u8>| word.to_ascii_uppercase())
.sink(
TestEncoder,
KeyHashRouter,
ChunkConfig::default(),
queues,
budget,
)
.build();
let mut cp = Checkpointer::new();
let (mut source, handle) = memory_source();
source.open(SourceCtx::new(cp.handle()))?;
let p0 = PartitionId(0);
cp.begin_epoch(&[p0], 1);
handle.assign_lanes(&[(LaneId(0), p0)]);
let SourceEvent::LanesAssigned(mut lanes) = source.poll_events(Duration::from_millis(100))?
else {
panic!("expected assignment");
};
for word in ["hello", "world", "hello", "rust", "n0pe", "world"] {
handle.push(p0, None, word.as_bytes());
}
let mut batch = lanes[0]
.poll(64, Duration::from_millis(200))?
.expect("records queued");
assert!(matches!(chain.push_batch(&mut batch, 0), PushOutcome::Done));
drop(batch);
assert!(matches!(chain.flush(), PushOutcome::Done));
let mut rows = Vec::new();
while let Ok(chunk) = receivers[0].try_recv() {
rows.extend(
spate_test::decode_rows(&chunk.frame)
.into_iter()
.map(|r| String::from_utf8_lossy(&r).into_owned()),
);
}
println!("deduped + validated + uppercased: {rows:?}");
assert_eq!(rows, ["HELLO", "WORLD", "RUST"]);
let (order_queues, mut order_receivers) = shard_queues(1, 16);
let order_budget = Arc::new(InflightBudget::new());
let mut orders = spate::ops::chain::<OrderHeaderF, _>(OrderHeaderDeser)
.filter(|order: &OrderHeader<'_>| !order.order_id.is_empty())
.map_rec::<Owned<Vec<u8>>, _>(billing_key)
.sink(
TestEncoder,
KeyHashRouter,
ChunkConfig::default(),
order_queues,
order_budget,
)
.build();
for payload in ["o-17|cust-2", "|cust-2", "o-18|cust-3"] {
handle.push(p0, None, payload.as_bytes());
}
let mut batch = lanes[0]
.poll(64, Duration::from_millis(200))?
.expect("order headers queued");
assert!(matches!(
orders.push_batch(&mut batch, 0),
PushOutcome::Done
));
drop(batch);
assert!(matches!(orders.flush(), PushOutcome::Done));
let mut billed = Vec::new();
while let Ok(chunk) = order_receivers[0].try_recv() {
billed.extend(
spate_test::decode_rows(&chunk.frame)
.into_iter()
.map(|r| String::from_utf8_lossy(&r).into_owned()),
);
}
println!("billing keys off borrowed records: {billed:?}");
assert_eq!(billed, ["cust-2/o-17", "cust-3/o-18"]);
drop(chain); drop(orders);
cp.drain();
println!("committable watermarks: {:?}", cp.take_watermarks());
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn runs_to_completion() {
super::main().expect("the example must run clean");
}
}