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