etl_core/pipeline/mod.rs
1//! Pipeline runtime: pinned driver threads, the source controller, and
2//! process assembly.
3//!
4//! Thread anatomy (see `docs/DESIGN.md` § Process 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 — only the controller touches the
16//! [`Source`](crate::source::Source) — and fatal reports). All channels are
17//! unbounded crossbeam channels: control traffic is rare and must never
18//! block a poll loop.
19//!
20//! Shutdown (also the full-revocation path, per DESIGN.md § Shutdown):
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.
47 AddLane(L),
48 /// Stop and drop the listed lanes (revocation): flush the chain, then
49 /// arrive at `barrier` once per stopped lane before `deadline`.
50 StopLanes {
51 lanes: Vec<LaneId>,
52 barrier: DrainBarrier,
53 deadline: Instant,
54 },
55 /// Stop everything (shutdown): flush the chain, drop all lanes, arrive
56 /// once at `barrier`, and exit the thread.
57 Shutdown {
58 barrier: DrainBarrier,
59 deadline: Instant,
60 },
61}
62
63/// Requests and reports a driver thread sends the controller.
64#[derive(Debug)]
65pub(crate) enum DriverEvent {
66 /// Backpressure tripped: pause these lanes at the source.
67 PauseLanes { lanes: Vec<LaneId> },
68 /// Backpressure cleared: resume these lanes.
69 ResumeLanes { lanes: Vec<LaneId> },
70 /// The chain failed or panicked; the pipeline must stop.
71 Fatal { thread: usize, error: FatalError },
72}
73
74/// What the sink reported when draining at shutdown — the sink layer's
75/// [`DrainReport`](crate::sink::DrainReport), re-exported so assemblies
76/// hand `SinkPool::drain`'s result straight through.
77pub use crate::sink::DrainReport;
78
79/// The sink half the runtime drives: the shared shard-queue handle plus a
80/// drain hook invoked once at shutdown with the remaining drain budget.
81///
82/// Built by the sink layer (`SinkPool`) or by tests; the runtime is
83/// deliberately ignorant of worker internals.
84pub struct SinkRuntime {
85 /// Sending side of the per-shard chunk queues (the runtime only uses
86 /// capacity introspection; the chain's terminal stage holds clones).
87 pub queues: ShardQueues,
88 /// Drain the sink: flush what's pending within the budget, fail the
89 /// acknowledgements of anything abandoned, and report.
90 pub drain: SinkDrainFn,
91 /// Optional connectivity probe (e.g. `SinkPool::probe_all`). The
92 /// runtime probes at startup and then periodically, driving the
93 /// sinks-connected half of `/readyz`. Without a probe the flag is set
94 /// unconditionally.
95 pub probe: Option<SinkProbeFn>,
96}
97
98impl std::fmt::Debug for SinkRuntime {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("SinkRuntime")
101 .field("queues", &self.queues)
102 .finish_non_exhaustive()
103 }
104}
105
106/// Boxed sink drain hook — defined next to the sink layer that produces
107/// it, re-exported here where the runtime consumes it.
108pub use crate::sink::{SinkDrainFn, SinkProbeFn};
109
110/// Terminal state of a pipeline run.
111#[derive(Clone, Debug, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum ExitState {
114 /// Drained and committed cleanly (SIGTERM or programmatic shutdown).
115 Completed,
116 /// A fatal error stopped the pipeline; the process should exit
117 /// non-zero.
118 Failed(FatalErrorReport),
119}
120
121/// Owned copy of the fatal error carried in the exit report.
122#[derive(Clone, Debug, PartialEq, Eq)]
123#[non_exhaustive]
124pub struct FatalErrorReport {
125 /// Component that failed.
126 pub component: String,
127 /// Human-readable cause.
128 pub reason: String,
129}
130
131impl std::fmt::Display for FatalErrorReport {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "pipeline failed in {}: {}", self.component, self.reason)
134 }
135}
136
137impl std::error::Error for FatalErrorReport {}
138
139/// Outcome of [`PipelineRuntime::run`].
140#[derive(Debug)]
141#[non_exhaustive]
142pub struct ExitReport {
143 /// How the run ended.
144 pub state: ExitState,
145 /// The sink's drain report (absent when the sink drain hook could not
146 /// run, e.g. the I/O runtime was already gone).
147 pub sink_drain: Option<DrainReport>,
148 /// The last committed watermark per partition, as reported by the
149 /// final commit.
150 pub final_watermarks: Vec<(PartitionId, i64)>,
151}
152
153impl ExitReport {
154 /// Log the outcome — state, sink drain, final watermarks — at the level
155 /// matching it (`info` for a clean exit, `error` for a failure).
156 pub fn log(&self) {
157 match &self.state {
158 ExitState::Completed => tracing::info!(
159 state = ?self.state,
160 drain = ?self.sink_drain,
161 watermarks = ?self.final_watermarks,
162 "pipeline finished"
163 ),
164 ExitState::Failed(failure) => tracing::error!(
165 component = %failure.component,
166 reason = %failure.reason,
167 drain = ?self.sink_drain,
168 watermarks = ?self.final_watermarks,
169 "pipeline failed"
170 ),
171 }
172 }
173
174 /// The process exit code this outcome maps to: `0` for a clean exit,
175 /// `1` for a failure.
176 #[must_use]
177 pub fn exit_code(&self) -> i32 {
178 match self.state {
179 ExitState::Completed => 0,
180 ExitState::Failed(_) => 1,
181 }
182 }
183
184 /// The report as a `Result`, so a `main` can `?` a failed run: a clean
185 /// exit passes the report through, a failure returns the
186 /// [`FatalErrorReport`] as the error.
187 pub fn ok(self) -> Result<ExitReport, FatalErrorReport> {
188 match &self.state {
189 ExitState::Completed => Ok(self),
190 ExitState::Failed(failure) => Err(failure.clone()),
191 }
192 }
193}
194
195#[cfg(all(test, not(loom)))]
196pub(crate) mod fakes;
197#[cfg(all(test, not(loom)))]
198mod tests;