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}