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