Skip to main content

laminar_db/pipeline/
callback.rs

1//! Pipeline callback trait and source registration types.
2//!
3//! Decouples the pipeline coordinator from `db.rs` internals so the
4//! TPC coordinator can drive SQL cycles, sink writes, and checkpoints
5//! through a narrow interface.
6
7use std::sync::Arc;
8use std::time::Duration;
9
10use arrow_array::RecordBatch;
11use laminar_connectors::checkpoint::SourceCheckpoint;
12use laminar_connectors::config::ConnectorConfig;
13use laminar_connectors::connector::{SourceConnector, SourcePosition};
14use laminar_core::checkpoint::{CheckpointAttempt, CheckpointAttemptRelation};
15use laminar_core::checkpoint::{CheckpointBarrier, CheckpointBarrierInjector};
16use laminar_core::cluster::control::CheckpointAssignmentFence;
17use rustc_hash::{FxHashMap, FxHashSet};
18
19#[cfg(feature = "cluster")]
20const CHECKPOINT_CONTROL_DIRECT_FALLBACK: Duration = Duration::from_millis(250);
21const CHECKPOINT_CONTROL_POLL_FALLBACK: Duration = Duration::from_millis(25);
22
23/// Room left on the graph input ports a source's batches are primed into.
24///
25/// The coordinator stages a source message only when it fits, so a full port makes source
26/// intake wait instead of halting the pipeline.
27#[doc(hidden)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct PortHeadroom {
30    /// Batches the fullest port can still take.
31    pub batches: usize,
32    /// Retained Arrow bytes the fullest port can still take, when ports have a byte bound.
33    pub bytes: Option<usize>,
34    /// Every port is empty. One message is then staged whatever its size, so the graph rejects a
35    /// batch no port could ever hold instead of the drain waiting for it forever.
36    pub empty: bool,
37}
38
39impl PortHeadroom {
40    /// Ports without a bound, or ports that shed instead of waiting.
41    pub const UNBOUNDED: Self = Self {
42        batches: usize::MAX,
43        bytes: None,
44        empty: true,
45    };
46
47    /// Charge one source message's batch if it fits, leaving the headroom unchanged otherwise.
48    pub(crate) fn try_charge(&mut self, batch: &RecordBatch) -> bool {
49        let bytes = self
50            .bytes
51            .map(|_| laminar_core::streaming::retained_arrow_bytes(batch));
52        let fits = self.empty
53            || (self.batches > 0
54                && bytes
55                    .zip(self.bytes)
56                    .is_none_or(|(bytes, left)| bytes <= left));
57        if fits {
58            self.empty = false;
59            self.batches = self.batches.saturating_sub(1);
60            if let (Some(left), Some(bytes)) = (self.bytes.as_mut(), bytes) {
61                *left = left.saturating_sub(bytes);
62            }
63        }
64        fits
65    }
66}
67
68/// Bounded-output pressure reported to the pipeline coordinator.
69#[doc(hidden)]
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ExternalOutputPressure {
72    /// No checkpoint or intake intervention is required.
73    Normal,
74    /// Accelerate the next configured periodic checkpoint without pausing compute.
75    CheckpointDue,
76    /// Stop compute intake until the next checkpoint's barriers or a local assignment drain cut
77    /// are in flight. With no prepared cut, only a new checkpoint can release the open output, and
78    /// a topology hold can keep one from starting.
79    CheckpointBackpressured,
80    /// Stop compute intake while a prepared checkpoint cut completes.
81    CommitBackpressured,
82}
83
84impl ExternalOutputPressure {
85    #[cfg(feature = "cluster")]
86    pub(crate) const fn checkpoint_due(self) -> bool {
87        matches!(self, Self::CheckpointDue | Self::CheckpointBackpressured)
88    }
89
90    /// Whether compute intake must stop. Checkpoint barriers and assignment drain cuts reach the
91    /// coordinator through the same source FIFO as data, and an active drain defers every
92    /// checkpoint until its cut arrives, so checkpoint backpressure releases intake while either
93    /// is in flight.
94    pub(crate) fn pauses_intake(self, cut_in_flight: impl FnOnce() -> bool) -> bool {
95        match self {
96            Self::Normal | Self::CheckpointDue => false,
97            Self::CheckpointBackpressured => !cut_in_flight(),
98            Self::CommitBackpressured => true,
99        }
100    }
101}
102
103/// Push wake plus bounded KV fallback for clustered checkpoint control.
104#[cfg(feature = "cluster")]
105#[doc(hidden)]
106pub struct CheckpointControlWake {
107    announcements: Option<
108        tokio::sync::watch::Receiver<Option<laminar_core::cluster::control::BarrierAnnouncement>>,
109    >,
110    fallback: Duration,
111}
112
113#[cfg(not(feature = "cluster"))]
114#[doc(hidden)]
115pub struct CheckpointControlWake {
116    _private: (),
117}
118
119#[cfg(feature = "cluster")]
120impl CheckpointControlWake {
121    #[must_use]
122    pub(crate) fn new(
123        announcements: Option<
124            tokio::sync::watch::Receiver<
125                Option<laminar_core::cluster::control::BarrierAnnouncement>,
126            >,
127        >,
128    ) -> Self {
129        let fallback = if announcements.is_some() {
130            CHECKPOINT_CONTROL_DIRECT_FALLBACK
131        } else {
132            CHECKPOINT_CONTROL_POLL_FALLBACK
133        };
134        Self {
135            announcements,
136            fallback,
137        }
138    }
139
140    pub(crate) async fn wait_until(&mut self, fallback_at: tokio::time::Instant) {
141        let Some(announcements) = self.announcements.as_mut() else {
142            tokio::time::sleep_until(fallback_at).await;
143            return;
144        };
145        tokio::select! {
146            biased;
147            changed = announcements.changed() => {
148                if changed.is_err() {
149                    self.announcements = None;
150                    self.fallback = CHECKPOINT_CONTROL_POLL_FALLBACK;
151                }
152            }
153            () = tokio::time::sleep_until(fallback_at) => {}
154        }
155    }
156
157    #[must_use]
158    pub(crate) const fn fallback(&self) -> Duration {
159        self.fallback
160    }
161
162    #[must_use]
163    pub(crate) const fn capacity_retry() -> Duration {
164        CHECKPOINT_CONTROL_POLL_FALLBACK
165    }
166}
167
168#[cfg(not(feature = "cluster"))]
169impl CheckpointControlWake {
170    pub(crate) async fn wait_until(&mut self, fallback_at: tokio::time::Instant) {
171        tokio::time::sleep_until(fallback_at).await;
172    }
173
174    #[must_use]
175    pub(crate) const fn fallback() -> Duration {
176        CHECKPOINT_CONTROL_POLL_FALLBACK
177    }
178
179    #[must_use]
180    pub(crate) const fn capacity_retry() -> Duration {
181        CHECKPOINT_CONTROL_POLL_FALLBACK
182    }
183}
184
185/// Retained source-task command for an exact barrier attempt.
186///
187/// A release is identity-bound so an old checkpoint cannot resume a source held at a newer cut.
188/// `Stop` lets shutdown terminate a held source without briefly reopening data intake.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub(crate) enum SourceBarrierSignal {
191    Release(CheckpointAttempt),
192    Stop,
193}
194
195/// Coordinator/callback control for one source's exact barrier command and hold.
196#[derive(Clone)]
197#[doc(hidden)]
198pub struct SourceBarrierControl {
199    injector: CheckpointBarrierInjector,
200    release_tx: tokio::sync::watch::Sender<Option<SourceBarrierSignal>>,
201}
202
203impl SourceBarrierControl {
204    pub(crate) fn new(
205        injector: CheckpointBarrierInjector,
206        release_tx: tokio::sync::watch::Sender<Option<SourceBarrierSignal>>,
207    ) -> Self {
208        Self {
209            injector,
210            release_tx,
211        }
212    }
213
214    #[cfg(feature = "cluster")]
215    pub(crate) fn can_trigger(&self) -> bool {
216        self.injector.can_trigger()
217    }
218
219    #[cfg(feature = "cluster")]
220    pub(crate) fn trigger(&self, barrier: CheckpointBarrier) -> bool {
221        self.injector.trigger(barrier)
222    }
223
224    /// Remove this exact command if it has not been claimed by the source yet.
225    pub(crate) fn cancel_exact(&self, barrier: CheckpointBarrier) -> bool {
226        self.injector.cancel_exact(barrier)
227    }
228
229    /// Release a source that has already emitted this exact attempt's barrier.
230    pub(crate) fn release_exact(&self, attempt: CheckpointAttempt) {
231        if !attempt.is_canonical() {
232            return;
233        }
234        self.release_tx.send_if_modified(|signal| match *signal {
235            Some(SourceBarrierSignal::Stop) => false,
236            Some(SourceBarrierSignal::Release(released)) => match released.relation_to(attempt) {
237                CheckpointAttemptRelation::Older => {
238                    *signal = Some(SourceBarrierSignal::Release(attempt));
239                    true
240                }
241                // Preserve exact/newer releases. Conflicting dimensions are equivocation and
242                // deliberately cannot overwrite or authorize either exact hold.
243                CheckpointAttemptRelation::Exact
244                | CheckpointAttemptRelation::Newer
245                | CheckpointAttemptRelation::Conflict => false,
246            },
247            None => {
248                *signal = Some(SourceBarrierSignal::Release(attempt));
249                true
250            }
251        });
252    }
253
254    pub(crate) fn stop_hold(&self) {
255        let _ = self.release_tx.send(Some(SourceBarrierSignal::Stop));
256    }
257}
258
259#[cfg(test)]
260mod source_barrier_tests {
261    use super::*;
262
263    #[test]
264    fn noncanonical_release_cannot_publish_or_advance_source_release() {
265        let injector = CheckpointBarrierInjector::new();
266        let (release_tx, release_rx) = tokio::sync::watch::channel(None);
267        let control = SourceBarrierControl::new(injector, release_tx);
268        let invalid = CheckpointAttempt::new(6, 7);
269
270        control.release_exact(invalid);
271        assert_eq!(*release_rx.borrow(), None);
272
273        let canonical = CheckpointAttempt::canonical(5);
274        control.release_exact(canonical);
275        assert_eq!(
276            *release_rx.borrow(),
277            Some(SourceBarrierSignal::Release(canonical))
278        );
279
280        control.release_exact(invalid);
281        assert_eq!(
282            *release_rx.borrow(),
283            Some(SourceBarrierSignal::Release(canonical))
284        );
285    }
286}
287
288/// Why a barrier checkpoint was deliberately skipped, as opposed to
289/// attempted-and-failed.
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub enum SkipReason {
292    /// A sink write timed out; skip to keep the replay window intact.
293    PreservingReplayWindowAfterSinkTimeout,
294}
295
296impl std::fmt::Display for SkipReason {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        f.write_str(match self {
299            SkipReason::PreservingReplayWindowAfterSinkTimeout => {
300                "preserving_replay_window_after_sink_timeout"
301            }
302        })
303    }
304}
305
306/// Outcome of a barrier-aligned checkpoint attempt.
307#[derive(Debug)]
308pub enum BarrierOutcome {
309    /// Checkpoint committed at the given epoch.
310    Committed(u64),
311    /// Checkpoint is processing asynchronously in the background.
312    Async,
313    /// Deliberately skipped (see `SkipReason`).
314    Skipped(SkipReason),
315    /// Topology authority closed before state capture; retry after a stable assignment.
316    CancelledBeforeCapture,
317    /// The exact attempt was terminated by authoritative cluster control before capture.
318    Aborted,
319    /// Attempted and failed; retry on the next interval.
320    Failed,
321}
322
323/// Durable completion of one exact checkpoint attempt.
324///
325/// The checkpoint ID cannot be reconstructed from the execution epoch: durable ID
326/// reservations may be burned when an earlier attempt is abandoned. Keeping the exact
327/// attempt on the completion channel prevents downstream barriers and source commits from
328/// being attributed to a different checkpoint timeline.
329#[derive(Debug)]
330pub(crate) enum CheckpointCompletion {
331    /// The exact attempt reached its durable commit point.
332    Committed {
333        /// Exact attempt admitted before capture.
334        attempt: CheckpointAttempt,
335        /// Coordinator result for the same attempt.
336        result: crate::checkpoint_coordinator::CheckpointResult,
337        /// Per-source positions persisted by that exact attempt.
338        source_checkpoints: FxHashMap<String, SourceCheckpoint>,
339        /// The committed handoff cut retained aligned replay and is not yet the final cut.
340        handoff_replay_pending: bool,
341    },
342    /// The admitted attempt terminated without a durable commit.
343    Failed {
344        /// Exact attempt that failed.
345        attempt: CheckpointAttempt,
346        /// Stable user-facing failure reason.
347        error: String,
348    },
349}
350
351/// Result of servicing authoritative cluster checkpoint control on a follower.
352///
353/// Exact attempt identity lets the coordinator adopt a leader-started checkpoint for state
354/// pressure without mistaking a stale completion for the current baseline.
355#[doc(hidden)]
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub enum CheckpointControlOutcome {
358    /// No new authoritative checkpoint command was observed.
359    Idle,
360    /// Follower admission failed before an exact leader-prepared attempt could be identified.
361    AdmissionFailed { error: String },
362    /// The exact leader-prepared attempt was admitted locally.
363    Started {
364        attempt: CheckpointAttempt,
365        captured: bool,
366        flags: u64,
367    },
368    /// The exact leader-prepared attempt was authoritatively aborted before capture.
369    Aborted { attempt: CheckpointAttempt },
370    /// The exact attempt was cleanly rejected after its shuffle scope closed before capture.
371    Cancelled { attempt: CheckpointAttempt },
372    /// The exact leader-prepared attempt was rejected before it could remain in flight.
373    Failed {
374        attempt: CheckpointAttempt,
375        error: String,
376    },
377}
378
379/// Assignment gate result before a checkpoint attempt ID is reserved.
380#[doc(hidden)]
381#[derive(Debug)]
382pub enum CheckpointAssignmentAdmission {
383    /// Admission may continue with a local cut or the exact certified cluster assignment.
384    Ready {
385        assignment_fence: Option<CheckpointAssignmentFence>,
386        flags: u64,
387        /// Retains the cluster assignment linearization boundary until the exact Prepare and
388        /// source-barrier commands are installed. `None` outside a clustered runtime.
389        assignment_guard: Option<tokio::sync::OwnedMutexGuard<()>>,
390    },
391    /// Topology is transitioning; retry later without faulting or reserving an attempt.
392    Deferred(String),
393    /// Assignment authority is invalid or unavailable and the pipeline must fail closed.
394    Fault(String),
395}
396
397impl CheckpointCompletion {
398    /// Create a completion for a path whose exact attempt is already authoritative.
399    #[cfg(any(feature = "cluster", test))]
400    #[must_use]
401    pub(crate) fn new(
402        attempt: CheckpointAttempt,
403        source_checkpoints: FxHashMap<String, SourceCheckpoint>,
404        handoff_replay_pending: bool,
405    ) -> Self {
406        Self::Committed {
407            attempt,
408            result: crate::checkpoint_coordinator::CheckpointResult {
409                success: true,
410                checkpoint_id: attempt.checkpoint_id,
411                epoch: attempt.epoch,
412                duration: std::time::Duration::ZERO,
413                error: None,
414                failure_disposition: None,
415            },
416            source_checkpoints,
417            handoff_replay_pending,
418        }
419    }
420
421    /// Build a completion only when the coordinator result belongs to the admitted attempt.
422    pub(crate) fn validated(
423        admitted: CheckpointAttempt,
424        result: crate::checkpoint_coordinator::CheckpointResult,
425        source_checkpoints: FxHashMap<String, SourceCheckpoint>,
426        handoff_replay_pending: bool,
427    ) -> Result<Self, String> {
428        let completed = CheckpointAttempt::new(result.epoch, result.checkpoint_id);
429        if completed != admitted {
430            return Err(format!(
431                "checkpoint completion identity mismatch: admitted epoch={} id={}, \
432                 coordinator completed epoch={} id={}",
433                admitted.epoch, admitted.checkpoint_id, completed.epoch, completed.checkpoint_id,
434            ));
435        }
436        // COMPAT: the fault-soak evidence scrapes this exact line as the durable commit signal
437        // at the single validation chokepoint a committed leader completion passes through.
438        tracing::info!(
439            checkpoint_id = admitted.checkpoint_id,
440            epoch = admitted.epoch,
441            "checkpoint completed"
442        );
443        Ok(Self::Committed {
444            attempt: admitted,
445            result,
446            source_checkpoints,
447            handoff_replay_pending,
448        })
449    }
450
451    /// Create a terminal failure for an already-admitted exact attempt.
452    pub(crate) fn failed(attempt: CheckpointAttempt, error: impl Into<String>) -> Self {
453        Self::Failed {
454            attempt,
455            error: error.into(),
456        }
457    }
458
459    /// Exact attempt that reached a terminal outcome.
460    #[must_use]
461    pub(crate) const fn attempt(&self) -> CheckpointAttempt {
462        match self {
463            Self::Committed { attempt, .. } | Self::Failed { attempt, .. } => *attempt,
464        }
465    }
466}
467
468/// How a failed `execute_cycle` should be handled by the coordinator.
469#[derive(Debug, thiserror::Error)]
470pub enum CycleError {
471    /// Non-deferrable error: `ExactlyOnce` recovers from checkpoint, `AtLeastOnce` drops it.
472    #[error("{0}")]
473    Fatal(String),
474    /// Shared runtime infrastructure failed; all delivery modes recover from a durable cut.
475    #[error("{0}")]
476    Recovery(String),
477    /// `backpressure_policy=Fail` (shutdown already signaled); stop, don't recover.
478    #[error("{0}")]
479    Halt(String),
480}
481
482/// Result of a pipeline cycle. Per-domain failure isolation lets healthy failure domains
483/// commit and advance while a faulted domain's sources are held back for replay.
484pub struct CycleOutcome {
485    /// Output of the domains that succeeded this cycle.
486    pub results: FxHashMap<Arc<str>, Vec<RecordBatch>>,
487    /// At least one failure domain faulted (it may have no *local* source, e.g. a cluster
488    /// follower reading a remote shuffle), so this can be set with `failed_sources` empty.
489    pub any_failed: bool,
490    /// Names of sources whose domain faulted; the coordinator must not commit their offsets.
491    pub failed_sources: FxHashSet<Arc<str>>,
492    /// At least one operator retained work for an exact local retry. This may be set with
493    /// `deferred_sources` empty on a cluster worker processing remote shuffle input.
494    pub any_deferred: bool,
495    /// Names of local sources whose input is retained in the graph. Their staged cursors must
496    /// remain uncommitted until a later cycle consumes all retained work.
497    pub deferred_sources: FxHashSet<Arc<str>>,
498}
499
500impl CycleOutcome {
501    /// A fully-successful cycle: every domain committed.
502    #[must_use]
503    pub fn clean(results: FxHashMap<Arc<str>, Vec<RecordBatch>>) -> Self {
504        Self {
505            results,
506            any_failed: false,
507            failed_sources: FxHashSet::default(),
508            any_deferred: false,
509            deferred_sources: FxHashSet::default(),
510        }
511    }
512}
513
514/// A registered source with its name and config.
515pub struct SourceRegistration {
516    /// Source name.
517    pub name: String,
518    /// The connector (owned).
519    pub connector: Box<dyn SourceConnector>,
520    /// Connector config included in the atomic startup request.
521    pub config: ConnectorConfig,
522    /// The runtime installed cluster vnode ownership for this source instance.
523    ///
524    /// This is an engine-owned admission fact, not a connector capability switch.
525    pub assignment_scoped: bool,
526    /// Exact position to install atomically when the source starts.
527    pub position: SourcePosition,
528}
529
530/// Callback trait for the coordinator to interact with the rest of the DB.
531/// Trait exists for test seam; production impl is `ConnectorPipelineCallback`.
532#[trait_variant::make(Send)]
533pub trait PipelineCallback: Send + 'static {
534    /// Install any newly published recovery cut before the coordinator removes
535    /// another source message from its FIFO. The default is a no-op outside a
536    /// clustered source-handoff runtime.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if a pending recovery cut cannot be installed.
541    fn prepare_source_intake(&mut self) -> Result<(), String> {
542        Ok(())
543    }
544
545    /// Pin decision-bound source frontiers before a new source-admission cycle begins.
546    ///
547    /// The coordinator does not call this while graph-owned deferred input is replaying, so one
548    /// snapshot covers source filtering, initial execution, and every retained replay pass.
549    ///
550    /// # Errors
551    /// Returns an error when the frontier snapshot cannot be installed.
552    fn pin_source_frontiers_for_new_cycle(&mut self) -> Result<(), String> {
553        Ok(())
554    }
555
556    /// Execute a SQL cycle over the accumulated source batches. `Err` is a whole-cycle
557    /// failure (all domains, or a backpressure halt); per-domain faults surface in
558    /// [`CycleOutcome`] so healthy domains still commit.
559    async fn execute_cycle(
560        &mut self,
561        source_batches: &FxHashMap<Arc<str>, Vec<RecordBatch>>,
562        watermark: i64,
563    ) -> Result<CycleOutcome, CycleError>;
564
565    /// Complete assignment-scoped vnode lifecycle work without admitting source input.
566    ///
567    /// Recovery can close intake before a predecessor transition has run. Implementations return
568    /// `true` only when pending work completed; `false` means idle or waiting for its exact
569    /// assignment certificate.
570    async fn complete_pending_vnode_transition(&mut self) -> Result<bool, CycleError>;
571
572    /// Drain every graph input that belongs to the frozen checkpoint cut.
573    ///
574    /// Implementations must deliver each drain pass's outputs before returning. The returned
575    /// future is one owner transaction: its caller must await an explicit result or destroy the
576    /// complete callback/coordinator generation. It must not drop the future while retaining that
577    /// generation. Operators may temporarily own graph input across an await, and a completed graph
578    /// pass may have partially published materialized-view, stream, or sink output.
579    ///
580    /// An implementation may cancel a nested operation for its absolute deadline or process lease
581    /// only when it regains control and records or returns a disposition that prevents checkpoint
582    /// capture from passing incomplete publication. A future caller that needs an outer timeout,
583    /// `select!`, or task abort must first add a coordinator-owned attempt transaction covering
584    /// graph/output publication, source-barrier ownership, and attempt cleanup. The current
585    /// implementation checks its absolute deadline between complete graph passes and inside
586    /// sink-publication awaits whose outcomes are consumed before capture.
587    async fn drain_checkpoint_edges_until(
588        &mut self,
589        deadline: tokio::time::Instant,
590    ) -> Result<(), CycleError>;
591
592    /// Push cycle results to stream subscriptions.
593    ///
594    /// # Errors
595    ///
596    /// Returns an error if subscription delivery rejects the cycle output.
597    fn push_to_streams(
598        &self,
599        results: &FxHashMap<Arc<str>, Vec<RecordBatch>>,
600    ) -> Result<(), CycleError> {
601        let _ = results;
602        Ok(())
603    }
604
605    /// Publish aggregate output bookkeeping after every cycle consumer accepted the output.
606    #[cfg(feature = "cluster")]
607    fn commit_subscription_output(&mut self) {}
608
609    /// Restore retryable aggregate bookkeeping after cycle publication fails.
610    #[cfg(feature = "cluster")]
611    fn abort_subscription_output(&mut self) {}
612
613    /// Update materialized view stores with cycle results.
614    ///
615    /// # Errors
616    ///
617    /// Returns an error if materialized state cannot apply the cycle output.
618    fn update_mv_stores(
619        &self,
620        results: &FxHashMap<Arc<str>, Vec<RecordBatch>>,
621    ) -> Result<(), CycleError> {
622        let _ = results;
623        Ok(())
624    }
625
626    /// Write cycle results to sinks, bounded by `deadline` when this is a checkpoint drain.
627    async fn write_to_sinks(
628        &mut self,
629        results: &FxHashMap<Arc<str>, Vec<RecordBatch>>,
630        deadline: Option<tokio::time::Instant>,
631    ) -> Result<(), CycleError>;
632
633    /// Extract watermark from a batch for a given source.
634    ///
635    /// # Errors
636    ///
637    /// Returns an error when the source batch has invalid event-time metadata.
638    fn extract_watermark(
639        &mut self,
640        source_name: &str,
641        batch: &RecordBatch,
642        admission_floor: i64,
643    ) -> Result<(), CycleError>;
644
645    /// Install the exact input-channel inventory carried by a source cursor.
646    ///
647    /// # Errors
648    ///
649    /// Returns an error when the inventory is invalid or conflicts with recovered progress.
650    fn reconcile_source_input_channels(
651        &mut self,
652        source_name: &str,
653        input_channels: Option<Arc<[Vec<u8>]>>,
654    ) -> Result<(), CycleError>;
655
656    /// Filter late rows while preserving any validated hidden source metadata.
657    ///
658    /// # Errors
659    ///
660    /// Returns an error when hidden metadata is malformed or the batch cannot be filtered.
661    fn filter_late_rows(
662        &self,
663        source_name: &str,
664        batch: &RecordBatch,
665    ) -> Result<Option<RecordBatch>, CycleError>;
666
667    /// Current pipeline watermark.
668    fn current_watermark(&self) -> i64;
669
670    /// `true` if this node is the cluster leader, or in single-node mode.
671    fn is_leader(&self) -> bool {
672        true
673    }
674
675    /// `true` while a coordinated restart is in flight; the checkpoint admission gate holds.
676    /// Default `false`.
677    fn is_recovering(&mut self) -> bool {
678        false
679    }
680
681    /// `true` while a local topology submission or assignment drain waits for a gap between
682    /// checkpoint cuts; periodic admission yields and manual requests proceed. Default `false`.
683    fn periodic_checkpoints_held(&self) -> bool {
684        false
685    }
686
687    /// `true` if a fatal cycle error should fault for recovery rather than drop-and-continue
688    /// (exactly-once, or coordinated recovery). Default `false` (at-least-once drops).
689    fn fault_on_cycle_error(&self) -> bool {
690        false
691    }
692
693    /// Take a pending deterministic pipeline halt. A halt is permanent for the current
694    /// deployment and therefore takes precedence over consistency faults that would otherwise
695    /// trigger recovery of the same poison input.
696    fn take_pipeline_halt(&mut self) -> Option<String> {
697        None
698    }
699
700    /// Take a pending consistency fault from checkpointing or a poisoned sink epoch. The
701    /// coordinator stops intake so recovery can replay from the last committed cut.
702    fn take_pipeline_fault(&mut self) -> Option<String> {
703        None
704    }
705
706    /// Cancel checkpoint tails only after coordinated recovery owns the lifecycle fence.
707    /// Returns whether that fence was observed, so shutdown need not repeat cancellation.
708    fn cancel_checkpoint_tails_for_recovery(&mut self) -> bool {
709        false
710    }
711
712    /// Record a checkpoint failure observed by the coordinator. Exactly-once implementations
713    /// fault for recovery; weaker guarantees may retain retry-on-next-interval behaviour.
714    fn record_checkpoint_failure(&mut self, _checkpoint_id: u64, _reason: &str) {}
715
716    /// Record an invariant failure after the checkpoint itself became durable.
717    fn record_checkpoint_continuation_fault(&mut self, _attempt: CheckpointAttempt, _reason: &str) {
718    }
719
720    /// Record a failure before an exact checkpoint attempt could be reserved.
721    fn record_checkpoint_admission_failure(&mut self, _reason: &str) {}
722
723    /// Join tracked asynchronous checkpoint tails before connector teardown.
724    fn settle_checkpoint_tail_tasks(
725        &mut self,
726    ) -> impl std::future::Future<Output = Result<(), String>> + Send {
727        std::future::ready(Ok(()))
728    }
729
730    /// Durably reserve the exact attempt before barriers are admitted to sources.
731    ///
732    /// Implementations must never synthesize an in-memory checkpoint ID. A successful
733    /// reservation may be abandoned, but its ID is permanently burned.
734    fn reserve_checkpoint_attempt(
735        &mut self,
736        _deadline: tokio::time::Instant,
737    ) -> impl std::future::Future<Output = Result<CheckpointAttempt, String>> + Send {
738        std::future::ready(Err(
739            "checkpoint coordinator has no durable attempt allocator".into(),
740        ))
741    }
742
743    /// Durably admit exact checkpoint artifacts, then publish the certified cluster `Prepare`,
744    /// before any source or shuffle barrier is injected.
745    fn publish_checkpoint_prepare(
746        &mut self,
747        _attempt: CheckpointAttempt,
748        _attempt_started: std::time::Instant,
749        _deadline: tokio::time::Instant,
750        _flags: u64,
751        _assignment_fence: Option<CheckpointAssignmentFence>,
752    ) -> impl std::future::Future<Output = Result<(), String>> + Send {
753        std::future::ready(Ok(()))
754    }
755
756    /// Abandon a reserved attempt that cannot reach the durable checkpoint tail.
757    /// Transactional sinks must roll back that epoch before the next attempt begins.
758    fn abandon_checkpoint_attempt(
759        &mut self,
760        _attempt: CheckpointAttempt,
761        _reason: &str,
762        _flags: u64,
763        _assignment_fence: Option<CheckpointAssignmentFence>,
764    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
765
766    /// Cancel an exact follower source-barrier attempt before capture.
767    ///
768    /// An attempt admitted with follower ownership releases its exact local reservation and
769    /// publishes a negative barrier acknowledgement, even if this process changes role while the
770    /// attempt is active. Originator-owned attempts use `abandon_checkpoint_attempt`.
771    fn cancel_source_barrier_attempt(
772        &mut self,
773        _attempt: CheckpointAttempt,
774        _reason: &str,
775    ) -> impl std::future::Future<Output = Result<(), String>> + Send;
776
777    /// Resolve exact local follower state after an authoritative pre-capture
778    /// [`BarrierOutcome::Aborted`]. This operation must not publish control traffic or wait on the
779    /// network because cluster authority has already terminated the attempt.
780    ///
781    /// # Errors
782    ///
783    /// Returns an error if local follower state cannot be resolved exactly.
784    fn resolve_authoritative_follower_abort(
785        &mut self,
786        _attempt: CheckpointAttempt,
787    ) -> Result<(), String> {
788        Err("authoritative follower Abort cleanup is not implemented by this callback".into())
789    }
790
791    /// Capture the exact assignment certificate for a new attempt.
792    fn checkpoint_assignment_for_admission(
793        &mut self,
794        _deadline: tokio::time::Instant,
795    ) -> impl std::future::Future<Output = CheckpointAssignmentAdmission> + Send {
796        std::future::ready(CheckpointAssignmentAdmission::Ready {
797            assignment_fence: None,
798            flags: laminar_core::checkpoint::flags::NONE,
799            assignment_guard: None,
800        })
801    }
802
803    /// Wake the coordinator for leader-originated checkpoint control. `None` keeps local
804    /// runtimes free of cluster polling.
805    fn checkpoint_control_wake(&self) -> Option<CheckpointControlWake> {
806        None
807    }
808
809    /// Wake the coordinator when deferred external operator work becomes ready.
810    fn shuffle_work_wake(&self) -> Option<Arc<tokio::sync::Notify>> {
811        None
812    }
813
814    /// Demote sources idle past their timeout so a quiet input doesn't pin the combined watermark.
815    fn tick_idle_watermark(&mut self) {}
816
817    /// Service a cluster follower announcement observed from the leader.
818    ///
819    /// Periodic, manual, and shutdown admission belongs exclusively to the streaming coordinator.
820    /// This control seam must never originate a local checkpoint.
821    async fn service_checkpoint_control(
822        &mut self,
823        source_offsets: FxHashMap<String, SourceCheckpoint>,
824    ) -> CheckpointControlOutcome;
825
826    /// Called when all sources have aligned on a barrier.
827    async fn checkpoint_with_barrier(
828        &mut self,
829        source_checkpoints: FxHashMap<String, SourceCheckpoint>,
830        attempt: CheckpointAttempt,
831        attempt_started: std::time::Instant,
832        deadline: tokio::time::Instant,
833        flags: u64,
834        assignment_fence: Option<CheckpointAssignmentFence>,
835    ) -> BarrierOutcome;
836
837    /// Record cycle metrics.
838    fn record_cycle(&self, events_ingested: u64, batches: u64, elapsed_ns: u64);
839
840    /// Record the three timed phases of one successfully published normal cycle. Checkpoint graph
841    /// drains use a separate execution path and must not call this hook.
842    fn record_cycle_phases(&self, _execute_ns: u64, _output_store_ns: u64, _sink_enqueue_ns: u64) {}
843
844    /// Count a fatal cycle error that was dropped-and-continued (at-least-once only).
845    fn note_cycle_error(&self) {}
846
847    /// Apply a DDL control message (add/drop stream) to the running pipeline.
848    fn apply_control(&mut self, msg: super::ControlMsg);
849
850    /// Room left on the graph ports that `source`'s batches are primed into.
851    fn source_port_headroom(&self, _source: &str) -> PortHeadroom {
852        PortHeadroom::UNBOUNDED
853    }
854
855    /// Count a cycle whose source intake waited for graph port headroom.
856    fn note_source_backpressure(&self) {}
857
858    /// Pressure from bounded checkpoint-committable output. A due checkpoint accelerates the next
859    /// periodic cut without stopping compute. Checkpoint and commit backpressure keep checkpoint
860    /// completion and control traffic live but stop source and deferred-input work until the next
861    /// checkpoint's barriers are in flight or the prepared cut resolves.
862    fn external_output_pressure(&self) -> ExternalOutputPressure {
863        ExternalOutputPressure::Normal
864    }
865
866    /// `true` while the runtime must not fold source or shuffle input into operator state.
867    /// Cluster startup and coordinated recovery use this stronger fence; ordinary backpressure
868    /// only pauses source polling.
869    fn intake_paused(&self) -> bool {
870        false
871    }
872
873    /// `true` when deferred operators have pending input to drain.
874    fn has_deferred_input(&self) -> bool {
875        false
876    }
877
878    /// `true` when retained work can run now without waiting for an external wake.
879    fn has_runnable_deferred_input(&self) -> bool {
880        self.has_deferred_input()
881    }
882
883    /// Reserve each subscription log's cursor at the aligned checkpoint cut.
884    ///
885    /// # Errors
886    ///
887    /// Returns an error if the subscription cut cannot be reserved atomically.
888    fn reserve_subscription_cut(&mut self, _attempt: CheckpointAttempt) -> Result<(), String> {
889        Ok(())
890    }
891
892    /// Discard an unresolved subscription cut after checkpoint failure.
893    fn abort_subscription_cut(&mut self, _attempt: CheckpointAttempt) {}
894
895    /// Resolve the exact cut for external SUBSCRIBE consumers after durable commit.
896    ///
897    /// # Errors
898    ///
899    /// Returns an error if the committed cut cannot be published atomically.
900    fn publish_barrier(&mut self, _attempt: CheckpointAttempt) -> Result<(), String> {
901        Ok(())
902    }
903
904    /// Terminate provisional subscription delivery before shutdown or recovery replay.
905    fn invalidate_subscriptions(&self, _reason: &str) {}
906
907    /// Resolve durable ownership of any open checkpoint-committable sink epoch while its actor is
908    /// still live. A failure must leave the actors open so lifecycle teardown can retry.
909    fn settle_sink_epoch_for_shutdown(
910        &mut self,
911    ) -> impl std::future::Future<Output = Result<(), String>> + Send {
912        std::future::ready(Ok(()))
913    }
914
915    /// Gracefully close sinks on shutdown (abort open transactions, flush) so a restart
916    /// re-initialises cleanly. Every sink must be attempted; the result aggregates failures.
917    fn close_sinks(&mut self) -> impl std::future::Future<Output = Result<(), String>> + Send {
918        std::future::ready(Ok(()))
919    }
920
921    /// Register the local source barrier injectors.
922    fn set_barrier_injectors(&mut self, injectors: Vec<SourceBarrierControl>) {
923        let _ = injectors;
924    }
925}
926
927#[cfg(all(test, feature = "cluster"))]
928mod tests {
929    use super::*;
930
931    async fn assert_quiet_wake_at(wake: &mut CheckpointControlWake, delay: Duration) {
932        let deadline = tokio::time::Instant::now() + delay;
933        assert!(tokio::time::timeout(
934            delay.saturating_sub(Duration::from_millis(1)),
935            wake.wait_until(deadline),
936        )
937        .await
938        .is_err());
939        tokio::time::timeout(Duration::from_millis(2), wake.wait_until(deadline))
940            .await
941            .expect("checkpoint control fallback exceeded its configured deadline");
942    }
943
944    #[tokio::test(start_paused = true)]
945    async fn checkpoint_control_wake_uses_bounded_direct_and_poll_fallbacks() {
946        let (_direct_tx, direct_rx) = tokio::sync::watch::channel(None);
947        let mut direct = CheckpointControlWake::new(Some(direct_rx));
948        assert_eq!(direct.fallback(), Duration::from_millis(250));
949        assert_quiet_wake_at(&mut direct, Duration::from_millis(250)).await;
950
951        let mut poll = CheckpointControlWake::new(None);
952        assert_eq!(poll.fallback(), Duration::from_millis(25));
953        assert_quiet_wake_at(&mut poll, Duration::from_millis(25)).await;
954    }
955
956    #[tokio::test(start_paused = true)]
957    async fn checkpoint_control_wake_degrades_when_direct_delivery_closes() {
958        let (direct_tx, direct_rx) = tokio::sync::watch::channel(None);
959        let mut wake = CheckpointControlWake::new(Some(direct_rx));
960        drop(direct_tx);
961
962        wake.wait_until(tokio::time::Instant::now() + Duration::from_millis(250))
963            .await;
964        assert_eq!(wake.fallback(), Duration::from_millis(25));
965        assert_quiet_wake_at(&mut wake, Duration::from_millis(25)).await;
966    }
967}