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}