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