spate_core/pipeline/mod.rs
1//! Pipeline runtime: pinned driver threads, the source controller, and
2//! process assembly.
3//!
4//! Thread anatomy:
5//!
6//! ```text
7//! main (run()) controller (std thread) driver 0..N (std threads)
8//! metrics/admin/io ── owns Source + Checkpointer ── own lanes + chain
9//! joins everything poll_events / commit tick poll → push_batch → route
10//! pause/resume application backpressure ticks
11//! ```
12//!
13//! Communication: the controller sends `ThreadControl` messages to
14//! drivers (lane assignment, drain barriers); drivers send `DriverEvent`
15//! requests back (pause/resume and fatal reports). Only the controller
16//! touches the [`Source`](crate::source::Source). All channels are unbounded
17//! crossbeam channels; control traffic is rare and must never block a poll
18//! loop.
19//!
20//! Shutdown (also the full-revocation path):
21//! SIGTERM → controller stops event polling and sends `Shutdown` to every
22//! driver → each driver flushes its chain, drops its lanes, and arrives at
23//! the barrier → main joins driver threads (dropping chains closes the
24//! shard queues) → the sink drains under the remaining deadline → the
25//! controller runs a final drain + commit + `flush_commits` → the process
26//! reports an [`ExitReport`]. A sink that cannot flush by the deadline is
27//! abandoned loudly; unacknowledged offsets are never committed, so the
28//! data replays after restart (at-least-once).
29
30mod builder;
31mod controller;
32mod driver;
33mod runtime;
34
35pub use builder::{BuildError, ChainCtx, Pipeline, PipelineError, SinkOptions};
36pub use runtime::{PipelineRuntime, RuntimeOptions, ShutdownHandle, StartError, metrics_settings};
37
38use crate::error::FatalError;
39use crate::record::PartitionId;
40use crate::sink::ShardQueues;
41use crate::source::{DrainBarrier, LaneId};
42use std::time::Instant;
43
44/// Control messages the controller sends to a driver thread.
45pub(crate) enum ThreadControl<L> {
46 /// Take ownership of a newly assigned lane, with the pending-ceiling
47 /// gate for its partition (`None` only if the partition is unknown to
48 /// the checkpointer, which is a controller bug; the lane then runs
49 /// ungated rather than not at all).
50 AddLane {
51 lane: L,
52 gate: Option<crate::checkpoint::PendingGate>,
53 },
54 /// Stop and drop the listed lanes (revocation): flush the chain, then
55 /// arrive at `barrier` once per stopped lane before `deadline`.
56 StopLanes {
57 lanes: Vec<LaneId>,
58 barrier: DrainBarrier,
59 deadline: Instant,
60 },
61 /// Flush the chain's partial terminal state now, without waiting out
62 /// the idle-flush lull
63 /// ([`SourceEvent::CommitReady`](crate::source::SourceEvent::CommitReady)).
64 ///
65 /// A lane that reached end-of-input has no more data coming, so its tail
66 /// would otherwise sit in the chain until `idle_flush` elapses,
67 /// unacknowledged, blocking the completion of whatever unit of work it
68 /// belongs to. Best-effort and unsynchronized; the controller does not
69 /// wait, and a blocked chain retries on the ordinary lull check.
70 FlushNow,
71 /// Drop the listed lanes without flushing or synchronizing
72 /// ([`SourceEvent::LanesRetired`](crate::source::SourceEvent::LanesRetired)):
73 /// their input is fully delivered, acknowledged *and* committed, so
74 /// nothing of theirs can sit unflushed in the chain. Pure bookkeeping,
75 /// with no barrier, no deadline, no `flush_until`. Forcing a flush here
76 /// would fragment sink batches and park the thread once per completed unit
77 /// of work; sources with anything in flight must use
78 /// [`ThreadControl::StopLanes`] instead.
79 DropLanes { lanes: Vec<LaneId> },
80 /// Stop everything (shutdown): flush the chain, drop all lanes, arrive
81 /// once at `barrier`, and exit the thread.
82 Shutdown {
83 barrier: DrainBarrier,
84 deadline: Instant,
85 },
86}
87
88/// Requests and reports a driver thread sends the controller.
89#[derive(Debug)]
90pub(crate) enum DriverEvent {
91 /// Backpressure tripped: pause these lanes at the source.
92 PauseLanes { lanes: Vec<LaneId> },
93 /// Backpressure cleared: resume these lanes.
94 ResumeLanes { lanes: Vec<LaneId> },
95 /// The chain failed or panicked; the pipeline must stop.
96 Fatal { thread: usize, error: FatalError },
97}
98
99/// What the sink reported when draining at shutdown. This is the sink
100/// layer's [`DrainReport`], re-exported so
101/// assemblies hand `SinkPool::drain`'s result straight through.
102pub use crate::sink::DrainReport;
103
104/// The sink half the runtime drives: the shared shard-queue handle plus a
105/// drain hook invoked once at shutdown with the remaining drain budget.
106///
107/// Built by the sink layer (`SinkPool`) or by tests; the runtime does not
108/// see worker internals.
109pub struct SinkRuntime {
110 /// Sending side of the per-shard chunk queues, one entry per installed
111 /// sink (the runtime only uses capacity introspection for the
112 /// backpressure resume gate; the chain's terminal stage holds clones).
113 /// A single-sink pipeline has one entry.
114 pub queues: Vec<ShardQueues>,
115 /// Drain the sinks: flush what's pending within the budget, fail the
116 /// acknowledgments of anything abandoned, and report. For multi-sink
117 /// pipelines this is the composed hook that drains every sink.
118 pub drain: SinkDrainFn,
119 /// Optional connectivity probe (e.g. `SinkPool::probe_all`). The
120 /// runtime probes at startup and then periodically, driving the
121 /// sinks-connected half of `/readyz`. Without a probe the flag is set
122 /// unconditionally.
123 pub probe: Option<SinkProbeFn>,
124}
125
126impl std::fmt::Debug for SinkRuntime {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("SinkRuntime")
129 .field("queues", &self.queues)
130 .finish_non_exhaustive()
131 }
132}
133
134/// Boxed sink drain hook, defined next to the sink layer that produces it
135/// and re-exported here where the runtime consumes it.
136pub use crate::sink::{SinkDrainFn, SinkProbeFn};
137
138/// Terminal state of a pipeline run.
139#[derive(Clone, Debug, PartialEq, Eq)]
140#[non_exhaustive]
141pub enum ExitState {
142 /// Drained and committed cleanly: a requested shutdown (SIGTERM or
143 /// programmatic), or a bounded source reporting
144 /// [`SourceEvent::Drained`](crate::source::SourceEvent::Drained) after
145 /// exhausting its input. For the drained case this additionally
146 /// guarantees every batch was acknowledged and the final watermark
147 /// commit persisted (anything less exits `Failed`).
148 Completed,
149 /// A fatal error stopped the pipeline; the process should exit
150 /// non-zero.
151 Failed(FatalErrorReport),
152}
153
154/// Owned copy of the fatal error carried in the exit report.
155#[derive(Clone, Debug, PartialEq, Eq)]
156#[non_exhaustive]
157pub struct FatalErrorReport {
158 /// Component that failed.
159 pub component: String,
160 /// Human-readable cause.
161 pub reason: String,
162}
163
164impl std::fmt::Display for FatalErrorReport {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 write!(f, "pipeline failed in {}: {}", self.component, self.reason)
167 }
168}
169
170impl std::error::Error for FatalErrorReport {}
171
172/// Outcome of [`PipelineRuntime::run`].
173#[derive(Debug)]
174#[non_exhaustive]
175pub struct ExitReport {
176 /// How the run ended.
177 pub state: ExitState,
178 /// The sink's drain report (absent when the sink drain hook could not
179 /// run, e.g. the I/O runtime was already gone).
180 pub sink_drain: Option<DrainReport>,
181 /// The last committed watermark per partition, as reported by the
182 /// final commit.
183 pub final_watermarks: Vec<(PartitionId, i64)>,
184}
185
186impl ExitReport {
187 /// Log the outcome (state, sink drain, final watermarks) at the level
188 /// matching it (`info` for a clean exit, `error` for a failure).
189 pub fn log(&self) {
190 match &self.state {
191 ExitState::Completed => tracing::info!(
192 state = ?self.state,
193 drain = ?self.sink_drain,
194 watermarks = ?self.final_watermarks,
195 "pipeline finished"
196 ),
197 ExitState::Failed(failure) => tracing::error!(
198 component = %failure.component,
199 reason = %failure.reason,
200 drain = ?self.sink_drain,
201 watermarks = ?self.final_watermarks,
202 "pipeline failed"
203 ),
204 }
205 }
206
207 /// The process exit code this outcome maps to: `0` for a clean exit,
208 /// `1` for a failure.
209 #[must_use]
210 pub fn exit_code(&self) -> i32 {
211 match self.state {
212 ExitState::Completed => 0,
213 ExitState::Failed(_) => 1,
214 }
215 }
216
217 /// The report as a `Result`, so a `main` can `?` a failed run: a clean
218 /// exit passes the report through, a failure returns the
219 /// [`FatalErrorReport`] as the error.
220 pub fn ok(self) -> Result<ExitReport, FatalErrorReport> {
221 match &self.state {
222 ExitState::Completed => Ok(self),
223 ExitState::Failed(failure) => Err(failure.clone()),
224 }
225 }
226}
227
228#[cfg(all(test, not(loom)))]
229pub(crate) mod fakes;
230#[cfg(all(test, not(loom)))]
231mod tests;