Expand description
Operator chain: statically composed push stages behind one type-erasure boundary per batch.
Stages compose via Collector (monomorphized, so a whole chain compiles
to one loop); the only virtual call on the data path is
RunnableChain::push_batch, once per poll batch. Records are born
(deserialized) and die (encoded into shard frames, filtered, or skipped)
inside a single push_batch call, so borrowed payloads never cross or
outlive the boundary (ADR-0013).
§Owned vs borrowed record families
For owned families (Owned<T>) the builder
offers ChainBuilder::map / ChainBuilder::try_map with plain
closure bounds; bare closures infer. For borrowing families a
rustc limitation (E0582: a higher-ranked lifetime may not appear only
in associated-type positions) rules out FnMut-with-projection-output
bounds at the definition site; use ChainBuilder::map_rec /
ChainBuilder::try_map_rec, whose bound goes through MapFn /
TryMapFn. Pass a fn item where you can: it satisfies a
higher-ranked bound by construction, where a closure satisfies it only
when the compiler infers a higher-ranked signature for it.
A stage over a borrowing family, written as a fn item:
struct Compact<'buf> {
key: &'buf str,
}
struct CompactF;
impl RecFamily for CompactF {
type Rec<'buf> = Compact<'buf>;
}
fn shrink<'a>(e: LogEvent<'a>) -> Compact<'a> {
Compact { key: e.key }
}
let stage = chain(log_deser).map_rec::<CompactF, _>(shrink);ChainBuilder::filter, ChainBuilder::inspect, and
ChainBuilder::flat_map have no output binding, so a single generic
method serves both kinds of family.
use spate_core::backpressure::InflightBudget;
use spate_core::deser::{BytesPassthrough, Owned};
use spate_core::error::ErrorPolicy;
use spate_core::ops::{ChunkConfig, chain};
use spate_core::record::Record;
use spate_core::sink::{KeyHashRouter, RowEncoder, shard_queues};
use std::sync::Arc;
// A trivial encoder writing `<u32 len><bytes>` rows.
#[derive(Clone)]
struct LenPrefix;
impl RowEncoder<Owned<Vec<u8>>> for LenPrefix {
fn encode<'buf>(
&mut self,
rec: &Record<Vec<u8>>,
buf: &mut bytes::BytesMut,
) -> Result<(), spate_core::error::SinkError> {
buf.extend_from_slice(&(rec.payload.len() as u32).to_le_bytes());
buf.extend_from_slice(&rec.payload);
Ok(())
}
}
let (queues, _rx) = shard_queues(2, 64);
let budget = Arc::new(InflightBudget::new());
let mut pipeline_chain = chain(BytesPassthrough)
.map(|mut bytes: Vec<u8>| {
bytes.make_ascii_uppercase();
bytes
})
.filter(|bytes: &Vec<u8>| !bytes.is_empty())
.try_map(
|bytes: Vec<u8>| String::from_utf8(bytes).map(String::into_bytes),
ErrorPolicy::Skip,
)
.sink(LenPrefix, KeyHashRouter, ChunkConfig::default(), queues, budget)
.build();Structs§
- Chain
Builder - Fluent builder for one pipeline’s operator chain.
DFis the deserializer’s record family;CurFthe family at the current end of the chain (changed bymap_recandflat_map, and bymapfor owned payloads). - Chain
Factory - Stamps out identical chains, one per pipeline thread.
Send + Syncwhen the deserializer, stage closures, encoder, and router are. - Chunk
Config - Tuning for the terminal stage’s per-shard chunking.
- Emitter
- Stack-borrowed emitter handed to
flat_mapclosures. Parameterized by the output family (a'statictag), so user closures never name the concrete downstream stack type or the buffer lifetime. Eachemitis one virtual call, confined to flat_map stages. - Filter
filter: drop records failing the predicate. A drop releases the record’s ack share, so it counts as success for the batch.- Filter
Part - Recorded
filterstage. - FlatMap
flat_map: one record in, 0..N out via a stack-borrowedEmitter. Carries the output family as a type parameter so the impl is fully constrained (closure argument types are not associated bindings).- Flat
MapPart - Recorded
flat_mapstage. - Inspect
inspect: observe without transforming (no metrics of its own).- Inspect
Part - Recorded
inspectstage. - Map
map: transform the payload.- MapPart
- Recorded
map/map_recstage. - Root
- The empty stage list.
- Routed
Split - A split-terminated chain, ready to
build. NotClone; the branches are built once. Stamp identical per-thread chains by re-running thechainsfactory closure (as the pipeline builder does). - Sink
- A typed,
Copyhandle to one split branch, carrying a branch index plus the destination family, soSplitEmitter::emitboth type-checks the row and recovers the branch with zero per-call lookup. Minted bySplitBuilder::add. - SinkCtx
- The per-sink handles a split branch needs, resolved by name from the
chain factory’s
ChainCtxviaChainCtx::sink. The bundle carries the sink’s name, soSplitBuilder::adddoes not repeat it. - Sink
Handoff - The chain’s terminal stage. Owns one accumulation buffer per shard,
seals
EncodedChunks atChunkConfig::target_bytes, and hands them to the sink workers through the boundedShardQueues, with atry_sendthat never blocks the pipeline thread. - Sinked
Chain - A fully specified chain, ready to build, or to stamp out one instance per
pipeline thread via
SinkedChain::build_factory. - Split
Builder - Accumulates split-sink branches before the routing closure is supplied.
Built by
ChainBuilder::split; eachadddeclares one destination and hands back a typed handle, thenroutetakes the closure that dispatches to them. - Split
Emitter - Stack-borrowed emitter handed to a
routeclosure.emitroutes one derived record to the branch named by aSink<F>handle; a record that emits to no branch triggers the split’sunmatchedpolicy. OneAnydowncast per emit, no per-call name lookup. - Split
Terminal - The chain’s split terminal. Runs the route closure over each record and aggregates the branches’ lifecycle (relieve/flush/fatal). See the module docs.
- TryMap
try_map: fallible transform with a per-stageErrorPolicy.Skipdrops the record (releasing its ack share) and counts it;Failrecords a fatal error; the batch aborts and the pipeline stops.- TryMap
Part - Recorded
try_map/try_map_recstage. - Typed
Chain - A concrete chain: deserializer + statically composed operator stack,
erased behind
RunnableChain.Opsmust accept the family’s record type at every buffer lifetime; the HRTB is what makes borrowed records legal behind the erased boundary.
Enums§
- Block
Reason - Why a batch could not complete yet. Both cases are retried with the
resume cursor, but only
BlockReason::Capacityengages the driver’s backpressure controller. A not-ready wait is an upstream dependency (e.g. a schema fetch), not sink pressure, and pausing the source for it would misreport the pipeline’s state. - Push
Outcome - Result of pushing one batch (or a resumed suffix of one) through a chain.
Traits§
- Assemble
- Assembles recorded parts into the concrete collector stack, given the
terminal stage. Takes
&selfso one set of parts can assemble many identical chains. Stage closures must therefore beClone(plain closures and closures overClone/Arcstate are). - Collector
- Push-model stage: receives one record, forwards 0..N downstream.
- Collector
For - Family-erased collector: accepts the family’s record type at any
buffer lifetime through a lifetime-generic method, which keeps it
dyn-compatible. This is what lets
flat_mapclosures hold a plain&mut Emitter<'_, OutF>without naming the downstream stack type. - MapFn
- A record-to-record transform between families. Implemented for every
FnMut(In) -> Out; expressed as an independent two-parameter trait so higher-ranked builder bounds stay legal for borrowing families (see the module docs on E0582).fnitems satisfy it at every lifetime. - Runnable
Chain - The one erasure boundary between a pipeline thread’s driver loop and a
typed chain. The methods are generic over the buffer lifetime
only, so
Box<dyn RunnableChain>is legal. - Stage
Lifecycle - Per-batch lifecycle cascade implemented by every stage. Combinators handle their own concern and delegate downstream; the terminal stage anchors the recursion.
- TryMap
Fn - Fallible variant of
MapFn, with the error type as a third parameter.
Functions§
- chain
- Start a chain from a deserializer producing family
F. - chain_
owned - Start a chain from a deserializer producing owned records
T.