Skip to main content

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;