Skip to main content

laminar_db/pipeline/
streaming_coordinator.rs

1//! Single-task pipeline coordinator on the dedicated `laminar-compute` thread.
2//!
3//! ```text
4//! Source task (main runtime) ──MAsyncTx──► StreamingCoordinator
5//!                                               │  execute_cycle / write_to_sinks
6//!                                               ▼  Sinks
7//! ```
8
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use arrow_array::RecordBatch;
14use crossfire::{mpsc, AsyncRx, MAsyncTx};
15use laminar_connectors::checkpoint::SourceCheckpoint;
16use laminar_connectors::connector::SourceBatch;
17use laminar_connectors::connector::{
18    ConnectorCancellationPolicy, ConnectorTaskTracker, DeliveryGuarantee, SourceConnector,
19    SourcePosition, SourceStart,
20};
21#[cfg(feature = "cluster")]
22use laminar_connectors::connector::{
23    SourceDrainOutcome, SourceDrainRequest, SourceDrainResolution,
24};
25use laminar_connectors::error::ConnectorError;
26#[cfg(feature = "cluster")]
27use laminar_core::checkpoint::{
28    AssignmentDrainId, AssignmentDrainTransition, CheckpointParticipant,
29};
30use laminar_core::checkpoint::{CheckpointBarrier, CheckpointBarrierInjector};
31#[cfg(feature = "cluster")]
32use laminar_core::cluster::control::ClusterController;
33use laminar_core::state::{CheckpointAttempt, CheckpointAttemptRelation};
34use rustc_hash::{FxHashMap, FxHashSet};
35
36use super::callback::{
37    BarrierOutcome, CheckpointAssignmentAdmission, CheckpointCompletion, CheckpointControlOutcome,
38    CheckpointControlWake, CycleError, CycleOutcome, PipelineCallback, SourceBarrierControl,
39    SourceBarrierSignal, SourceRegistration,
40};
41#[cfg(test)]
42use super::config::CheckpointSchedule;
43use super::config::PipelineConfig;
44use crate::connector_task_fence::{ConnectorTaskFenceRegistration, OwnedConnectorTaskFences};
45use crate::error::DbError;
46
47type SourceMsgRx = AsyncRx<mpsc::Array<SourceMsg>>;
48type SourceMsgTx = MAsyncTx<mpsc::Array<SourceMsg>>;
49type ControlMsgRx = AsyncRx<mpsc::Array<super::ControlMsg>>;
50type ForceCheckpointReply = crate::db::ForceCheckpointReply;
51
52#[cfg(feature = "cluster")]
53struct SourceProcessAuthority {
54    controller: Arc<ClusterController>,
55    lost: tokio_util::sync::CancellationToken,
56    watcher_abort: Option<tokio::task::AbortHandle>,
57}
58
59#[cfg(feature = "cluster")]
60impl Drop for SourceProcessAuthority {
61    fn drop(&mut self) {
62        if let Some(watcher) = &self.watcher_abort {
63            watcher.abort();
64        }
65    }
66}
67
68/// Shared process-lease observation for every source task in one coordinator.
69///
70/// One watcher owns the deadline timer. Per-poll code selects only on the retained cancellation
71/// token and rechecks the controller's monotonic deadline at publication boundaries.
72#[cfg(feature = "cluster")]
73impl SourceProcessAuthority {
74    fn new(controller: Arc<ClusterController>) -> Arc<Self> {
75        let lost = tokio_util::sync::CancellationToken::new();
76        if !controller.process_lease_is_live() {
77            lost.cancel();
78        }
79        let watcher_abort = if lost.is_cancelled() {
80            None
81        } else {
82            let task_controller = Arc::clone(&controller);
83            let task_lost = lost.clone();
84            let watcher = tokio::spawn(async move {
85                task_controller.wait_for_process_lease_loss().await;
86                task_lost.cancel();
87            });
88            Some(watcher.abort_handle())
89        };
90        Arc::new(Self {
91            controller,
92            lost,
93            watcher_abort,
94        })
95    }
96
97    #[inline]
98    fn is_live(&self) -> bool {
99        !self.lost.is_cancelled() && self.controller.process_lease_is_live()
100    }
101
102    async fn cancelled(&self) {
103        self.lost.cancelled().await;
104    }
105}
106
107#[cfg(feature = "cluster")]
108#[inline]
109fn source_process_authority_is_live(authority: Option<&SourceProcessAuthority>) -> bool {
110    authority.is_none_or(SourceProcessAuthority::is_live)
111}
112
113/// Wait for coordinator batching or idle work without letting process-lease loss remain hidden
114/// behind the timer.
115async fn wait_coordinator_delay(
116    duration: Duration,
117    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
118) -> bool {
119    #[cfg(feature = "cluster")]
120    if let Some(authority) = process_authority {
121        return tokio::select! {
122            biased;
123            () = authority.cancelled() => true,
124            () = tokio::time::sleep(duration) => false,
125        };
126    }
127
128    tokio::time::sleep(duration).await;
129    false
130}
131
132/// Message from a source task to the coordinator; carries the [`SourceCheckpoint`]
133/// captured at production time so no offset is checkpointed for unprocessed data.
134enum SourceMsg {
135    Batch {
136        source_idx: usize,
137        batch: RecordBatch,
138        /// Committed to `committed_offsets` only after successful cycle publication.
139        checkpoint: SourceCheckpoint,
140    },
141    Barrier {
142        source_idx: usize,
143        barrier: CheckpointBarrier,
144        checkpoint: SourceCheckpoint,
145    },
146}
147
148/// Publish into the bounded source FIFO without allowing backpressure to hide terminal shutdown or
149/// process-lease loss. Process authority is revalidated after a successful send because its fence
150/// may cross while the queue wakes the producer.
151async fn send_source_msg(
152    tx: &SourceMsgTx,
153    msg: SourceMsg,
154    shutdown: &tokio::sync::Notify,
155    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
156) -> bool {
157    #[cfg(feature = "cluster")]
158    if let Some(authority) = process_authority {
159        if !authority.is_live() {
160            return false;
161        }
162        let sent = tokio::select! {
163            biased;
164            () = authority.cancelled() => false,
165            () = shutdown.notified() => false,
166            result = tx.send(msg) => result.is_ok(),
167        };
168        return sent && authority.is_live();
169    }
170
171    tokio::select! {
172        biased;
173        () = shutdown.notified() => false,
174        result = tx.send(msg) => result.is_ok(),
175    }
176}
177
178/// Fatal source-task control event. Faults use a dedicated unbounded side channel so they cannot
179/// sit behind a full data queue or be hidden while external-commit backpressure pauses data.
180struct SourceFault {
181    source: Arc<str>,
182    error: String,
183}
184
185/// Reports every source-task exit that was not initiated by coordinator shutdown, including a
186/// panic while polling connector code. `UnboundedSender::send` is synchronous and allocation-only,
187/// so this remains reliable from `Drop` during unwinding without blocking the data path.
188struct SourceTaskExitGuard {
189    source: Arc<str>,
190    expected_shutdown: Arc<AtomicBool>,
191    fault_tx: tokio::sync::mpsc::UnboundedSender<SourceFault>,
192}
193
194impl Drop for SourceTaskExitGuard {
195    fn drop(&mut self) {
196        if !self.expected_shutdown.load(Ordering::Acquire) {
197            let _ = self.fault_tx.send(SourceFault {
198                source: Arc::clone(&self.source),
199                error: "source task exited without coordinator shutdown".into(),
200            });
201        }
202    }
203}
204
205struct SourceActorTerminalState {
206    finished: AtomicBool,
207    notify: tokio::sync::Notify,
208}
209
210impl SourceActorTerminalState {
211    fn new() -> Self {
212        Self {
213            finished: AtomicBool::new(false),
214            notify: tokio::sync::Notify::new(),
215        }
216    }
217
218    fn finish(&self) {
219        if !self.finished.swap(true, Ordering::AcqRel) {
220            self.notify.notify_waiters();
221        }
222    }
223
224    fn is_finished(&self) -> bool {
225        self.finished.load(Ordering::Acquire)
226    }
227
228    async fn wait_finished(&self) {
229        loop {
230            let notified = self.notify.notified();
231            tokio::pin!(notified);
232            notified.as_mut().enable();
233            if self.is_finished() {
234                return;
235            }
236            notified.await;
237        }
238    }
239}
240
241#[cfg(test)]
242struct SourceActorLifetime(Arc<SourceActorTerminalState>);
243
244#[cfg(test)]
245impl Drop for SourceActorLifetime {
246    fn drop(&mut self) {
247        self.0.finish();
248    }
249}
250
251#[cfg(test)]
252fn source_actor_terminal_guard() -> (SourceActorLifetime, Arc<SourceActorTerminalState>) {
253    let terminal = Arc::new(SourceActorTerminalState::new());
254    (SourceActorLifetime(Arc::clone(&terminal)), terminal)
255}
256
257struct SourceActorFuture<F> {
258    actor: Option<std::pin::Pin<Box<F>>>,
259    terminal: Arc<SourceActorTerminalState>,
260}
261
262// Moving this wrapper never moves the separately pinned actor allocation.
263impl<F> Unpin for SourceActorFuture<F> {}
264
265impl<F> std::future::Future for SourceActorFuture<F>
266where
267    F: std::future::Future<Output = ()>,
268{
269    type Output = ();
270
271    fn poll(
272        mut self: std::pin::Pin<&mut Self>,
273        context: &mut std::task::Context<'_>,
274    ) -> std::task::Poll<Self::Output> {
275        let actor = self
276            .actor
277            .as_mut()
278            .expect("source actor polled after terminal completion");
279        if actor.as_mut().poll(context).is_pending() {
280            return std::task::Poll::Pending;
281        }
282        // Drop the complete actor future, including its connector, before publishing exit.
283        self.actor.take();
284        self.terminal.finish();
285        std::task::Poll::Ready(())
286    }
287}
288
289impl<F> Drop for SourceActorFuture<F> {
290    fn drop(&mut self) {
291        // Cancellation must drop the actor and its connector before another generation can observe
292        // terminal completion.
293        self.actor.take();
294        self.terminal.finish();
295    }
296}
297
298fn spawn_source_actor<F>(
299    runtime: &tokio::runtime::Handle,
300    actor: F,
301) -> (tokio::task::JoinHandle<()>, Arc<SourceActorTerminalState>)
302where
303    F: std::future::Future<Output = ()> + Send + 'static,
304{
305    let terminal = Arc::new(SourceActorTerminalState::new());
306    let join = runtime.spawn(SourceActorFuture {
307        actor: Some(Box::pin(actor)),
308        terminal: Arc::clone(&terminal),
309    });
310    (join, terminal)
311}
312
313#[derive(Clone)]
314enum SourceTaskOutcome {
315    Success,
316    Cancelled,
317    Failed(Arc<str>),
318}
319
320struct SourceTaskState {
321    name: Arc<str>,
322    shutdown: Arc<tokio::sync::Notify>,
323    expected_shutdown: Arc<AtomicBool>,
324    abort: tokio::task::AbortHandle,
325    actor_terminal: Arc<SourceActorTerminalState>,
326    terminal_tasks: Option<ConnectorTaskTracker>,
327    outcome: parking_lot::Mutex<Option<SourceTaskOutcome>>,
328    #[cfg(feature = "cluster")]
329    drain: parking_lot::Mutex<Option<SourceDrainLeaseControl>>,
330}
331
332#[cfg(feature = "cluster")]
333#[derive(Clone)]
334enum SourceDrainCommand {
335    Begin {
336        request: SourceDrainRequest,
337        participant: CheckpointParticipant,
338        deadline: tokio::time::Instant,
339    },
340    Resolve {
341        resolution: SourceDrainResolution,
342        deadline: tokio::time::Instant,
343    },
344}
345
346#[cfg(feature = "cluster")]
347#[derive(Clone, Debug)]
348enum SourceDrainTaskStatus {
349    Idle,
350    Pausing(AssignmentDrainId),
351    Ready(SourceDrainReceipt),
352    Resolved {
353        round: AssignmentDrainId,
354        outcome: SourceDrainOutcome,
355    },
356}
357
358#[cfg(feature = "cluster")]
359#[derive(Clone, Debug, PartialEq, Eq)]
360struct SourceDrainReceipt {
361    round: AssignmentDrainId,
362    participant: CheckpointParticipant,
363    source_task_incarnation: uuid::Uuid,
364}
365
366#[cfg(feature = "cluster")]
367impl SourceDrainReceipt {
368    fn is_canonical(&self) -> bool {
369        self.round.is_canonical()
370            && self.participant.node_id != 0
371            && !self.participant.boot_incarnation.is_nil()
372            && !self.source_task_incarnation.is_nil()
373    }
374}
375
376#[cfg(feature = "cluster")]
377fn validate_source_drain_receipts(
378    round: AssignmentDrainId,
379    participant: CheckpointParticipant,
380    receipts: &[SourceDrainReceipt],
381) -> Result<(), String> {
382    if receipts.iter().any(|receipt| {
383        !receipt.is_canonical() || receipt.round != round || receipt.participant != participant
384    }) {
385        return Err("source drain contains a stale or non-canonical task receipt".into());
386    }
387    let mut task_incarnations: Vec<uuid::Uuid> = receipts
388        .iter()
389        .map(|receipt| receipt.source_task_incarnation)
390        .collect();
391    task_incarnations.sort_unstable();
392    if task_incarnations.windows(2).any(|pair| pair[0] == pair[1]) {
393        return Err("source drain contains duplicate task receipts".into());
394    }
395    Ok(())
396}
397
398#[cfg(feature = "cluster")]
399#[derive(Clone)]
400struct SourceDrainLeaseControl {
401    task_incarnation: uuid::Uuid,
402    command_tx: tokio::sync::watch::Sender<Option<SourceDrainCommand>>,
403    status_tx: tokio::sync::watch::Sender<SourceDrainTaskStatus>,
404    wake: Arc<tokio::sync::Notify>,
405}
406
407#[cfg(feature = "cluster")]
408struct ActiveSourceDrain {
409    request: SourceDrainRequest,
410    participant: CheckpointParticipant,
411    provider_drain: bool,
412    prepare_deadline: tokio::time::Instant,
413    ready: bool,
414    pending_resolution: Option<PendingSourceDrainResolution>,
415}
416
417#[cfg(feature = "cluster")]
418#[derive(Clone, Copy)]
419struct PendingSourceDrainResolution {
420    resolution: SourceDrainResolution,
421    deadline: tokio::time::Instant,
422}
423
424/// DB-owned proof that one source generation has reached terminal completion.
425///
426/// Actor exit is published by a wrapper that owns the connector future before spawn, and connector
427/// children are observed through the exact tracker captured at construction. Neither proof depends
428/// on the detached outcome supervisor continuing to run.
429#[derive(Clone)]
430pub(crate) struct SourceTaskLease {
431    state: Arc<SourceTaskState>,
432}
433
434pub(crate) type OwnedSourceTasks = Arc<parking_lot::Mutex<Vec<SourceTaskLease>>>;
435
436impl SourceTaskLease {
437    fn supervise(
438        name: Arc<str>,
439        shutdown: Arc<tokio::sync::Notify>,
440        expected_shutdown: Arc<AtomicBool>,
441        join: tokio::task::JoinHandle<()>,
442        actor_terminal: Arc<SourceActorTerminalState>,
443        terminal_tasks: Option<ConnectorTaskTracker>,
444        runtime: &tokio::runtime::Handle,
445    ) -> Self {
446        let state = Arc::new(SourceTaskState {
447            name,
448            shutdown,
449            expected_shutdown,
450            abort: join.abort_handle(),
451            actor_terminal,
452            terminal_tasks,
453            outcome: parking_lot::Mutex::new(None),
454            #[cfg(feature = "cluster")]
455            drain: parking_lot::Mutex::new(None),
456        });
457        let supervisor_state = Arc::clone(&state);
458        let supervisor = runtime.spawn(async move {
459            let outcome = match join.await {
460                Ok(()) => SourceTaskOutcome::Success,
461                Err(error) if error.is_cancelled() => SourceTaskOutcome::Cancelled,
462                Err(error) => SourceTaskOutcome::Failed(Arc::from(error.to_string())),
463            };
464            *supervisor_state.outcome.lock() = Some(outcome);
465        });
466        // Tokio owns the scheduled outcome observer. Terminal replacement fencing is independent
467        // of this detached task and comes from the actor wrapper plus connector tracker above.
468        drop(supervisor);
469        Self { state }
470    }
471
472    pub(crate) fn name(&self) -> &str {
473        &self.state.name
474    }
475
476    #[cfg(feature = "cluster")]
477    fn install_drain_control(&self, control: SourceDrainLeaseControl) {
478        *self.state.drain.lock() = Some(control);
479    }
480
481    #[cfg(feature = "cluster")]
482    fn drain_control(&self) -> Option<SourceDrainLeaseControl> {
483        self.state.drain.lock().clone()
484    }
485
486    pub(crate) fn is_finished(&self) -> bool {
487        self.state.actor_terminal.is_finished()
488            && self
489                .state
490                .terminal_tasks
491                .as_ref()
492                .is_none_or(ConnectorTaskTracker::is_terminated)
493    }
494
495    pub(crate) fn request_shutdown(&self) {
496        self.mark_expected_shutdown();
497        self.notify_shutdown();
498    }
499
500    fn mark_expected_shutdown(&self) {
501        self.state.expected_shutdown.store(true, Ordering::Release);
502    }
503
504    fn notify_shutdown(&self) {
505        self.state.shutdown.notify_one();
506    }
507
508    pub(crate) fn abort(&self) {
509        self.state.abort.abort();
510    }
511
512    async fn wait_finished(&self) {
513        let actor = self.state.actor_terminal.wait_finished();
514        let connector_tasks = async {
515            if let Some(tasks) = self.state.terminal_tasks.as_ref() {
516                tasks.wait_terminated().await;
517            }
518        };
519        tokio::join!(actor, connector_tasks);
520    }
521
522    pub(crate) async fn wait_until(&self, deadline: tokio::time::Instant) -> bool {
523        if self.is_finished() {
524            return true;
525        }
526        tokio::time::timeout_at(deadline, self.wait_finished())
527            .await
528            .is_ok()
529            || self.is_finished()
530    }
531
532    pub(crate) fn log_terminal_outcome(&self) {
533        match self.state.outcome.lock().clone() {
534            Some(SourceTaskOutcome::Failed(error)) => {
535                tracing::warn!(source = %self.state.name, %error, "source task panicked");
536            }
537            Some(SourceTaskOutcome::Success | SourceTaskOutcome::Cancelled) | None => {}
538        }
539    }
540}
541
542/// Stable owner for source generations constructed through [`StreamingCoordinator::new`].
543#[derive(Clone)]
544pub struct StreamingCoordinatorRuntime {
545    owned_source_tasks: OwnedSourceTasks,
546    owned_connector_task_fences: OwnedConnectorTaskFences,
547    construction: Arc<tokio::sync::Mutex<()>>,
548    active_coordinator: Arc<AtomicBool>,
549}
550
551struct StreamingCoordinatorGeneration {
552    runtime: StreamingCoordinatorRuntime,
553}
554
555impl Drop for StreamingCoordinatorGeneration {
556    fn drop(&mut self) {
557        self.runtime
558            .active_coordinator
559            .store(false, Ordering::Release);
560    }
561}
562
563impl StreamingCoordinatorRuntime {
564    /// Creates an empty runtime owner with no active source generation.
565    #[must_use]
566    pub fn new() -> Self {
567        Self {
568            owned_source_tasks: Arc::new(parking_lot::Mutex::new(Vec::new())),
569            owned_connector_task_fences: Arc::new(parking_lot::Mutex::new(Vec::new())),
570            construction: Arc::new(tokio::sync::Mutex::new(())),
571            active_coordinator: Arc::new(AtomicBool::new(false)),
572        }
573    }
574
575    fn claim_generation(&self) -> Result<StreamingCoordinatorGeneration, DbError> {
576        self.active_coordinator
577            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
578            .map_err(|_| {
579                DbError::Pipeline(
580                    "cannot construct a replacement streaming coordinator while the prior \
581                     coordinator generation is still active"
582                        .into(),
583                )
584            })?;
585        Ok(StreamingCoordinatorGeneration {
586            runtime: self.clone(),
587        })
588    }
589
590    fn prune_and_require_idle(&self) -> Result<(), DbError> {
591        let source_names = {
592            let mut sources = self.owned_source_tasks.lock();
593            sources.retain(|source| !source.is_finished());
594            sources
595                .iter()
596                .map(|source| source.name().to_owned())
597                .collect::<Vec<_>>()
598        };
599        let connector_names = {
600            let mut connectors = self.owned_connector_task_fences.lock();
601            connectors.retain(|connector| !connector.is_finished());
602            connectors
603                .iter()
604                .map(|connector| connector.name().to_owned())
605                .collect::<Vec<_>>()
606        };
607        if source_names.is_empty() && connector_names.is_empty() {
608            return Ok(());
609        }
610        Err(DbError::Pipeline(format!(
611            "cannot construct a replacement streaming coordinator while prior connector \
612             generations remain unresolved: sources=[{}], connector_tasks=[{}]",
613            source_names.join(", "),
614            connector_names.join(", ")
615        )))
616    }
617}
618
619impl Default for StreamingCoordinatorRuntime {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624
625#[cfg(feature = "cluster")]
626async fn await_source_drain_receipt(
627    task: &SourceTaskLease,
628    control: &SourceDrainLeaseControl,
629    round: AssignmentDrainId,
630    deadline: tokio::time::Instant,
631) -> Result<SourceDrainReceipt, String> {
632    let mut status_rx = control.status_tx.subscribe();
633    loop {
634        match status_rx.borrow_and_update().clone() {
635            SourceDrainTaskStatus::Ready(receipt) if receipt.round == round => {
636                return Ok(receipt);
637            }
638            SourceDrainTaskStatus::Ready(receipt) => {
639                return Err(format!(
640                    "source '{}' retained stale drain receipt {:?} while waiting for {round:?}",
641                    task.name(),
642                    receipt.round
643                ));
644            }
645            SourceDrainTaskStatus::Pausing(active) if active != round => {
646                return Err(format!(
647                    "source '{}' is pausing conflicting drain {active:?}",
648                    task.name()
649                ));
650            }
651            SourceDrainTaskStatus::Resolved { round: active, .. } if active == round => {
652                return Err(format!(
653                    "source '{}' resolved drain {round:?} before publishing a receipt",
654                    task.name()
655                ));
656            }
657            SourceDrainTaskStatus::Idle
658            | SourceDrainTaskStatus::Pausing(_)
659            | SourceDrainTaskStatus::Resolved { .. } => {}
660        }
661        if task.is_finished() {
662            return Err(format!(
663                "source '{}' exited while preparing drain {round:?}",
664                task.name()
665            ));
666        }
667        let task_finished = task.wait_finished();
668        tokio::pin!(task_finished);
669        if task.is_finished() {
670            continue;
671        }
672        let wait = async {
673            tokio::select! {
674                changed = status_rx.changed() => changed.map_err(|_| "source drain status channel closed"),
675                () = task_finished.as_mut() => Ok(()),
676            }
677        };
678        match tokio::time::timeout_at(deadline, wait).await {
679            Ok(Ok(())) => {}
680            Ok(Err(error)) => return Err(error.into()),
681            Err(_) => {
682                return Err(format!(
683                    "source '{}' did not reach drain {round:?} before its deadline",
684                    task.name()
685                ));
686            }
687        }
688    }
689}
690
691#[cfg(feature = "cluster")]
692pub(crate) async fn prepare_owned_source_drain(
693    tasks: &OwnedSourceTasks,
694    transition: &AssignmentDrainTransition,
695    participant: CheckpointParticipant,
696    deadline: tokio::time::Instant,
697) -> Result<(), String> {
698    if !transition.is_canonical()
699        || transition
700            .predecessor
701            .participant_incarnation(participant.node_id)
702            != Some(participant.boot_incarnation)
703    {
704        return Err("local source drain participant is absent from predecessor roster".into());
705    }
706    let request = SourceDrainRequest::new(transition.id()).map_err(|error| error.to_string())?;
707    let snapshot: Vec<(SourceTaskLease, SourceDrainLeaseControl)> = tasks
708        .lock()
709        .iter()
710        .map(|task| {
711            task.drain_control()
712                .map(|control| (task.clone(), control))
713                .ok_or_else(|| format!("source '{}' has no cluster drain control", task.name()))
714        })
715        .collect::<Result<_, _>>()?;
716    for (task, control) in &snapshot {
717        if task.is_finished() {
718            return Err(format!(
719                "source '{}' is not live at drain admission",
720                task.name()
721            ));
722        }
723        control
724            .command_tx
725            .send(Some(SourceDrainCommand::Begin {
726                request: request.clone(),
727                participant,
728                deadline,
729            }))
730            .map_err(|_| format!("source '{}' drain command channel closed", task.name()))?;
731        control.wake.notify_one();
732    }
733    let mut receipts = Vec::with_capacity(snapshot.len());
734    for (task, control) in &snapshot {
735        let receipt = await_source_drain_receipt(task, control, request.round, deadline).await?;
736        if receipt.source_task_incarnation != control.task_incarnation {
737            return Err(format!(
738                "source '{}' returned a receipt from a replaced task generation",
739                task.name()
740            ));
741        }
742        receipts.push(receipt);
743    }
744    validate_source_drain_receipts(request.round, participant, &receipts)?;
745
746    // Revalidate the exact active source generation immediately before acknowledging the cut.
747    let mut expected: Vec<uuid::Uuid> = snapshot
748        .iter()
749        .map(|(_, control)| control.task_incarnation)
750        .collect();
751    let mut current: Vec<uuid::Uuid> = tasks
752        .lock()
753        .iter()
754        .map(|task| {
755            task.drain_control()
756                .map(|control| control.task_incarnation)
757                .ok_or_else(|| format!("source '{}' lost cluster drain control", task.name()))
758        })
759        .collect::<Result<_, _>>()?;
760    expected.sort_unstable();
761    current.sort_unstable();
762    if current != expected {
763        return Err("source task generation changed while preparing the drain".into());
764    }
765    Ok(())
766}
767
768#[cfg(feature = "cluster")]
769pub(crate) fn owned_source_drain_resolved(
770    tasks: &OwnedSourceTasks,
771    resolution: SourceDrainResolution,
772) -> Result<bool, String> {
773    for task in tasks.lock().iter() {
774        let control = task
775            .drain_control()
776            .ok_or_else(|| format!("source '{}' has no cluster drain control", task.name()))?;
777        if task.is_finished() {
778            if resolution.outcome == SourceDrainOutcome::Abort {
779                continue;
780            }
781            return Ok(false);
782        }
783        if !matches!(
784            control.status_tx.borrow().clone(),
785            SourceDrainTaskStatus::Resolved { round, outcome }
786                if round == resolution.round && outcome == resolution.outcome
787        ) {
788            return Ok(false);
789        }
790    }
791    Ok(true)
792}
793
794#[cfg(feature = "cluster")]
795pub(crate) async fn resolve_owned_source_drain(
796    tasks: &OwnedSourceTasks,
797    resolution: SourceDrainResolution,
798    deadline: tokio::time::Instant,
799) -> Result<(), String> {
800    let snapshot: Vec<(SourceTaskLease, SourceDrainLeaseControl)> = tasks
801        .lock()
802        .iter()
803        .map(|task| {
804            task.drain_control()
805                .map(|control| (task.clone(), control))
806                .ok_or_else(|| format!("source '{}' has no cluster drain control", task.name()))
807        })
808        .collect::<Result<_, _>>()?;
809    for (task, control) in &snapshot {
810        if task.is_finished() {
811            if resolution.outcome == SourceDrainOutcome::Abort {
812                continue;
813            }
814            return Err(format!(
815                "source '{}' exited before committing drain {:?}",
816                task.name(),
817                resolution.round
818            ));
819        }
820        let sent = control
821            .command_tx
822            .send(Some(SourceDrainCommand::Resolve {
823                resolution,
824                deadline,
825            }))
826            .is_ok();
827        if !sent {
828            if resolution.outcome == SourceDrainOutcome::Abort {
829                continue;
830            }
831            return Err(format!(
832                "source '{}' drain command channel closed",
833                task.name()
834            ));
835        }
836        control.wake.notify_one();
837    }
838    for (task, control) in &snapshot {
839        if task.is_finished() && resolution.outcome == SourceDrainOutcome::Abort {
840            continue;
841        }
842        let mut status_rx = control.status_tx.subscribe();
843        loop {
844            match status_rx.borrow_and_update().clone() {
845                SourceDrainTaskStatus::Resolved { round, outcome }
846                    if round == resolution.round && outcome == resolution.outcome =>
847                {
848                    break;
849                }
850                SourceDrainTaskStatus::Resolved { round, .. } if round != resolution.round => {
851                    return Err(format!(
852                        "source '{}' resolved stale drain {round:?}",
853                        task.name()
854                    ));
855                }
856                _ => {}
857            }
858            if task.is_finished() {
859                if resolution.outcome == SourceDrainOutcome::Abort {
860                    break;
861                }
862                return Err(format!(
863                    "source '{}' exited while resolving drain {:?}",
864                    task.name(),
865                    resolution.round
866                ));
867            }
868            let task_finished = task.wait_finished();
869            tokio::pin!(task_finished);
870            if task.is_finished() {
871                continue;
872            }
873            let wait = async {
874                tokio::select! {
875                    changed = status_rx.changed() => changed.map_err(|_| "source drain status channel closed"),
876                    () = task_finished.as_mut() => Ok(()),
877                }
878            };
879            match tokio::time::timeout_at(deadline, wait).await {
880                Ok(Ok(())) => {}
881                Ok(Err(error)) => {
882                    return Err(format!("source '{}': {error}", task.name()));
883                }
884                Err(_) => {
885                    return Err(format!(
886                        "source '{}' did not resolve drain {:?} before its deadline",
887                        task.name(),
888                        resolution.round
889                    ));
890                }
891            }
892        }
893    }
894    Ok(())
895}
896
897#[cfg(all(test, feature = "cluster"))]
898pub(crate) fn install_replacement_source_drain_task_for_test(
899    tasks: &OwnedSourceTasks,
900    name: &str,
901) -> SourceTaskLease {
902    let (command_tx, mut command_rx) =
903        tokio::sync::watch::channel::<Option<SourceDrainCommand>>(None);
904    let (status_tx, _status_rx) = tokio::sync::watch::channel(SourceDrainTaskStatus::Idle);
905    let wake = Arc::new(tokio::sync::Notify::new());
906    let shutdown = Arc::new(tokio::sync::Notify::new());
907    let actor_wake = Arc::clone(&wake);
908    let actor_shutdown = Arc::clone(&shutdown);
909    let actor_status = status_tx.clone();
910    let runtime = tokio::runtime::Handle::current();
911    let (join, actor_terminal) = spawn_source_actor(&runtime, async move {
912        loop {
913            tokio::select! {
914                () = actor_shutdown.notified() => return,
915                () = actor_wake.notified() => {
916                    let command = command_rx.borrow_and_update().clone();
917                    if let Some(SourceDrainCommand::Resolve { resolution, .. }) = command {
918                        actor_status.send_replace(SourceDrainTaskStatus::Resolved {
919                            round: resolution.round,
920                            outcome: resolution.outcome,
921                        });
922                    }
923                }
924            }
925        }
926    });
927    let task = SourceTaskLease::supervise(
928        Arc::from(name),
929        shutdown,
930        Arc::new(AtomicBool::new(false)),
931        join,
932        actor_terminal,
933        None,
934        &runtime,
935    );
936    task.install_drain_control(SourceDrainLeaseControl {
937        task_incarnation: uuid::Uuid::new_v4(),
938        command_tx,
939        status_tx,
940        wake,
941    });
942    tasks.lock().push(task.clone());
943    task
944}
945
946/// Handle to a running source I/O task.
947struct SourceHandle {
948    /// Whether this source's checkpoint is a durable recovery cursor. Ephemeral source
949    /// checkpoints may align a barrier but must never enter a manifest or cluster handoff.
950    recovery_cursor: bool,
951    task: SourceTaskLease,
952    /// One-shot startup fence. Source I/O cannot begin until the compute loop has installed its
953    /// control plane and published the runtime-ready boundary.
954    startup_activation: Option<crossfire::oneshot::TxOneshot<()>>,
955    barrier_injector: CheckpointBarrierInjector,
956    /// Retained exact release/stop command for a source held after barrier emission.
957    barrier_release_tx: tokio::sync::watch::Sender<Option<SourceBarrierSignal>>,
958    /// Notifies the source of a committed `(epoch, checkpoint)` so it can ack upstream.
959    /// The checkpoint is what was written to the manifest (may lag). Empty only when the epoch
960    /// captured no state for this source; an empty one is a no-op for upstream advancement.
961    epoch_committed_tx: tokio::sync::watch::Sender<Option<(u64, SourceCheckpoint)>>,
962}
963
964pub(crate) struct TrackedSourceRegistration {
965    source: SourceRegistration,
966    task_fence: ConnectorTaskFenceRegistration,
967}
968
969impl TrackedSourceRegistration {
970    pub(crate) fn capture(source: SourceRegistration, owned: &OwnedConnectorTaskFences) -> Self {
971        let task_fence = ConnectorTaskFenceRegistration::capture_registered(
972            Arc::<str>::from(format!("source:{}", source.name)),
973            source.connector.terminal_task_tracker(),
974            owned,
975        );
976        Self { source, task_fence }
977    }
978
979    pub(crate) fn from_captured(
980        source: SourceRegistration,
981        task_fence: ConnectorTaskFenceRegistration,
982    ) -> Self {
983        Self { source, task_fence }
984    }
985}
986
987impl std::ops::Deref for TrackedSourceRegistration {
988    type Target = SourceRegistration;
989
990    fn deref(&self) -> &Self::Target {
991        &self.source
992    }
993}
994
995impl std::ops::DerefMut for TrackedSourceRegistration {
996    fn deref_mut(&mut self) -> &mut Self::Target {
997        &mut self.source
998    }
999}
1000
1001struct PreparedSourceGeneration {
1002    registration: TrackedSourceRegistration,
1003}
1004
1005impl SourceHandle {
1006    fn barrier_control(&self) -> SourceBarrierControl {
1007        SourceBarrierControl::new(
1008            self.barrier_injector.clone(),
1009            self.barrier_release_tx.clone(),
1010        )
1011    }
1012}
1013
1014/// Why [`StreamingCoordinator::run`] returned.
1015#[derive(Debug)]
1016pub enum ExitReason {
1017    /// Coordinator shutdown was explicitly signaled — a clean stop.
1018    Shutdown,
1019    /// Fatal runtime error; the lifecycle restarts or coordinates recovery as configured.
1020    Fault(String),
1021}
1022
1023/// Single-task pipeline coordinator — no core threads.
1024pub struct StreamingCoordinator {
1025    config: PipelineConfig,
1026    rx: SourceMsgRx,
1027    source_fault_rx: tokio::sync::mpsc::UnboundedReceiver<SourceFault>,
1028    source_handles: Vec<SourceHandle>,
1029    source_names: Vec<Arc<str>>,
1030    shutdown: Arc<tokio::sync::Notify>,
1031    terminal_shutdown: tokio_util::sync::CancellationToken,
1032    pending_barrier: PendingBarrier,
1033    last_checkpoint: Instant,
1034    checkpoint_retry_not_before: Option<Instant>,
1035    checkpoint_retry_backoff: Duration,
1036    source_batches_buf: FxHashMap<Arc<str>, Vec<RecordBatch>>,
1037    /// At most one FIFO message removed just as the external intake gate closes. Exact source
1038    /// barrier holds make post-barrier data impossible; this slot exists only for that gate race.
1039    parked_source_msg: Option<SourceMsg>,
1040    pending_watermark_batches: Vec<(Arc<str>, RecordBatch)>,
1041    /// Sources that delivered a barrier this drain cycle. A later batch from one of these sources
1042    /// violates the source hold protocol and faults the pipeline.
1043    barrier_seen: FxHashSet<usize>,
1044    /// Per-source offset merged from `pending_offsets` after successful cycle publication.
1045    committed_offsets: Vec<Option<SourceCheckpoint>>,
1046    /// Offsets staged by `process_msg`; a replay-preserving deferral retains them until the graph
1047    /// consumes its buffered work, while a fault discards them.
1048    pending_offsets: Vec<Option<SourceCheckpoint>>,
1049    /// The previous cycle retained graph work. Its retry is scheduled ahead of source intake so a
1050    /// newer connector cursor cannot overtake the buffered mutation.
1051    replay_pending: bool,
1052    control_rx: ControlMsgRx,
1053    checkpoint_complete_rx:
1054        Option<crossfire::AsyncRx<crossfire::mpsc::Array<CheckpointCompletion>>>,
1055    /// Public checkpoint requests waiting for the next newly-admitted exact attempt.
1056    force_ckpt_rx: Option<crate::db::ForceCheckpointRx>,
1057    manual_waiting: Vec<ForceCheckpointReply>,
1058    /// Requests attached at admission. Later requests remain in `manual_waiting`.
1059    manual_active: Option<ManualCheckpointAttempt>,
1060    /// Epochs between admission and durable (tails still running); shared with callback.
1061    checkpoint_in_flight: Arc<AtomicU64>,
1062    /// Last durable completion published to sources/subscribers in this runtime. This is a
1063    /// defense-in-depth monotonic fence in addition to serialized tail admission.
1064    last_published_checkpoint: Option<CheckpointAttempt>,
1065    /// Shared exact external-commit bound, checked before ID reservation/barrier injection.
1066    coordinated_commit_admission: Option<crate::checkpoint_coordinator::CoordinatedCommitAdmission>,
1067    #[cfg(feature = "cluster")]
1068    process_authority: Option<Arc<SourceProcessAuthority>>,
1069    public_generation: Option<StreamingCoordinatorGeneration>,
1070}
1071
1072struct CoordinatorRunState {
1073    batch_window: Duration,
1074    coordinated_commit_progress: Option<Arc<tokio::sync::Notify>>,
1075    checkpoint_control_wake: Option<CheckpointControlWake>,
1076    checkpoint_control_poll_at: tokio::time::Instant,
1077    checkpoint_control_pending: bool,
1078    barriers: Vec<(usize, CheckpointBarrier, SourceCheckpoint)>,
1079    fault: Option<String>,
1080    halted: bool,
1081    source_channel_expected: bool,
1082}
1083
1084#[derive(Default)]
1085struct CoordinatorWake {
1086    message: Option<SourceMsg>,
1087    retrying_replay: bool,
1088    checkpoint_control_due: bool,
1089    gates: CoordinatorGates,
1090}
1091
1092#[derive(Default)]
1093struct CoordinatorGates {
1094    intake_paused: bool,
1095    external_commit_paused: bool,
1096}
1097
1098enum CoordinatorWaitAction {
1099    Cycle,
1100    Continue,
1101    Stop,
1102}
1103
1104struct CoordinatorWait {
1105    action: CoordinatorWaitAction,
1106    wake: CoordinatorWake,
1107}
1108
1109impl CoordinatorWait {
1110    fn cycle(wake: CoordinatorWake) -> Self {
1111        Self {
1112            action: CoordinatorWaitAction::Cycle,
1113            wake,
1114        }
1115    }
1116
1117    fn continue_loop() -> Self {
1118        Self {
1119            action: CoordinatorWaitAction::Continue,
1120            wake: CoordinatorWake::default(),
1121        }
1122    }
1123
1124    fn stop() -> Self {
1125        Self {
1126            action: CoordinatorWaitAction::Stop,
1127            wake: CoordinatorWake::default(),
1128        }
1129    }
1130}
1131
1132impl Drop for StreamingCoordinator {
1133    fn drop(&mut self) {
1134        // `run` owns the coordinator by value, so cancellation or unwind would otherwise discard
1135        // the only per-source shutdown controls. The DB-owned leases remain registered until the
1136        // actor-exit wrapper and exact connector tracker both prove terminal completion.
1137        for handle in &self.source_handles {
1138            handle.task.request_shutdown();
1139        }
1140        // Release public construction ownership only after every source has observed shutdown.
1141        drop(self.public_generation.take());
1142    }
1143}
1144
1145/// Public checkpoint callers attached to one exact attempt at admission time.
1146struct ManualCheckpointAttempt {
1147    attempt: CheckpointAttempt,
1148    replies: Vec<ForceCheckpointReply>,
1149}
1150
1151struct CheckpointAdmission {
1152    manual: bool,
1153    assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
1154}
1155
1156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1157enum CheckpointCleanupOwner {
1158    /// Embedded, single-node, or cluster-leader attempt originator.
1159    Originator,
1160    /// Cluster follower that reserved an attempt announced by the originator.
1161    Follower,
1162}
1163
1164struct AlignedCheckpointContext {
1165    cleanup_owner: CheckpointCleanupOwner,
1166    attempt: CheckpointAttempt,
1167    started_at: Instant,
1168    assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
1169}
1170
1171/// Tracks in-flight checkpoint barrier alignment.
1172struct PendingBarrier {
1173    attempt: Option<CheckpointAttempt>,
1174    sources_total: usize,
1175    sources_aligned: FxHashSet<usize>,
1176    source_checkpoints: FxHashMap<String, SourceCheckpoint>,
1177    started_at: Instant,
1178    active: bool,
1179    assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
1180    cleanup_owner: CheckpointCleanupOwner,
1181}
1182
1183impl PendingBarrier {
1184    fn new() -> Self {
1185        Self {
1186            attempt: None,
1187            sources_total: 0,
1188            sources_aligned: FxHashSet::default(),
1189            source_checkpoints: FxHashMap::default(),
1190            started_at: Instant::now(),
1191            active: false,
1192            assignment_fence: None,
1193            cleanup_owner: CheckpointCleanupOwner::Originator,
1194        }
1195    }
1196
1197    #[cfg(test)]
1198    fn reset(&mut self, attempt: CheckpointAttempt, sources_total: usize) {
1199        self.reset_with_assignment(attempt, sources_total, None);
1200    }
1201
1202    fn reset_follower(&mut self, attempt: CheckpointAttempt, sources_total: usize) {
1203        self.reset_inner(
1204            attempt,
1205            sources_total,
1206            None,
1207            CheckpointCleanupOwner::Follower,
1208        );
1209    }
1210
1211    fn reset_with_assignment(
1212        &mut self,
1213        attempt: CheckpointAttempt,
1214        sources_total: usize,
1215        assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
1216    ) {
1217        self.reset_inner(
1218            attempt,
1219            sources_total,
1220            assignment_fence,
1221            CheckpointCleanupOwner::Originator,
1222        );
1223    }
1224
1225    fn reset_inner(
1226        &mut self,
1227        attempt: CheckpointAttempt,
1228        sources_total: usize,
1229        assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
1230        cleanup_owner: CheckpointCleanupOwner,
1231    ) {
1232        self.attempt = Some(attempt);
1233        self.sources_total = sources_total;
1234        self.sources_aligned.clear();
1235        self.source_checkpoints.clear();
1236        self.started_at = Instant::now();
1237        self.active = true;
1238        self.assignment_fence = assignment_fence;
1239        self.cleanup_owner = cleanup_owner;
1240    }
1241
1242    /// Clear alignment state and return the exact active attempt and its cleanup owner.
1243    fn take_active_attempt(&mut self) -> Option<(CheckpointAttempt, CheckpointCleanupOwner)> {
1244        if !self.active {
1245            return None;
1246        }
1247        let cleanup_owner = self.cleanup_owner;
1248        self.active = false;
1249        self.sources_total = 0;
1250        self.sources_aligned.clear();
1251        self.source_checkpoints.clear();
1252        self.assignment_fence = None;
1253        self.cleanup_owner = CheckpointCleanupOwner::Originator;
1254        self.attempt.take().map(|attempt| (attempt, cleanup_owner))
1255    }
1256
1257    fn clear(&mut self) {
1258        self.active = false;
1259        self.attempt = None;
1260        self.sources_total = 0;
1261        self.sources_aligned.clear();
1262        self.source_checkpoints.clear();
1263        self.assignment_fence = None;
1264        self.cleanup_owner = CheckpointCleanupOwner::Originator;
1265    }
1266}
1267
1268/// Fallback timeout for idle wake.
1269const IDLE_TIMEOUT: Duration = Duration::from_millis(100);
1270
1271/// Internal topology-retry floor and cap. Assignment admission remains the authoritative gate.
1272const CHECKPOINT_RETRY_BASE: Duration = Duration::from_millis(100);
1273const CHECKPOINT_RETRY_MAX: Duration = Duration::from_secs(5);
1274
1275/// Cap on a source task's post-shutdown flush so a hot source can't stall shutdown.
1276const SHUTDOWN_DRAIN_BUDGET: Duration = Duration::from_secs(2);
1277
1278/// Cap on awaiting a source task at shutdown before retiring its connector generation.
1279const SHUTDOWN_JOIN_TIMEOUT: Duration = Duration::from_secs(8);
1280
1281/// Shutdown-only poll cadence. It closes the atomic/channel race when a tail drops its in-flight
1282/// guard without producing another wakeup (for example an aborted cluster follower tail).
1283const SHUTDOWN_COMPLETION_TICK: Duration = Duration::from_millis(10);
1284
1285/// Grace period for already-captured asynchronous checkpoint tails. On expiry their tracked
1286/// tasks are cancelled before sources or sinks are torn down; exact attempt namespaces leave any
1287/// ambiguous remote write safe for recovery.
1288const SHUTDOWN_CHECKPOINT_TAIL_TIMEOUT: Duration = Duration::from_secs(8);
1289
1290/// Graceful stop gives already-sealed checkpoints one bounded opportunity to
1291/// reach coordinated external sinks. Timeout leaves durable markers for replay.
1292const COORDINATED_COMMIT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
1293
1294fn warn_external_commit_cap_throttled(known: bool, pending: u64, cap: u64) {
1295    static THROTTLE: crate::log_throttle::LogThrottle =
1296        crate::log_throttle::LogThrottle::every(Duration::from_secs(10));
1297    if THROTTLE.allow() {
1298        tracing::warn!(
1299            lag_known = known,
1300            pending_external_checkpoints = pending,
1301            cap,
1302            "checkpoint admission paused at coordinated external-commit bound"
1303        );
1304    }
1305}
1306
1307fn try_source_checkpoint(
1308    connector: &dyn SourceConnector,
1309    assignment_scoped: bool,
1310) -> Result<Option<SourceCheckpoint>, ConnectorError> {
1311    let checkpoint = connector.try_checkpoint()?;
1312    let Some(captured) = checkpoint.as_ref() else {
1313        return Ok(None);
1314    };
1315    match (assignment_scoped, captured.assignment_version()) {
1316        (true, None) => Err(ConnectorError::Internal(
1317            "cluster-assigned source checkpoint is missing its assignment version".into(),
1318        )),
1319        (false, Some(version)) => Err(ConnectorError::Internal(format!(
1320            "local source checkpoint unexpectedly carries cluster assignment version {version}"
1321        ))),
1322        _ => Ok(checkpoint),
1323    }
1324}
1325
1326/// Apply the newest durable commit notification while no source poll borrows
1327/// the connector. Non-best-effort pipelines fault if upstream acknowledgement
1328/// fails because silently continuing can exhaust upstream retention or acknowledgement headroom.
1329async fn acknowledge_latest_source_commit(
1330    connector: &mut dyn SourceConnector,
1331    epoch_committed_rx: &mut tokio::sync::watch::Receiver<Option<(u64, SourceCheckpoint)>>,
1332    delivery_guarantee: DeliveryGuarantee,
1333    src_name: &str,
1334    fault_tx: &tokio::sync::mpsc::UnboundedSender<SourceFault>,
1335    deadline: tokio::time::Instant,
1336    cancellation_policy: ConnectorCancellationPolicy,
1337    lifecycle: &mut SourceConnectorLifecycle,
1338    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
1339) -> bool {
1340    let Some((epoch, checkpoint)) = epoch_committed_rx.borrow_and_update().clone() else {
1341        return true;
1342    };
1343    let result = match run_source_operation(
1344        deadline,
1345        #[cfg(feature = "cluster")]
1346        process_authority,
1347        || connector.notify_epoch_committed(epoch, &checkpoint),
1348    )
1349    .await
1350    {
1351        SourceOperationOutcome::Completed(result) => result,
1352        SourceOperationOutcome::Deadline => {
1353            lifecycle.cancelled(cancellation_policy);
1354            Err(source_operation_deadline_error(
1355                src_name,
1356                "commit notification",
1357            ))
1358        }
1359        #[cfg(feature = "cluster")]
1360        SourceOperationOutcome::ProcessAuthorityLost => {
1361            lifecycle.authority_lost();
1362            return false;
1363        }
1364    };
1365    if let Err(error) = result {
1366        if delivery_guarantee == DeliveryGuarantee::BestEffort {
1367            tracing::warn!(
1368                source = src_name,
1369                %error,
1370                epoch,
1371                "notify_epoch_committed failed",
1372            );
1373            return lifecycle.may_invoke_connector();
1374        }
1375        lifecycle.fault_data_plane();
1376        let _ = fault_tx.send(SourceFault {
1377            source: Arc::from(src_name),
1378            error: format!("commit notification failed at epoch {epoch}: {error}"),
1379        });
1380        return false;
1381    }
1382    true
1383}
1384
1385#[cfg(feature = "cluster")]
1386async fn apply_latest_source_drain_command_fenced(
1387    connector: &mut dyn SourceConnector,
1388    command_rx: &mut tokio::sync::watch::Receiver<Option<SourceDrainCommand>>,
1389    status_tx: &tokio::sync::watch::Sender<SourceDrainTaskStatus>,
1390    active: &mut Option<ActiveSourceDrain>,
1391    provider_drain: bool,
1392    source_name: &str,
1393    cancellation_policy: ConnectorCancellationPolicy,
1394    lifecycle: &mut SourceConnectorLifecycle,
1395    process_authority: Option<&SourceProcessAuthority>,
1396) -> Result<(), ConnectorError> {
1397    match command_rx.has_changed() {
1398        Ok(false) => return Ok(()),
1399        Err(_) => {
1400            return Err(ConnectorError::Internal(
1401                "source drain command channel closed".into(),
1402            ));
1403        }
1404        Ok(true) => {}
1405    }
1406    let Some(command) = command_rx.borrow_and_update().clone() else {
1407        return Ok(());
1408    };
1409    match command {
1410        SourceDrainCommand::Begin {
1411            request,
1412            participant,
1413            deadline,
1414        } => {
1415            if let Some(current) = active.as_ref() {
1416                if current.request != request || current.participant != participant {
1417                    return Err(ConnectorError::InvalidState {
1418                        expected: format!("active source drain {:?}", current.request.round),
1419                        actual: format!("conflicting source drain {:?}", request.round),
1420                    });
1421                }
1422                // A retry may carry a fresh caller wait budget, but it must not extend the
1423                // provider operation that already started. Retain the original deadline.
1424                return Ok(());
1425            }
1426            if tokio::time::Instant::now() >= deadline {
1427                return Err(ConnectorError::Internal(format!(
1428                    "source drain {:?} expired before provider preparation began",
1429                    request.round
1430                )));
1431            }
1432            if provider_drain {
1433                lifecycle.run_sync_hook(
1434                    source_name,
1435                    "drain preparation",
1436                    deadline,
1437                    cancellation_policy,
1438                    process_authority,
1439                    || connector.begin_drain(&request, deadline),
1440                )?;
1441            } else {
1442                check_source_sync_fence(
1443                    source_name,
1444                    "drain preparation",
1445                    deadline,
1446                    false,
1447                    cancellation_policy,
1448                    lifecycle,
1449                    process_authority,
1450                )?;
1451            }
1452            lifecycle.run_sync_hook(
1453                source_name,
1454                "drain preparation publication",
1455                deadline,
1456                cancellation_policy,
1457                process_authority,
1458                || {
1459                    let _ = status_tx.send_replace(SourceDrainTaskStatus::Pausing(request.round));
1460                    Ok(())
1461                },
1462            )?;
1463            *active = Some(ActiveSourceDrain {
1464                request,
1465                participant,
1466                provider_drain,
1467                prepare_deadline: deadline,
1468                ready: false,
1469                pending_resolution: None,
1470            });
1471        }
1472        SourceDrainCommand::Resolve {
1473            resolution,
1474            deadline,
1475        } => {
1476            let Some(current) = active.as_ref() else {
1477                match status_tx.borrow().clone() {
1478                    SourceDrainTaskStatus::Resolved { round, outcome }
1479                        if round == resolution.round && outcome == resolution.outcome =>
1480                    {
1481                        return Ok(());
1482                    }
1483                    SourceDrainTaskStatus::Resolved { round, outcome } => {
1484                        return Err(ConnectorError::InvalidState {
1485                            expected: format!("resolved source drain {round:?} as {outcome:?}"),
1486                            actual: format!("conflicting resolution {resolution:?}"),
1487                        });
1488                    }
1489                    SourceDrainTaskStatus::Pausing(round)
1490                    | SourceDrainTaskStatus::Ready(SourceDrainReceipt { round, .. }) => {
1491                        return Err(ConnectorError::InvalidState {
1492                            expected: format!("active source drain {round:?}"),
1493                            actual: "source drain task state was lost".into(),
1494                        });
1495                    }
1496                    SourceDrainTaskStatus::Idle => {}
1497                }
1498                // Prepare broadcasts Begin to every source before awaiting receipts. If one
1499                // source fails quickly, cleanup may overwrite an unobserved Begin in another
1500                // task's retained command slot. No connector work can have started while
1501                // `active` is empty, so abort is a safe terminal no-op. A replacement task may
1502                // also observe a durable commit after its predecessor published the receipt and
1503                // exited. It has no provider cut to finish; accept that commit only after its
1504                // target assignment and recovery cursor are reconciled.
1505                if resolution.outcome == SourceDrainOutcome::Abort {
1506                    lifecycle.run_sync_hook(
1507                        source_name,
1508                        "replacement drain abort publication",
1509                        deadline,
1510                        cancellation_policy,
1511                        process_authority,
1512                        || {
1513                            let _ = status_tx.send_replace(SourceDrainTaskStatus::Resolved {
1514                                round: resolution.round,
1515                                outcome: resolution.outcome,
1516                            });
1517                            Ok(())
1518                        },
1519                    )?;
1520                    return Ok(());
1521                }
1522                if tokio::time::Instant::now() >= deadline {
1523                    return Err(ConnectorError::Internal(format!(
1524                        "source drain resolution {:?} expired before replacement reconciliation",
1525                        resolution.round
1526                    )));
1527                }
1528                if provider_drain {
1529                    let checkpoint_ready = lifecycle.run_sync_hook(
1530                        source_name,
1531                        "replacement checkpoint readiness",
1532                        deadline,
1533                        cancellation_policy,
1534                        process_authority,
1535                        || connector.checkpoint_ready(),
1536                    )?;
1537                    if !checkpoint_ready {
1538                        command_rx.mark_changed();
1539                        return Ok(());
1540                    }
1541                    let checkpoint = lifecycle.run_sync_hook(
1542                        source_name,
1543                        "replacement checkpoint capture",
1544                        deadline,
1545                        cancellation_policy,
1546                        process_authority,
1547                        || try_source_checkpoint(connector, true),
1548                    )?;
1549                    let Some(checkpoint) = checkpoint else {
1550                        command_rx.mark_changed();
1551                        return Ok(());
1552                    };
1553                    let expected = std::num::NonZeroU64::new(resolution.round.target_version)
1554                        .ok_or_else(|| {
1555                            ConnectorError::Internal(
1556                                "replacement drain target has zero assignment version".into(),
1557                            )
1558                        })?;
1559                    if checkpoint.assignment_version() != Some(expected) {
1560                        return Err(ConnectorError::InvalidState {
1561                            expected: format!(
1562                                "replacement source assignment {}",
1563                                resolution.round.target_version
1564                            ),
1565                            actual: checkpoint.assignment_version().map_or_else(
1566                                || "unbound source checkpoint".into(),
1567                                |version| format!("source assignment {version}"),
1568                            ),
1569                        });
1570                    }
1571                }
1572                lifecycle.run_sync_hook(
1573                    source_name,
1574                    "replacement drain resolution publication",
1575                    deadline,
1576                    cancellation_policy,
1577                    process_authority,
1578                    || {
1579                        let _ = status_tx.send_replace(SourceDrainTaskStatus::Resolved {
1580                            round: resolution.round,
1581                            outcome: resolution.outcome,
1582                        });
1583                        Ok(())
1584                    },
1585                )?;
1586                return Ok(());
1587            };
1588            if current.request.round != resolution.round {
1589                return Err(ConnectorError::InvalidState {
1590                    expected: format!("active source drain {:?}", current.request.round),
1591                    actual: format!("resolution for {:?}", resolution.round),
1592                });
1593            }
1594            let resolution_deadline = match current.pending_resolution {
1595                Some(pending) if pending.resolution != resolution => {
1596                    return Err(ConnectorError::InvalidState {
1597                        expected: format!("pending resolution for {:?}", current.request.round),
1598                        actual: format!("conflicting resolution {resolution:?}"),
1599                    });
1600                }
1601                Some(pending) => pending.deadline,
1602                None => deadline,
1603            };
1604            if tokio::time::Instant::now() >= resolution_deadline {
1605                return Err(ConnectorError::Internal(format!(
1606                    "source drain resolution {:?} expired before provider resolution",
1607                    resolution.round
1608                )));
1609            }
1610            if !current.ready {
1611                if resolution.outcome != SourceDrainOutcome::Abort {
1612                    return Err(ConnectorError::InvalidState {
1613                        expected: "receipt-backed source drain cut before commit".into(),
1614                        actual: format!("unready source drain {:?}", resolution.round),
1615                    });
1616                }
1617                let current = active.as_mut().expect("checked above");
1618                // Rewinding before the FIFO boundary is consumed would duplicate payloads that
1619                // are already queued ahead of it. Keep flushing, then resolve from the certified
1620                // cut published by `poll_drain_ready`.
1621                if current.pending_resolution.is_none() {
1622                    current.pending_resolution = Some(PendingSourceDrainResolution {
1623                        resolution,
1624                        deadline: resolution_deadline,
1625                    });
1626                }
1627                return Ok(());
1628            }
1629            if current.provider_drain {
1630                match run_source_operation(resolution_deadline, process_authority, || {
1631                    connector.finish_drain(resolution, resolution_deadline)
1632                })
1633                .await
1634                {
1635                    SourceOperationOutcome::Completed(result) => result?,
1636                    SourceOperationOutcome::Deadline => {
1637                        lifecycle.cancelled(cancellation_policy);
1638                        return Err(source_operation_deadline_error(
1639                            source_name,
1640                            "drain resolution",
1641                        ));
1642                    }
1643                    SourceOperationOutcome::ProcessAuthorityLost => {
1644                        lifecycle.authority_lost();
1645                        return Err(source_operation_authority_error(
1646                            source_name,
1647                            "drain resolution",
1648                        ));
1649                    }
1650                }
1651            }
1652            lifecycle.run_sync_hook(
1653                source_name,
1654                "drain resolution publication",
1655                resolution_deadline,
1656                cancellation_policy,
1657                process_authority,
1658                || {
1659                    let _ = status_tx.send_replace(SourceDrainTaskStatus::Resolved {
1660                        round: resolution.round,
1661                        outcome: resolution.outcome,
1662                    });
1663                    Ok(())
1664                },
1665            )?;
1666            *active = None;
1667        }
1668    }
1669    Ok(())
1670}
1671
1672#[cfg(feature = "cluster")]
1673async fn resolve_pending_source_drain_fenced(
1674    connector: &mut dyn SourceConnector,
1675    status_tx: &tokio::sync::watch::Sender<SourceDrainTaskStatus>,
1676    active: &mut Option<ActiveSourceDrain>,
1677    source_name: &str,
1678    cancellation_policy: ConnectorCancellationPolicy,
1679    lifecycle: &mut SourceConnectorLifecycle,
1680    process_authority: Option<&SourceProcessAuthority>,
1681) -> Result<(), ConnectorError> {
1682    let Some(pending) = active.as_ref().and_then(|current| {
1683        current
1684            .ready
1685            .then_some(current.pending_resolution)
1686            .flatten()
1687    }) else {
1688        return Ok(());
1689    };
1690    if tokio::time::Instant::now() >= pending.deadline {
1691        return Err(ConnectorError::Internal(format!(
1692            "source drain resolution {:?} exceeded its deadline while awaiting the FIFO cut",
1693            pending.resolution.round
1694        )));
1695    }
1696    if active
1697        .as_ref()
1698        .is_some_and(|current| current.provider_drain)
1699    {
1700        match run_source_operation(pending.deadline, process_authority, || {
1701            connector.finish_drain(pending.resolution, pending.deadline)
1702        })
1703        .await
1704        {
1705            SourceOperationOutcome::Completed(result) => result?,
1706            SourceOperationOutcome::Deadline => {
1707                lifecycle.cancelled(cancellation_policy);
1708                return Err(source_operation_deadline_error(
1709                    source_name,
1710                    "pending drain resolution",
1711                ));
1712            }
1713            SourceOperationOutcome::ProcessAuthorityLost => {
1714                lifecycle.authority_lost();
1715                return Err(source_operation_authority_error(
1716                    source_name,
1717                    "pending drain resolution",
1718                ));
1719            }
1720        }
1721    }
1722    lifecycle.run_sync_hook(
1723        source_name,
1724        "pending drain resolution publication",
1725        pending.deadline,
1726        cancellation_policy,
1727        process_authority,
1728        || {
1729            let _ = status_tx.send_replace(SourceDrainTaskStatus::Resolved {
1730                round: pending.resolution.round,
1731                outcome: pending.resolution.outcome,
1732            });
1733            Ok(())
1734        },
1735    )?;
1736    *active = None;
1737    Ok(())
1738}
1739
1740#[cfg(all(test, feature = "cluster"))]
1741async fn apply_latest_source_drain_command(
1742    connector: &mut dyn SourceConnector,
1743    command_rx: &mut tokio::sync::watch::Receiver<Option<SourceDrainCommand>>,
1744    status_tx: &tokio::sync::watch::Sender<SourceDrainTaskStatus>,
1745    active: &mut Option<ActiveSourceDrain>,
1746    provider_drain: bool,
1747) -> Result<(), ConnectorError> {
1748    let mut lifecycle = SourceConnectorLifecycle::default();
1749    apply_latest_source_drain_command_fenced(
1750        connector,
1751        command_rx,
1752        status_tx,
1753        active,
1754        provider_drain,
1755        "test-source",
1756        ConnectorCancellationPolicy::RetireConnector,
1757        &mut lifecycle,
1758        None,
1759    )
1760    .await
1761}
1762
1763#[cfg(all(test, feature = "cluster"))]
1764async fn resolve_pending_source_drain(
1765    connector: &mut dyn SourceConnector,
1766    status_tx: &tokio::sync::watch::Sender<SourceDrainTaskStatus>,
1767    active: &mut Option<ActiveSourceDrain>,
1768) -> Result<(), ConnectorError> {
1769    let mut lifecycle = SourceConnectorLifecycle::default();
1770    resolve_pending_source_drain_fenced(
1771        connector,
1772        status_tx,
1773        active,
1774        "test-source",
1775        ConnectorCancellationPolicy::RetireConnector,
1776        &mut lifecycle,
1777        None,
1778    )
1779    .await
1780}
1781
1782#[cfg(feature = "cluster")]
1783fn publish_source_drain_ready_fenced(
1784    connector: &mut dyn SourceConnector,
1785    control: &SourceDrainLeaseControl,
1786    active: &mut Option<ActiveSourceDrain>,
1787    source_name: &str,
1788    cancellation_policy: ConnectorCancellationPolicy,
1789    lifecycle: &mut SourceConnectorLifecycle,
1790    process_authority: Option<&SourceProcessAuthority>,
1791) -> Result<(), ConnectorError> {
1792    let Some(current) = active.as_mut() else {
1793        return Ok(());
1794    };
1795    if current.ready {
1796        return Ok(());
1797    }
1798    if tokio::time::Instant::now() >= current.prepare_deadline {
1799        return Err(ConnectorError::Internal(format!(
1800            "source drain {:?} exceeded its preparation deadline",
1801            current.request.round
1802        )));
1803    }
1804    let ready = if current.provider_drain {
1805        lifecycle.run_sync_hook(
1806            source_name,
1807            "drain readiness",
1808            current.prepare_deadline,
1809            cancellation_policy,
1810            process_authority,
1811            || connector.poll_drain_ready(current.request.round),
1812        )?
1813    } else {
1814        check_source_sync_fence(
1815            source_name,
1816            "drain readiness",
1817            current.prepare_deadline,
1818            false,
1819            cancellation_policy,
1820            lifecycle,
1821            process_authority,
1822        )?;
1823        true
1824    };
1825    if !ready {
1826        return Ok(());
1827    }
1828    let receipt = SourceDrainReceipt {
1829        round: current.request.round,
1830        participant: current.participant,
1831        source_task_incarnation: control.task_incarnation,
1832    };
1833    if !receipt.is_canonical() {
1834        return Err(ConnectorError::Internal(
1835            "source task produced a non-canonical drain receipt".into(),
1836        ));
1837    }
1838    lifecycle.run_sync_hook(
1839        source_name,
1840        "drain readiness publication",
1841        current.prepare_deadline,
1842        cancellation_policy,
1843        process_authority,
1844        || {
1845            let _ = control
1846                .status_tx
1847                .send_replace(SourceDrainTaskStatus::Ready(receipt));
1848            Ok(())
1849        },
1850    )?;
1851    current.ready = true;
1852    Ok(())
1853}
1854
1855#[cfg(all(test, feature = "cluster"))]
1856fn publish_source_drain_ready(
1857    connector: &mut dyn SourceConnector,
1858    control: &SourceDrainLeaseControl,
1859    active: &mut Option<ActiveSourceDrain>,
1860) -> Result<(), ConnectorError> {
1861    let mut lifecycle = SourceConnectorLifecycle::default();
1862    publish_source_drain_ready_fenced(
1863        connector,
1864        control,
1865        active,
1866        "test-source",
1867        ConnectorCancellationPolicy::RetireConnector,
1868        &mut lifecycle,
1869        None,
1870    )
1871}
1872
1873#[cfg(feature = "cluster")]
1874fn source_drain_flushing(active: Option<&ActiveSourceDrain>) -> bool {
1875    active.is_some_and(|drain| !drain.ready)
1876}
1877
1878#[cfg(feature = "cluster")]
1879fn source_drain_held(active: Option<&ActiveSourceDrain>) -> bool {
1880    active.is_some_and(|drain| drain.ready)
1881}
1882
1883enum SourcePollOutcome {
1884    Completed(Result<Option<SourceBatch>, ConnectorError>),
1885    Deadline,
1886    Shutdown,
1887    #[cfg(feature = "cluster")]
1888    ProcessAuthorityLost,
1889}
1890
1891enum SourceOperationOutcome<T> {
1892    Completed(T),
1893    Deadline,
1894    #[cfg(feature = "cluster")]
1895    ProcessAuthorityLost,
1896}
1897
1898#[derive(Default)]
1899struct SourceConnectorLifecycle {
1900    retired: bool,
1901    data_plane_faulted: bool,
1902    #[cfg(feature = "cluster")]
1903    process_authority_lost: bool,
1904}
1905
1906impl SourceConnectorLifecycle {
1907    fn cancelled(&mut self, cancellation_policy: ConnectorCancellationPolicy) {
1908        if cancellation_policy == ConnectorCancellationPolicy::RetireConnector {
1909            self.retired = true;
1910        }
1911    }
1912
1913    #[cfg(feature = "cluster")]
1914    fn authority_lost(&mut self) {
1915        self.process_authority_lost = true;
1916    }
1917
1918    #[cfg(feature = "cluster")]
1919    fn process_authority_lost(&self) -> bool {
1920        self.process_authority_lost
1921    }
1922
1923    fn may_invoke_connector(&self) -> bool {
1924        !self.retired && {
1925            #[cfg(feature = "cluster")]
1926            {
1927                !self.process_authority_lost
1928            }
1929            #[cfg(not(feature = "cluster"))]
1930            {
1931                true
1932            }
1933        }
1934    }
1935
1936    fn fault_data_plane(&mut self) {
1937        self.data_plane_faulted = true;
1938    }
1939
1940    fn may_poll_or_ack(&self) -> bool {
1941        self.may_invoke_connector() && !self.data_plane_faulted
1942    }
1943
1944    fn run_sync_hook<T>(
1945        &mut self,
1946        source: &str,
1947        operation: &str,
1948        deadline: tokio::time::Instant,
1949        cancellation_policy: ConnectorCancellationPolicy,
1950        #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
1951        hook: impl FnOnce() -> Result<T, ConnectorError>,
1952    ) -> Result<T, ConnectorError> {
1953        check_source_sync_fence(
1954            source,
1955            operation,
1956            deadline,
1957            false,
1958            cancellation_policy,
1959            self,
1960            #[cfg(feature = "cluster")]
1961            process_authority,
1962        )?;
1963        let result = hook();
1964        check_source_sync_fence(
1965            source,
1966            operation,
1967            deadline,
1968            true,
1969            cancellation_policy,
1970            self,
1971            #[cfg(feature = "cluster")]
1972            process_authority,
1973        )?;
1974        result
1975    }
1976}
1977
1978/// Run one connector lifecycle operation behind the process-authority and absolute-deadline
1979/// fences. The future is not constructed after either fence has crossed. Authority and deadline
1980/// also win a ready tie, and a completed branch is revalidated before its result is admitted.
1981async fn run_source_operation<T, F, Fut>(
1982    deadline: tokio::time::Instant,
1983    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
1984    make_future: F,
1985) -> SourceOperationOutcome<T>
1986where
1987    F: FnOnce() -> Fut,
1988    Fut: std::future::Future<Output = T>,
1989{
1990    #[cfg(feature = "cluster")]
1991    if !source_process_authority_is_live(process_authority) {
1992        return SourceOperationOutcome::ProcessAuthorityLost;
1993    }
1994    if tokio::time::Instant::now() >= deadline {
1995        return SourceOperationOutcome::Deadline;
1996    }
1997
1998    let future = make_future();
1999    tokio::pin!(future);
2000
2001    #[cfg(feature = "cluster")]
2002    if let Some(authority) = process_authority {
2003        return tokio::select! {
2004            biased;
2005            () = authority.cancelled() => SourceOperationOutcome::ProcessAuthorityLost,
2006            () = tokio::time::sleep_until(deadline) => SourceOperationOutcome::Deadline,
2007            result = &mut future => {
2008                if !authority.is_live() {
2009                    SourceOperationOutcome::ProcessAuthorityLost
2010                } else if tokio::time::Instant::now() >= deadline {
2011                    SourceOperationOutcome::Deadline
2012                } else {
2013                    SourceOperationOutcome::Completed(result)
2014                }
2015            }
2016        };
2017    }
2018
2019    tokio::select! {
2020        biased;
2021        () = tokio::time::sleep_until(deadline) => SourceOperationOutcome::Deadline,
2022        result = &mut future => {
2023            if tokio::time::Instant::now() >= deadline {
2024                SourceOperationOutcome::Deadline
2025            } else {
2026                SourceOperationOutcome::Completed(result)
2027            }
2028        }
2029    }
2030}
2031
2032fn source_operation_deadline_error(source: &str, operation: &str) -> ConnectorError {
2033    ConnectorError::Internal(format!(
2034        "source '{source}' {operation} exceeded its end-to-end deadline"
2035    ))
2036}
2037
2038fn check_source_sync_fence(
2039    source: &str,
2040    operation: &str,
2041    deadline: tokio::time::Instant,
2042    operation_started: bool,
2043    cancellation_policy: ConnectorCancellationPolicy,
2044    lifecycle: &mut SourceConnectorLifecycle,
2045    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2046) -> Result<(), ConnectorError> {
2047    #[cfg(feature = "cluster")]
2048    if !source_process_authority_is_live(process_authority) {
2049        lifecycle.authority_lost();
2050        return Err(source_operation_authority_error(source, operation));
2051    }
2052    if tokio::time::Instant::now() >= deadline {
2053        if operation_started {
2054            lifecycle.cancelled(cancellation_policy);
2055        }
2056        return Err(source_operation_deadline_error(source, operation));
2057    }
2058    Ok(())
2059}
2060
2061#[cfg(feature = "cluster")]
2062fn source_operation_authority_error(source: &str, operation: &str) -> ConnectorError {
2063    ConnectorError::InvalidState {
2064        expected: "live cluster process lease".into(),
2065        actual: format!("source '{source}' lost process authority during {operation}"),
2066    }
2067}
2068
2069enum SourceStartOutcome {
2070    Completed(Result<(), ConnectorError>),
2071    TimedOut,
2072    #[cfg(feature = "cluster")]
2073    ProcessAuthorityLost,
2074}
2075
2076enum SourceStartFailure {
2077    Connector(String),
2078    Retired(String),
2079    #[cfg(feature = "cluster")]
2080    ProcessAuthorityLost(String),
2081}
2082
2083async fn start_source_once(
2084    connector: &mut dyn SourceConnector,
2085    request: SourceStart,
2086    deadline: tokio::time::Instant,
2087    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2088) -> SourceStartOutcome {
2089    match run_source_operation(
2090        deadline,
2091        #[cfg(feature = "cluster")]
2092        process_authority,
2093        || connector.start(request),
2094    )
2095    .await
2096    {
2097        SourceOperationOutcome::Completed(result) => SourceStartOutcome::Completed(result),
2098        SourceOperationOutcome::Deadline => SourceStartOutcome::TimedOut,
2099        #[cfg(feature = "cluster")]
2100        SourceOperationOutcome::ProcessAuthorityLost => SourceStartOutcome::ProcessAuthorityLost,
2101    }
2102}
2103
2104async fn poll_source_once(
2105    connector: &mut dyn SourceConnector,
2106    max_records: usize,
2107    deadline: tokio::time::Instant,
2108    shutdown: &tokio::sync::Notify,
2109    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2110) -> SourcePollOutcome {
2111    #[cfg(feature = "cluster")]
2112    if !source_process_authority_is_live(process_authority) {
2113        return SourcePollOutcome::ProcessAuthorityLost;
2114    }
2115    if tokio::time::Instant::now() >= deadline {
2116        return SourcePollOutcome::Deadline;
2117    }
2118    let mut poll = std::pin::pin!(connector.poll_batch(max_records));
2119    #[cfg(feature = "cluster")]
2120    if let Some(authority) = process_authority {
2121        return tokio::select! {
2122            biased;
2123            () = authority.cancelled() => {
2124                SourcePollOutcome::ProcessAuthorityLost
2125            }
2126            () = shutdown.notified() => SourcePollOutcome::Shutdown,
2127            () = tokio::time::sleep_until(deadline) => SourcePollOutcome::Deadline,
2128            result = poll.as_mut() => {
2129                if !authority.is_live() {
2130                    SourcePollOutcome::ProcessAuthorityLost
2131                } else if tokio::time::Instant::now() >= deadline {
2132                    SourcePollOutcome::Deadline
2133                } else {
2134                    SourcePollOutcome::Completed(result)
2135                }
2136            },
2137        };
2138    }
2139    tokio::select! {
2140        biased;
2141        () = shutdown.notified() => SourcePollOutcome::Shutdown,
2142        () = tokio::time::sleep_until(deadline) => SourcePollOutcome::Deadline,
2143        result = poll.as_mut() => {
2144            if tokio::time::Instant::now() >= deadline {
2145                SourcePollOutcome::Deadline
2146            } else {
2147                SourcePollOutcome::Completed(result)
2148            }
2149        },
2150    }
2151}
2152
2153/// Backoff between completed polls while still servicing durable commit
2154/// notifications immediately. This never races a live `poll_batch` future.
2155async fn wait_source_idle(
2156    connector: &mut dyn SourceConnector,
2157    epoch_committed_rx: &mut tokio::sync::watch::Receiver<Option<(u64, SourceCheckpoint)>>,
2158    delivery_guarantee: DeliveryGuarantee,
2159    src_name: &str,
2160    fault_tx: &tokio::sync::mpsc::UnboundedSender<SourceFault>,
2161    shutdown: &tokio::sync::Notify,
2162    control_wake: Option<&tokio::sync::Notify>,
2163    operation_timeout: Duration,
2164    cancellation_policy: ConnectorCancellationPolicy,
2165    lifecycle: &mut SourceConnectorLifecycle,
2166    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2167    poll_interval: Duration,
2168) -> bool {
2169    let data_ready = connector.data_ready_notify();
2170    #[cfg(feature = "cluster")]
2171    if let Some(authority) = process_authority {
2172        return tokio::select! {
2173            biased;
2174            () = authority.cancelled() => {
2175                lifecycle.authority_lost();
2176                false
2177            },
2178            () = shutdown.notified() => false,
2179            changed = epoch_committed_rx.changed() => if changed.is_ok() {
2180                acknowledge_latest_source_commit(
2181                    connector,
2182                    epoch_committed_rx,
2183                    delivery_guarantee,
2184                    src_name,
2185                    fault_tx,
2186                    tokio::time::Instant::now() + operation_timeout,
2187                    cancellation_policy,
2188                    lifecycle,
2189                    Some(authority),
2190                ).await
2191            } else {
2192                lifecycle.fault_data_plane();
2193                false
2194            },
2195            () = async move {
2196                match data_ready {
2197                    Some(notify) => notify.notified().await,
2198                    None => std::future::pending().await,
2199                }
2200            } => true,
2201            () = async move {
2202                match control_wake {
2203                    Some(notify) => notify.notified().await,
2204                    None => std::future::pending().await,
2205                }
2206            } => true,
2207            () = tokio::time::sleep(poll_interval) => true,
2208        };
2209    }
2210
2211    tokio::select! {
2212        biased;
2213        () = shutdown.notified() => false,
2214        changed = epoch_committed_rx.changed() => if changed.is_ok() {
2215            acknowledge_latest_source_commit(
2216                connector,
2217                epoch_committed_rx,
2218                delivery_guarantee,
2219                src_name,
2220                fault_tx,
2221                tokio::time::Instant::now() + operation_timeout,
2222                cancellation_policy,
2223                lifecycle,
2224                #[cfg(feature = "cluster")]
2225                None,
2226            ).await
2227        } else {
2228            lifecycle.fault_data_plane();
2229            false
2230        },
2231        () = async move {
2232            match data_ready {
2233                Some(notify) => notify.notified().await,
2234                None => std::future::pending().await,
2235            }
2236        } => true,
2237        () = async move {
2238            match control_wake {
2239                Some(notify) => notify.notified().await,
2240                None => std::future::pending().await,
2241            }
2242        } => true,
2243        () = tokio::time::sleep(poll_interval) => true,
2244    }
2245}
2246
2247#[cfg(feature = "cluster")]
2248async fn wait_source_drain_hold(
2249    connector: &mut dyn SourceConnector,
2250    epoch_committed_rx: &mut tokio::sync::watch::Receiver<Option<(u64, SourceCheckpoint)>>,
2251    delivery_guarantee: DeliveryGuarantee,
2252    src_name: &str,
2253    fault_tx: &tokio::sync::mpsc::UnboundedSender<SourceFault>,
2254    shutdown: &tokio::sync::Notify,
2255    control_wake: &tokio::sync::Notify,
2256    operation_timeout: Duration,
2257    cancellation_policy: ConnectorCancellationPolicy,
2258    lifecycle: &mut SourceConnectorLifecycle,
2259    process_authority: Option<&SourceProcessAuthority>,
2260    poll_interval: Duration,
2261) -> bool {
2262    if let Some(authority) = process_authority {
2263        return tokio::select! {
2264            biased;
2265            () = authority.cancelled() => {
2266                lifecycle.authority_lost();
2267                false
2268            },
2269            () = shutdown.notified() => false,
2270            changed = epoch_committed_rx.changed() => if changed.is_ok() {
2271                acknowledge_latest_source_commit(
2272                    connector,
2273                    epoch_committed_rx,
2274                    delivery_guarantee,
2275                    src_name,
2276                    fault_tx,
2277                    tokio::time::Instant::now() + operation_timeout,
2278                    cancellation_policy,
2279                    lifecycle,
2280                    Some(authority),
2281                ).await
2282            } else {
2283                lifecycle.fault_data_plane();
2284                false
2285            },
2286            () = control_wake.notified() => true,
2287            () = tokio::time::sleep(poll_interval) => true,
2288        };
2289    }
2290
2291    tokio::select! {
2292        biased;
2293        () = shutdown.notified() => false,
2294        changed = epoch_committed_rx.changed() => if changed.is_ok() {
2295            acknowledge_latest_source_commit(
2296                connector,
2297                epoch_committed_rx,
2298                delivery_guarantee,
2299                src_name,
2300                fault_tx,
2301                tokio::time::Instant::now() + operation_timeout,
2302                cancellation_policy,
2303                lifecycle,
2304                None,
2305            ).await
2306        } else {
2307            lifecycle.fault_data_plane();
2308            false
2309        },
2310        () = control_wake.notified() => true,
2311        () = tokio::time::sleep(poll_interval) => true,
2312    }
2313}
2314
2315fn source_barrier_release_covers(released: CheckpointAttempt, held: CheckpointAttempt) -> bool {
2316    matches!(
2317        released.relation_to(held),
2318        CheckpointAttemptRelation::Exact | CheckpointAttemptRelation::Newer
2319    )
2320}
2321
2322/// Hold a source at an emitted barrier until the coordinator releases that exact attempt.
2323///
2324/// The retained watch value closes the release-before-wait race. While held, the source keeps its
2325/// connector control plane and durable upstream acknowledgements live, but never polls data.
2326async fn wait_source_barrier_release(
2327    connector: &mut dyn SourceConnector,
2328    epoch_committed_rx: &mut tokio::sync::watch::Receiver<Option<(u64, SourceCheckpoint)>>,
2329    barrier_release_rx: &mut tokio::sync::watch::Receiver<Option<SourceBarrierSignal>>,
2330    delivery_guarantee: DeliveryGuarantee,
2331    src_name: &str,
2332    fault_tx: &tokio::sync::mpsc::UnboundedSender<SourceFault>,
2333    shutdown: &tokio::sync::Notify,
2334    operation_timeout: Duration,
2335    cancellation_policy: ConnectorCancellationPolicy,
2336    lifecycle: &mut SourceConnectorLifecycle,
2337    #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2338    poll_interval: Duration,
2339    barrier: CheckpointBarrier,
2340) -> bool {
2341    let attempt = CheckpointAttempt::new(barrier.epoch, barrier.checkpoint_id);
2342    loop {
2343        #[cfg(feature = "cluster")]
2344        if !source_process_authority_is_live(process_authority) {
2345            lifecycle.authority_lost();
2346            return false;
2347        }
2348        let signal = *barrier_release_rx.borrow_and_update();
2349        match signal {
2350            Some(SourceBarrierSignal::Release(released))
2351                if source_barrier_release_covers(released, attempt) =>
2352            {
2353                return true;
2354            }
2355            Some(SourceBarrierSignal::Stop) => {
2356                lifecycle.fault_data_plane();
2357                return false;
2358            }
2359            _ => {}
2360        }
2361
2362        let control_deadline = tokio::time::Instant::now() + operation_timeout;
2363        if let Err(error) = lifecycle.run_sync_hook(
2364            src_name,
2365            "barrier-hold control-plane drive",
2366            control_deadline,
2367            cancellation_policy,
2368            #[cfg(feature = "cluster")]
2369            process_authority,
2370            || {
2371                connector.drive_control_plane();
2372                Ok(())
2373            },
2374        ) {
2375            lifecycle.fault_data_plane();
2376            let _ = fault_tx.send(SourceFault {
2377                source: Arc::from(src_name),
2378                error: error.to_string(),
2379            });
2380            return false;
2381        }
2382        #[cfg(feature = "cluster")]
2383        if let Some(authority) = process_authority {
2384            tokio::select! {
2385                biased;
2386                () = authority.cancelled() => {
2387                    lifecycle.authority_lost();
2388                    return false;
2389                },
2390                () = shutdown.notified() => {
2391                    lifecycle.fault_data_plane();
2392                    return false;
2393                },
2394                changed = barrier_release_rx.changed() => {
2395                    if changed.is_err() {
2396                        lifecycle.fault_data_plane();
2397                        return false;
2398                    }
2399                }
2400                changed = epoch_committed_rx.changed() => {
2401                    if changed.is_err() {
2402                        lifecycle.fault_data_plane();
2403                        return false;
2404                    }
2405                    if !acknowledge_latest_source_commit(
2406                        connector,
2407                        epoch_committed_rx,
2408                        delivery_guarantee,
2409                        src_name,
2410                        fault_tx,
2411                        tokio::time::Instant::now() + operation_timeout,
2412                        cancellation_policy,
2413                        lifecycle,
2414                        Some(authority),
2415                    ).await {
2416                        return false;
2417                    }
2418                },
2419                () = tokio::time::sleep(poll_interval) => {}
2420            }
2421            continue;
2422        }
2423
2424        tokio::select! {
2425            biased;
2426            () = shutdown.notified() => {
2427                lifecycle.fault_data_plane();
2428                return false;
2429            },
2430            changed = barrier_release_rx.changed() => {
2431                if changed.is_err() {
2432                    lifecycle.fault_data_plane();
2433                    return false;
2434                }
2435            }
2436            changed = epoch_committed_rx.changed() => {
2437                if changed.is_err() {
2438                    lifecycle.fault_data_plane();
2439                    return false;
2440                }
2441                if !acknowledge_latest_source_commit(
2442                    connector,
2443                    epoch_committed_rx,
2444                    delivery_guarantee,
2445                    src_name,
2446                    fault_tx,
2447                    tokio::time::Instant::now() + operation_timeout,
2448                    cancellation_policy,
2449                    lifecycle,
2450                    #[cfg(feature = "cluster")]
2451                    None,
2452                ).await {
2453                    return false;
2454                }
2455            },
2456            () = tokio::time::sleep(poll_interval) => {}
2457        }
2458    }
2459}
2460
2461impl StreamingCoordinator {
2462    #[cfg(feature = "cluster")]
2463    #[inline]
2464    fn require_process_authority(&self, boundary: &str) -> Result<(), CycleError> {
2465        if self
2466            .process_authority
2467            .as_deref()
2468            .is_none_or(SourceProcessAuthority::is_live)
2469        {
2470            Ok(())
2471        } else {
2472            Err(CycleError::Recovery(format!(
2473                "cluster process lease expired before {boundary}"
2474            )))
2475        }
2476    }
2477
2478    async fn close_startup_source(
2479        source: &mut SourceRegistration,
2480        cleanup_deadline: tokio::time::Instant,
2481        #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2482    ) {
2483        match run_source_operation(
2484            cleanup_deadline,
2485            #[cfg(feature = "cluster")]
2486            process_authority,
2487            || source.connector.close(),
2488        )
2489        .await
2490        {
2491            SourceOperationOutcome::Completed(Ok(())) => {}
2492            SourceOperationOutcome::Completed(Err(error)) => {
2493                tracing::warn!(
2494                    source = %source.name,
2495                    %error,
2496                    "source close failed while rolling back pipeline startup"
2497                );
2498            }
2499            SourceOperationOutcome::Deadline => {
2500                tracing::warn!(
2501                    source = %source.name,
2502                    "source close exceeded its pipeline-startup cleanup deadline"
2503                );
2504            }
2505            #[cfg(feature = "cluster")]
2506            SourceOperationOutcome::ProcessAuthorityLost => {}
2507        }
2508    }
2509
2510    async fn close_prepared_sources(
2511        sources: &mut Vec<PreparedSourceGeneration>,
2512        cleanup_timeout: Duration,
2513        #[cfg(feature = "cluster")] process_authority: Option<&SourceProcessAuthority>,
2514    ) {
2515        let cleanup_deadline = tokio::time::Instant::now() + cleanup_timeout;
2516        futures::future::join_all(sources.iter_mut().map(|source| {
2517            Self::close_startup_source(
2518                &mut source.registration,
2519                cleanup_deadline,
2520                #[cfg(feature = "cluster")]
2521                process_authority,
2522            )
2523        }))
2524        .await;
2525        sources.clear();
2526    }
2527
2528    /// Reap a source task while preserving its generation fence until actual termination.
2529    fn reap_source_task(task: SourceTaskLease) {
2530        if !task.is_finished() {
2531            tracing::warn!(
2532                source = %task.name(),
2533                "source task did not exit within shutdown budget; retiring its connector generation"
2534            );
2535            task.abort();
2536            // The DB-owned lease remains registered until the actor wrapper and connector tracker
2537            // prove actual termination. Coordinator shutdown stays bounded without admitting an
2538            // overlapping replacement generation.
2539            drop(task);
2540            return;
2541        }
2542        task.log_terminal_outcome();
2543        drop(task);
2544    }
2545
2546    /// Notify every source of a committed epoch so it can release retained upstream data.
2547    fn broadcast_epoch_committed(
2548        &self,
2549        epoch: u64,
2550        per_source: &FxHashMap<String, SourceCheckpoint>,
2551    ) {
2552        for handle in &self.source_handles {
2553            let cp = per_source
2554                .get(handle.task.name())
2555                .cloned()
2556                .unwrap_or_else(SourceCheckpoint::new);
2557            let _ = handle.epoch_committed_tx.send(Some((epoch, cp)));
2558        }
2559    }
2560
2561    fn release_source_barrier_attempt(&self, attempt: CheckpointAttempt) {
2562        for handle in &self.source_handles {
2563            handle.barrier_control().release_exact(attempt);
2564        }
2565    }
2566
2567    fn release_source_barrier_for(&self, source_idx: usize, attempt: CheckpointAttempt) {
2568        if let Some(handle) = self.source_handles.get(source_idx) {
2569            handle.barrier_control().release_exact(attempt);
2570        }
2571    }
2572
2573    fn stop_source_barrier_holds(&self) {
2574        for handle in &self.source_handles {
2575            handle.barrier_control().stop_hold();
2576        }
2577    }
2578
2579    fn cancel_local_source_barriers(&self, barrier: CheckpointBarrier) {
2580        for handle in &self.source_handles {
2581            handle.barrier_control().cancel_exact(barrier);
2582        }
2583    }
2584
2585    /// Build the coordinator, atomically start each source connector, and spawn source tasks.
2586    ///
2587    /// # Errors
2588    ///
2589    /// Returns an error if delivery guarantee constraints are violated or a source fails to start
2590    /// at its requested initial/recovered position.
2591    pub async fn new(
2592        runtime: &StreamingCoordinatorRuntime,
2593        sources: Vec<SourceRegistration>,
2594        config: PipelineConfig,
2595        shutdown: Arc<tokio::sync::Notify>,
2596        control_rx: ControlMsgRx,
2597        source_gate: Arc<std::sync::atomic::AtomicBool>,
2598    ) -> Result<Self, DbError> {
2599        let _construction = runtime.construction.lock().await;
2600        runtime.prune_and_require_idle()?;
2601        let generation = runtime.claim_generation()?;
2602        let sources = sources
2603            .into_iter()
2604            .map(|source| {
2605                TrackedSourceRegistration::capture(source, &runtime.owned_connector_task_fences)
2606            })
2607            .collect::<Vec<_>>();
2608        if let Some(source) = sources.iter().find(|source| source.assignment_scoped) {
2609            return Err(DbError::Config(format!(
2610                "assignment-scoped source '{}' requires the database-owned cluster runtime",
2611                source.name
2612            )));
2613        }
2614        let mut coordinator = Self::new_with_tracked_source_registry(
2615            sources,
2616            config,
2617            shutdown,
2618            control_rx,
2619            source_gate,
2620            #[cfg(feature = "cluster")]
2621            None,
2622            Arc::clone(&runtime.owned_source_tasks),
2623            crate::db::RuntimeMode::Local,
2624        )
2625        .await?;
2626        coordinator.public_generation = Some(generation);
2627        Ok(coordinator)
2628    }
2629
2630    #[cfg(all(test, feature = "cluster"))]
2631    pub(crate) async fn new_with_source_registry(
2632        sources: Vec<SourceRegistration>,
2633        config: PipelineConfig,
2634        shutdown: Arc<tokio::sync::Notify>,
2635        control_rx: ControlMsgRx,
2636        source_gate: Arc<std::sync::atomic::AtomicBool>,
2637        #[cfg(feature = "cluster")] source_process_authority: Option<Arc<ClusterController>>,
2638        owned_source_tasks: OwnedSourceTasks,
2639        owned_connector_task_fences: OwnedConnectorTaskFences,
2640        runtime_mode: crate::db::RuntimeMode,
2641    ) -> Result<Self, DbError> {
2642        let sources = sources
2643            .into_iter()
2644            .map(|source| TrackedSourceRegistration::capture(source, &owned_connector_task_fences))
2645            .collect();
2646        Self::new_with_tracked_source_registry(
2647            sources,
2648            config,
2649            shutdown,
2650            control_rx,
2651            source_gate,
2652            #[cfg(feature = "cluster")]
2653            source_process_authority,
2654            owned_source_tasks,
2655            runtime_mode,
2656        )
2657        .await
2658    }
2659
2660    pub(crate) async fn new_with_tracked_source_registry(
2661        sources: Vec<TrackedSourceRegistration>,
2662        config: PipelineConfig,
2663        shutdown: Arc<tokio::sync::Notify>,
2664        control_rx: ControlMsgRx,
2665        source_gate: Arc<std::sync::atomic::AtomicBool>,
2666        #[cfg(feature = "cluster")] source_process_authority: Option<Arc<ClusterController>>,
2667        owned_source_tasks: OwnedSourceTasks,
2668        #[cfg(feature = "cluster")] runtime_mode: crate::db::RuntimeMode,
2669        #[cfg(not(feature = "cluster"))] _runtime_mode: crate::db::RuntimeMode,
2670    ) -> Result<Self, DbError> {
2671        if config
2672            .checkpoint_schedule
2673            .periodic_interval()
2674            .is_some_and(|interval| interval.is_zero())
2675        {
2676            return Err(DbError::Config(
2677                "checkpoint interval must be greater than zero; use manual checkpointing instead"
2678                    .into(),
2679            ));
2680        }
2681        if config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
2682            for src in &sources {
2683                if !src.contract.is_exact_delivery_certified() {
2684                    return Err(DbError::Config(format!(
2685                        "[{}] exactly-once source '{}' is not production-certified",
2686                        laminar_core::error_codes::EXACTLY_ONCE_SOURCE_UNCERTIFIED,
2687                        src.name
2688                    )));
2689                }
2690            }
2691        }
2692        if matches!(
2693            config.delivery_guarantee,
2694            DeliveryGuarantee::AtLeastOnce | DeliveryGuarantee::ExactlyOnce
2695        ) {
2696            for src in &sources {
2697                if !src.contract.supports_replay() {
2698                    return Err(DbError::Config(format!(
2699                        "[LDB-5031] {} requires source '{}' to support replay",
2700                        config.delivery_guarantee, src.name
2701                    )));
2702                }
2703            }
2704            if !config.checkpoint_schedule.is_enabled() {
2705                return Err(DbError::Config(format!(
2706                    "[LDB-5032] {} requires checkpointing to be enabled",
2707                    config.delivery_guarantee
2708                )));
2709            }
2710        }
2711
2712        // A source that releases externally retained data only on durable commit needs
2713        // checkpointing; otherwise that data can grow without bound. Reject the combination up
2714        // front.
2715        if !config.checkpoint_schedule.is_enabled() {
2716            for src in &sources {
2717                if src.contract.requires_checkpointing() {
2718                    return Err(DbError::Config(format!(
2719                        "[LDB-5034] source '{}' requires checkpointing to be enabled: externally \
2720                         retained data is only released at a durable checkpoint",
2721                        src.name
2722                    )));
2723                }
2724            }
2725        }
2726
2727        if config.channel_capacity == 0 {
2728            return Err(DbError::Config(
2729                "[LDB-0010] channel_capacity must be > 0".into(),
2730            ));
2731        }
2732
2733        #[cfg(feature = "cluster")]
2734        if runtime_mode.is_cluster() {
2735            let controller = source_process_authority.as_ref().ok_or_else(|| {
2736                DbError::Config(
2737                    "cluster source runtime requires a cluster controller with process lease authority"
2738                        .into(),
2739                )
2740            })?;
2741            if controller.process_lease_deadline().is_none() {
2742                return Err(DbError::Config(
2743                    "cluster source runtime requires one shared process lease deadline before construction"
2744                        .into(),
2745                ));
2746            }
2747        } else if source_process_authority.is_some() {
2748            return Err(DbError::Config(
2749                "local source runtime cannot install cluster process lease authority".into(),
2750            ));
2751        }
2752
2753        #[cfg(feature = "cluster")]
2754        let source_process_authority = source_process_authority.map(SourceProcessAuthority::new);
2755
2756        let mut source_starts = Vec::with_capacity(sources.len());
2757        for src in sources {
2758            let start = SourceStart::new(
2759                src.config.clone(),
2760                src.position.clone(),
2761                config.delivery_guarantee,
2762            )
2763            .map_err(|error| match &src.position {
2764                SourcePosition::Initial => DbError::Config(format!(
2765                    "source '{}' has an invalid initial startup request: {error}",
2766                    src.name
2767                )),
2768                SourcePosition::Resume { attempt, .. } => DbError::Checkpoint(format!(
2769                    "[LDB-6003] source '{}' has an invalid resume request for checkpoint epoch={} id={}: {error}",
2770                    src.name, attempt.epoch, attempt.checkpoint_id
2771                )),
2772            })?;
2773            source_starts.push((src, start));
2774        }
2775        let source_count = source_starts.len();
2776        let mut prepared_sources = Vec::with_capacity(source_count);
2777        let mut committed_offsets = Vec::with_capacity(source_count);
2778        let source_start_timeout = config.checkpoint_timeout;
2779        let source_start_deadline = tokio::time::Instant::now() + source_start_timeout;
2780
2781        // Do not spawn a polling task until every source has atomically installed its startup
2782        // position. Otherwise a later startup failure detaches the earlier tasks and they keep
2783        // polling without an owner capable of shutting them down.
2784        for (mut src, start) in source_starts {
2785            let src_name = src.name.clone();
2786            let start_position = src.position.clone();
2787            if tokio::time::Instant::now() >= source_start_deadline {
2788                #[cfg(feature = "cluster")]
2789                if !source_process_authority_is_live(source_process_authority.as_deref()) {
2790                    return Err(DbError::Connector(format!(
2791                        "source '{src_name}' start was not attempted: cluster process lease expired"
2792                    )));
2793                }
2794                // `timeout_at` polls its inner future once even when already expired. Starting a
2795                // source can acquire a lease/slot, so never construct or poll it after the shared
2796                // stage budget has been consumed by earlier sources.
2797                Self::close_prepared_sources(
2798                    &mut prepared_sources,
2799                    PipelineConfig::CONNECTOR_STARTUP_CLEANUP_TIMEOUT,
2800                    #[cfg(feature = "cluster")]
2801                    source_process_authority.as_deref(),
2802                )
2803                .await;
2804                let error = format!(
2805                    "shared {source_start_timeout:?} source-start stage deadline exhausted before start began"
2806                );
2807                return match start_position {
2808                    SourcePosition::Initial => Err(DbError::Config(format!(
2809                        "source '{src_name}' start was not attempted: {error}"
2810                    ))),
2811                    SourcePosition::Resume { attempt, .. } => Err(DbError::Checkpoint(format!(
2812                        "[LDB-6003] source '{src_name}' start was not attempted while resuming exact checkpoint epoch={} id={}: {error}",
2813                        attempt.epoch, attempt.checkpoint_id
2814                    ))),
2815                };
2816            }
2817            // Seed with the durable resume position so a pre-data shutdown still checkpoints it.
2818            // Capture it before moving the complete request into `start`; no connector lifecycle
2819            // operation is allowed between configuration and cursor installation.
2820            let committed_offset = match &src.position {
2821                SourcePosition::Initial => None,
2822                SourcePosition::Resume { checkpoint, .. } => Some(checkpoint.clone()),
2823            };
2824            let cancellation_policy = src.connector.cancellation_policy();
2825            let source_start_authorized = {
2826                #[cfg(feature = "cluster")]
2827                {
2828                    source_process_authority_is_live(source_process_authority.as_deref())
2829                }
2830                #[cfg(not(feature = "cluster"))]
2831                {
2832                    true
2833                }
2834            };
2835            let start_error = if source_start_authorized {
2836                match start_source_once(
2837                    src.connector.as_mut(),
2838                    start,
2839                    source_start_deadline,
2840                    #[cfg(feature = "cluster")]
2841                    source_process_authority.as_deref(),
2842                )
2843                .await
2844                {
2845                    SourceStartOutcome::Completed(Ok(())) => {
2846                        #[cfg(feature = "cluster")]
2847                        if source_process_authority_is_live(source_process_authority.as_deref()) {
2848                            None
2849                        } else {
2850                            Some(SourceStartFailure::ProcessAuthorityLost(
2851                                "process lease expired as source start completed".to_owned(),
2852                            ))
2853                        }
2854                        #[cfg(not(feature = "cluster"))]
2855                        None
2856                    }
2857                    SourceStartOutcome::Completed(Err(error)) => {
2858                        Some(if error.is_outcome_unknown() {
2859                            SourceStartFailure::Retired(error.to_string())
2860                        } else {
2861                            SourceStartFailure::Connector(error.to_string())
2862                        })
2863                    }
2864                    SourceStartOutcome::TimedOut => Some(
2865                        if cancellation_policy == ConnectorCancellationPolicy::RetireConnector {
2866                            SourceStartFailure::Retired(format!(
2867                                "exceeded the shared {source_start_timeout:?} source-start stage deadline"
2868                            ))
2869                        } else {
2870                            SourceStartFailure::Connector(format!(
2871                                "exceeded the shared {source_start_timeout:?} source-start stage deadline"
2872                            ))
2873                        },
2874                    ),
2875                    #[cfg(feature = "cluster")]
2876                    SourceStartOutcome::ProcessAuthorityLost => {
2877                        Some(SourceStartFailure::ProcessAuthorityLost(
2878                            "process lease expired while source start was in flight".to_owned(),
2879                        ))
2880                    }
2881                }
2882            } else {
2883                #[cfg(feature = "cluster")]
2884                {
2885                    Some(SourceStartFailure::ProcessAuthorityLost(
2886                        "process lease expired before source start began".to_owned(),
2887                    ))
2888                }
2889                #[cfg(not(feature = "cluster"))]
2890                {
2891                    unreachable!("local source startup is always authorized")
2892                }
2893            };
2894            if let Some(failure) = start_error {
2895                let error = match failure {
2896                    SourceStartFailure::Connector(error) => {
2897                        // A completed failure may have acquired resources, but the connector is
2898                        // still valid for its bounded terminal cleanup.
2899                        prepared_sources.push(PreparedSourceGeneration { registration: src });
2900                        Self::close_prepared_sources(
2901                            &mut prepared_sources,
2902                            PipelineConfig::CONNECTOR_STARTUP_CLEANUP_TIMEOUT,
2903                            #[cfg(feature = "cluster")]
2904                            source_process_authority.as_deref(),
2905                        )
2906                        .await;
2907                        error
2908                    }
2909                    SourceStartFailure::Retired(error) => {
2910                        // The cancelled current generation is terminal. Drop it without invoking
2911                        // close and clean up only sources whose starts completed.
2912                        Self::close_prepared_sources(
2913                            &mut prepared_sources,
2914                            PipelineConfig::CONNECTOR_STARTUP_CLEANUP_TIMEOUT,
2915                            #[cfg(feature = "cluster")]
2916                            source_process_authority.as_deref(),
2917                        )
2918                        .await;
2919                        error
2920                    }
2921                    #[cfg(feature = "cluster")]
2922                    SourceStartFailure::ProcessAuthorityLost(error) => {
2923                        // Generic close may publish externally. Drop every generation without
2924                        // another connector call after the process authority fence.
2925                        error
2926                    }
2927                };
2928                return match start_position {
2929                    SourcePosition::Initial => Err(DbError::Config(format!(
2930                        "source '{src_name}' start failed at initial position: {error}"
2931                    ))),
2932                    SourcePosition::Resume { attempt, .. } => Err(DbError::Checkpoint(format!(
2933                        "[LDB-6003] source '{src_name}' start failed while resuming exact \
2934                             checkpoint epoch={} id={}: {error}",
2935                        attempt.epoch, attempt.checkpoint_id
2936                    ))),
2937                };
2938            }
2939
2940            committed_offsets.push(committed_offset);
2941            prepared_sources.push(PreparedSourceGeneration { registration: src });
2942        }
2943
2944        let (tx, rx) = mpsc::bounded_async::<SourceMsg>(config.channel_capacity);
2945        let (source_fault_tx, source_fault_rx) = tokio::sync::mpsc::unbounded_channel();
2946        let mut source_handles = Vec::with_capacity(source_count);
2947        let mut source_names = Vec::with_capacity(source_count);
2948        let source_runtime = tokio::runtime::Handle::current();
2949
2950        for (idx, prepared) in prepared_sources.into_iter().enumerate() {
2951            let PreparedSourceGeneration { registration } = prepared;
2952            let TrackedSourceRegistration {
2953                source: src,
2954                task_fence,
2955            } = registration;
2956            let terminal_tasks = task_fence.tracker();
2957            let task_shutdown = Arc::new(tokio::sync::Notify::new());
2958            let task_shutdown_clone = Arc::clone(&task_shutdown);
2959            let task_tx = tx.clone();
2960            let task_fault_tx = source_fault_tx.clone();
2961            let task_gate = Arc::clone(&source_gate);
2962            #[cfg(feature = "cluster")]
2963            let task_process_authority = source_process_authority.clone();
2964            let max_poll = config.max_poll_records;
2965            let poll_interval = config.fallback_poll_interval;
2966            let source_operation_timeout = config.checkpoint_timeout;
2967            let delivery_guarantee = config.delivery_guarantee;
2968            let src_name = src.name.clone();
2969            let recovery_cursor = src.contract.supports_replay();
2970            let assignment_scoped = src.assignment_scoped;
2971            let cancellation_policy = src.connector.cancellation_policy();
2972            let mut connector = src.connector;
2973
2974            #[cfg(feature = "cluster")]
2975            let drain_control = runtime_mode.is_cluster().then(|| {
2976                let (command_tx, _) =
2977                    tokio::sync::watch::channel::<Option<SourceDrainCommand>>(None);
2978                let (status_tx, _) = tokio::sync::watch::channel(SourceDrainTaskStatus::Idle);
2979                SourceDrainLeaseControl {
2980                    task_incarnation: uuid::Uuid::new_v4(),
2981                    command_tx,
2982                    status_tx,
2983                    wake: Arc::new(tokio::sync::Notify::new()),
2984                }
2985            });
2986            #[cfg(feature = "cluster")]
2987            let task_drain_control = drain_control.clone();
2988            #[cfg(feature = "cluster")]
2989            let mut task_drain_command_rx = task_drain_control
2990                .as_ref()
2991                .map(|control| control.command_tx.subscribe());
2992            #[cfg(feature = "cluster")]
2993            let task_control_wake = task_drain_control
2994                .as_ref()
2995                .map(|control| Arc::clone(&control.wake));
2996            #[cfg(not(feature = "cluster"))]
2997            let task_control_wake: Option<Arc<tokio::sync::Notify>> = None;
2998
2999            let barrier_injector = CheckpointBarrierInjector::new();
3000            let barrier_handle = barrier_injector.handle();
3001            let (barrier_release_tx, mut barrier_release_rx) =
3002                tokio::sync::watch::channel::<Option<SourceBarrierSignal>>(None);
3003
3004            let (epoch_committed_tx, mut epoch_committed_rx) =
3005                tokio::sync::watch::channel::<Option<(u64, SourceCheckpoint)>>(None);
3006            let (startup_activation_tx, startup_activation_rx) =
3007                crossfire::oneshot::oneshot::<()>();
3008            let expected_shutdown = Arc::new(AtomicBool::new(false));
3009            let task_expected_shutdown = Arc::clone(&expected_shutdown);
3010
3011            let task_exit_guard = SourceTaskExitGuard {
3012                source: Arc::from(src_name.as_str()),
3013                expected_shutdown: task_expected_shutdown,
3014                fault_tx: task_fault_tx.clone(),
3015            };
3016            let (join, actor_terminal) = spawn_source_actor(&source_runtime, async move {
3017                let _exit_guard = task_exit_guard;
3018                // Starting connectors and starting their I/O are separate phases. Keeping every
3019                // task behind this one-shot fence prevents a fast poll/commit failure from being
3020                // queued before the dedicated compute loop can publish its readiness boundary.
3021                // Cancellation before activation closes the connector without polling it.
3022                #[cfg(feature = "cluster")]
3023                let activated = if let Some(authority) = task_process_authority.as_deref() {
3024                    tokio::select! {
3025                        biased;
3026                        () = authority.cancelled() => false,
3027                        () = task_shutdown_clone.notified() => false,
3028                        activation = startup_activation_rx => activation.is_ok(),
3029                    }
3030                } else {
3031                    tokio::select! {
3032                        biased;
3033                        () = task_shutdown_clone.notified() => false,
3034                        activation = startup_activation_rx => activation.is_ok(),
3035                    }
3036                };
3037                #[cfg(not(feature = "cluster"))]
3038                let activated = tokio::select! {
3039                    biased;
3040                    () = task_shutdown_clone.notified() => false,
3041                    activation = startup_activation_rx => activation.is_ok(),
3042                };
3043
3044                if !activated {
3045                    let deadline = tokio::time::Instant::now() + SHUTDOWN_DRAIN_BUDGET;
3046                    match run_source_operation(
3047                        deadline,
3048                        #[cfg(feature = "cluster")]
3049                        task_process_authority.as_deref(),
3050                        || connector.close(),
3051                    )
3052                    .await
3053                    {
3054                        SourceOperationOutcome::Completed(Ok(())) => {}
3055                        SourceOperationOutcome::Completed(Err(error)) => {
3056                            tracing::warn!(source = %src_name, %error, "source close error");
3057                        }
3058                        SourceOperationOutcome::Deadline => {
3059                            tracing::warn!(
3060                                source = %src_name,
3061                                "source close exceeded its shutdown deadline"
3062                            );
3063                        }
3064                        #[cfg(feature = "cluster")]
3065                        SourceOperationOutcome::ProcessAuthorityLost => {}
3066                    }
3067                    return;
3068                }
3069
3070                let mut lifecycle = SourceConnectorLifecycle::default();
3071                let mut pending_barrier = None;
3072                let mut pending_batch: Option<SourceBatch> = None;
3073                #[cfg(feature = "cluster")]
3074                let mut active_source_drain: Option<ActiveSourceDrain> = None;
3075
3076                // Acknowledge a fresh commit before polling more to preserve upstream retention
3077                // and acknowledgement headroom.
3078                loop {
3079                    #[cfg(feature = "cluster")]
3080                    if !source_process_authority_is_live(task_process_authority.as_deref()) {
3081                        lifecycle.authority_lost();
3082                        break;
3083                    }
3084
3085                    #[cfg(feature = "cluster")]
3086                    if let (Some(control), Some(command_rx)) =
3087                        (task_drain_control.as_ref(), task_drain_command_rx.as_mut())
3088                    {
3089                        if let Err(error) = apply_latest_source_drain_command_fenced(
3090                            connector.as_mut(),
3091                            command_rx,
3092                            &control.status_tx,
3093                            &mut active_source_drain,
3094                            assignment_scoped,
3095                            &src_name,
3096                            cancellation_policy,
3097                            &mut lifecycle,
3098                            task_process_authority.as_deref(),
3099                        )
3100                        .await
3101                        {
3102                            if !lifecycle.process_authority_lost() {
3103                                lifecycle.fault_data_plane();
3104                                let _ = task_fault_tx.send(SourceFault {
3105                                    source: Arc::from(src_name.as_str()),
3106                                    error: error.to_string(),
3107                                });
3108                            }
3109                            break;
3110                        }
3111                        // At loop entry every earlier source-channel send has completed. A cut
3112                        // observed in the preceding poll can therefore become externally Ready.
3113                        if pending_batch.is_none() {
3114                            if let Err(error) = publish_source_drain_ready_fenced(
3115                                connector.as_mut(),
3116                                control,
3117                                &mut active_source_drain,
3118                                &src_name,
3119                                cancellation_policy,
3120                                &mut lifecycle,
3121                                task_process_authority.as_deref(),
3122                            ) {
3123                                if !lifecycle.process_authority_lost() {
3124                                    lifecycle.fault_data_plane();
3125                                    let _ = task_fault_tx.send(SourceFault {
3126                                        source: Arc::from(src_name.as_str()),
3127                                        error: error.to_string(),
3128                                    });
3129                                }
3130                                break;
3131                            }
3132                        }
3133                        if let Err(error) = resolve_pending_source_drain_fenced(
3134                            connector.as_mut(),
3135                            &control.status_tx,
3136                            &mut active_source_drain,
3137                            &src_name,
3138                            cancellation_policy,
3139                            &mut lifecycle,
3140                            task_process_authority.as_deref(),
3141                        )
3142                        .await
3143                        {
3144                            if !lifecycle.process_authority_lost() {
3145                                lifecycle.fault_data_plane();
3146                                let _ = task_fault_tx.send(SourceFault {
3147                                    source: Arc::from(src_name.as_str()),
3148                                    error: error.to_string(),
3149                                });
3150                            }
3151                            break;
3152                        }
3153                    }
3154
3155                    #[cfg(feature = "cluster")]
3156                    if !source_process_authority_is_live(task_process_authority.as_deref()) {
3157                        lifecycle.authority_lost();
3158                        break;
3159                    }
3160
3161                    match epoch_committed_rx.has_changed() {
3162                        Ok(true) => {
3163                            if !acknowledge_latest_source_commit(
3164                                connector.as_mut(),
3165                                &mut epoch_committed_rx,
3166                                delivery_guarantee,
3167                                &src_name,
3168                                &task_fault_tx,
3169                                tokio::time::Instant::now() + source_operation_timeout,
3170                                cancellation_policy,
3171                                &mut lifecycle,
3172                                #[cfg(feature = "cluster")]
3173                                task_process_authority.as_deref(),
3174                            )
3175                            .await
3176                            {
3177                                break;
3178                            }
3179                            #[cfg(feature = "cluster")]
3180                            if !source_process_authority_is_live(task_process_authority.as_deref())
3181                            {
3182                                lifecycle.authority_lost();
3183                                break;
3184                            }
3185                            continue;
3186                        }
3187                        Ok(false) => {}
3188                        Err(_) => {
3189                            lifecycle.fault_data_plane();
3190                            break;
3191                        }
3192                    }
3193
3194                    let control_deadline = tokio::time::Instant::now() + source_operation_timeout;
3195                    if let Err(error) = lifecycle.run_sync_hook(
3196                        &src_name,
3197                        "control-plane drive",
3198                        control_deadline,
3199                        cancellation_policy,
3200                        #[cfg(feature = "cluster")]
3201                        task_process_authority.as_deref(),
3202                        || {
3203                            connector.drive_control_plane();
3204                            Ok(())
3205                        },
3206                    ) {
3207                        lifecycle.fault_data_plane();
3208                        let _ = task_fault_tx.send(SourceFault {
3209                            source: Arc::from(src_name.as_str()),
3210                            error: error.to_string(),
3211                        });
3212                        break;
3213                    }
3214
3215                    // A cluster-aware source may observe a new ownership publication before its
3216                    // external consumer has rebound and validated the version-bound handoff
3217                    // cursor. Do not poll data or consume a barrier during that window.
3218                    let checkpoint_ready = match lifecycle.run_sync_hook(
3219                        &src_name,
3220                        "checkpoint readiness",
3221                        control_deadline,
3222                        cancellation_policy,
3223                        #[cfg(feature = "cluster")]
3224                        task_process_authority.as_deref(),
3225                        || connector.checkpoint_ready(),
3226                    ) {
3227                        Ok(ready) => ready,
3228                        Err(error) => {
3229                            lifecycle.fault_data_plane();
3230                            tracing::error!(
3231                                source = %src_name,
3232                                %error,
3233                                "source control-plane reconciliation failed"
3234                            );
3235                            let _ = task_fault_tx.send(SourceFault {
3236                                source: Arc::from(src_name.as_str()),
3237                                error: error.to_string(),
3238                            });
3239                            break;
3240                        }
3241                    };
3242                    if !checkpoint_ready {
3243                        if !wait_source_idle(
3244                            connector.as_mut(),
3245                            &mut epoch_committed_rx,
3246                            delivery_guarantee,
3247                            &src_name,
3248                            &task_fault_tx,
3249                            &task_shutdown_clone,
3250                            task_control_wake.as_deref(),
3251                            source_operation_timeout,
3252                            cancellation_policy,
3253                            &mut lifecycle,
3254                            #[cfg(feature = "cluster")]
3255                            task_process_authority.as_deref(),
3256                            poll_interval,
3257                        )
3258                        .await
3259                        {
3260                            break;
3261                        }
3262                        continue;
3263                    }
3264
3265                    // A source assignment can publish after a batch was drained but before its
3266                    // cursor is captured. Keep the already-polled batch ahead of later data and
3267                    // retry its cursor once reconciliation completes; faulting or dropping here
3268                    // would turn a normal rotation into either downtime or data loss.
3269                    if let Some(batch) = pending_batch.take() {
3270                        match lifecycle.run_sync_hook(
3271                            &src_name,
3272                            "pending batch checkpoint capture",
3273                            control_deadline,
3274                            cancellation_policy,
3275                            #[cfg(feature = "cluster")]
3276                            task_process_authority.as_deref(),
3277                            || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3278                        ) {
3279                            Ok(Some(checkpoint)) => {
3280                                let msg = SourceMsg::Batch {
3281                                    source_idx: idx,
3282                                    batch: batch.records,
3283                                    checkpoint,
3284                                };
3285                                if !send_source_msg(
3286                                    &task_tx,
3287                                    msg,
3288                                    &task_shutdown_clone,
3289                                    #[cfg(feature = "cluster")]
3290                                    task_process_authority.as_deref(),
3291                                )
3292                                .await
3293                                {
3294                                    lifecycle.fault_data_plane();
3295                                    break;
3296                                }
3297                            }
3298                            Ok(None) => {
3299                                pending_batch = Some(batch);
3300                                if !wait_source_idle(
3301                                    connector.as_mut(),
3302                                    &mut epoch_committed_rx,
3303                                    delivery_guarantee,
3304                                    &src_name,
3305                                    &task_fault_tx,
3306                                    &task_shutdown_clone,
3307                                    task_control_wake.as_deref(),
3308                                    source_operation_timeout,
3309                                    cancellation_policy,
3310                                    &mut lifecycle,
3311                                    #[cfg(feature = "cluster")]
3312                                    task_process_authority.as_deref(),
3313                                    poll_interval,
3314                                )
3315                                .await
3316                                {
3317                                    break;
3318                                }
3319                            }
3320                            Err(error) => {
3321                                lifecycle.fault_data_plane();
3322                                let _ = task_fault_tx.send(SourceFault {
3323                                    source: Arc::from(src_name.as_str()),
3324                                    error: error.to_string(),
3325                                });
3326                                break;
3327                            }
3328                        }
3329                        continue;
3330                    }
3331
3332                    let drain_flushing = {
3333                        #[cfg(feature = "cluster")]
3334                        {
3335                            source_drain_flushing(active_source_drain.as_ref())
3336                        }
3337                        #[cfg(not(feature = "cluster"))]
3338                        {
3339                            false
3340                        }
3341                    };
3342                    let drain_held = {
3343                        #[cfg(feature = "cluster")]
3344                        {
3345                            source_drain_held(active_source_drain.as_ref())
3346                        }
3347                        #[cfg(not(feature = "cluster"))]
3348                        {
3349                            false
3350                        }
3351                    };
3352
3353                    // Once claimed, a barrier stays ahead of all later data from this source.
3354                    // A transient publication race retries the same barrier instead of dropping
3355                    // it or polling another batch across the cut.
3356                    if !drain_flushing {
3357                        let barrier = pending_barrier.take().or_else(|| {
3358                            if drain_held {
3359                                barrier_handle.poll()
3360                            } else {
3361                                None
3362                            }
3363                        });
3364                        if let Some(barrier) = barrier {
3365                            match lifecycle.run_sync_hook(
3366                                &src_name,
3367                                "barrier checkpoint capture",
3368                                control_deadline,
3369                                cancellation_policy,
3370                                #[cfg(feature = "cluster")]
3371                                task_process_authority.as_deref(),
3372                                || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3373                            ) {
3374                                Ok(Some(checkpoint)) => {
3375                                    let msg = SourceMsg::Barrier {
3376                                        source_idx: idx,
3377                                        barrier,
3378                                        checkpoint,
3379                                    };
3380                                    if !send_source_msg(
3381                                        &task_tx,
3382                                        msg,
3383                                        &task_shutdown_clone,
3384                                        #[cfg(feature = "cluster")]
3385                                        task_process_authority.as_deref(),
3386                                    )
3387                                    .await
3388                                    {
3389                                        lifecycle.fault_data_plane();
3390                                        break;
3391                                    }
3392                                    if !wait_source_barrier_release(
3393                                        connector.as_mut(),
3394                                        &mut epoch_committed_rx,
3395                                        &mut barrier_release_rx,
3396                                        delivery_guarantee,
3397                                        &src_name,
3398                                        &task_fault_tx,
3399                                        &task_shutdown_clone,
3400                                        source_operation_timeout,
3401                                        cancellation_policy,
3402                                        &mut lifecycle,
3403                                        #[cfg(feature = "cluster")]
3404                                        task_process_authority.as_deref(),
3405                                        poll_interval,
3406                                        barrier,
3407                                    )
3408                                    .await
3409                                    {
3410                                        break;
3411                                    }
3412                                }
3413                                Ok(None) => {
3414                                    pending_barrier = Some(barrier);
3415                                    if !wait_source_idle(
3416                                        connector.as_mut(),
3417                                        &mut epoch_committed_rx,
3418                                        delivery_guarantee,
3419                                        &src_name,
3420                                        &task_fault_tx,
3421                                        &task_shutdown_clone,
3422                                        task_control_wake.as_deref(),
3423                                        source_operation_timeout,
3424                                        cancellation_policy,
3425                                        &mut lifecycle,
3426                                        #[cfg(feature = "cluster")]
3427                                        task_process_authority.as_deref(),
3428                                        poll_interval,
3429                                    )
3430                                    .await
3431                                    {
3432                                        break;
3433                                    }
3434                                }
3435                                Err(error) => {
3436                                    lifecycle.fault_data_plane();
3437                                    let _ = task_fault_tx.send(SourceFault {
3438                                        source: Arc::from(src_name.as_str()),
3439                                        error: error.to_string(),
3440                                    });
3441                                    break;
3442                                }
3443                            }
3444                            continue;
3445                        }
3446                    }
3447
3448                    #[cfg(feature = "cluster")]
3449                    if drain_held {
3450                        let control = task_drain_control
3451                            .as_ref()
3452                            .expect("active source drain has task control");
3453                        if !wait_source_drain_hold(
3454                            connector.as_mut(),
3455                            &mut epoch_committed_rx,
3456                            delivery_guarantee,
3457                            &src_name,
3458                            &task_fault_tx,
3459                            &task_shutdown_clone,
3460                            &control.wake,
3461                            source_operation_timeout,
3462                            cancellation_policy,
3463                            &mut lifecycle,
3464                            task_process_authority.as_deref(),
3465                            poll_interval,
3466                        )
3467                        .await
3468                        {
3469                            break;
3470                        }
3471                        continue;
3472                    }
3473
3474                    // Source-intake gate: held closed during a coordinated round until the
3475                    // restore quorum, so a rewound source doesn't re-shuffle its replay into a
3476                    // peer whose receiver hasn't rebound (the frames would be dropped). The
3477                    // compute loop keeps draining the shuffle receiver on idle cycles meanwhile.
3478                    if task_gate.load(std::sync::atomic::Ordering::Acquire) && !drain_flushing {
3479                        // Preserve a claimed barrier ahead of later data while the strong startup
3480                        // or recovery fence is closed. The coordinator will not fold it until
3481                        // authority reopens. A drain predecessor keeps this strong gate open and
3482                        // uses the held-drain path above for its pre-rotation barrier.
3483                        if let Some(barrier) =
3484                            pending_barrier.take().or_else(|| barrier_handle.poll())
3485                        {
3486                            match lifecycle.run_sync_hook(
3487                                &src_name,
3488                                "gated barrier checkpoint capture",
3489                                control_deadline,
3490                                cancellation_policy,
3491                                #[cfg(feature = "cluster")]
3492                                task_process_authority.as_deref(),
3493                                || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3494                            ) {
3495                                Ok(Some(checkpoint)) => {
3496                                    let msg = SourceMsg::Barrier {
3497                                        source_idx: idx,
3498                                        barrier,
3499                                        checkpoint,
3500                                    };
3501                                    if !send_source_msg(
3502                                        &task_tx,
3503                                        msg,
3504                                        &task_shutdown_clone,
3505                                        #[cfg(feature = "cluster")]
3506                                        task_process_authority.as_deref(),
3507                                    )
3508                                    .await
3509                                    {
3510                                        lifecycle.fault_data_plane();
3511                                        break;
3512                                    }
3513                                    if !wait_source_barrier_release(
3514                                        connector.as_mut(),
3515                                        &mut epoch_committed_rx,
3516                                        &mut barrier_release_rx,
3517                                        delivery_guarantee,
3518                                        &src_name,
3519                                        &task_fault_tx,
3520                                        &task_shutdown_clone,
3521                                        source_operation_timeout,
3522                                        cancellation_policy,
3523                                        &mut lifecycle,
3524                                        #[cfg(feature = "cluster")]
3525                                        task_process_authority.as_deref(),
3526                                        poll_interval,
3527                                        barrier,
3528                                    )
3529                                    .await
3530                                    {
3531                                        break;
3532                                    }
3533                                    continue;
3534                                }
3535                                Ok(None) => pending_barrier = Some(barrier),
3536                                Err(error) => {
3537                                    lifecycle.fault_data_plane();
3538                                    let _ = task_fault_tx.send(SourceFault {
3539                                        source: Arc::from(src_name.as_str()),
3540                                        error: error.to_string(),
3541                                    });
3542                                    break;
3543                                }
3544                            }
3545                        }
3546                        if !wait_source_idle(
3547                            connector.as_mut(),
3548                            &mut epoch_committed_rx,
3549                            delivery_guarantee,
3550                            &src_name,
3551                            &task_fault_tx,
3552                            &task_shutdown_clone,
3553                            task_control_wake.as_deref(),
3554                            source_operation_timeout,
3555                            cancellation_policy,
3556                            &mut lifecycle,
3557                            #[cfg(feature = "cluster")]
3558                            task_process_authority.as_deref(),
3559                            poll_interval,
3560                        )
3561                        .await
3562                        {
3563                            break;
3564                        }
3565                        continue;
3566                    }
3567                    #[cfg(feature = "cluster")]
3568                    if !source_process_authority_is_live(task_process_authority.as_deref()) {
3569                        lifecycle.authority_lost();
3570                        break;
3571                    }
3572                    let poll_deadline = tokio::time::Instant::now() + source_operation_timeout;
3573                    let poll_result = match poll_source_once(
3574                        connector.as_mut(),
3575                        max_poll,
3576                        poll_deadline,
3577                        &task_shutdown_clone,
3578                        #[cfg(feature = "cluster")]
3579                        task_process_authority.as_deref(),
3580                    )
3581                    .await
3582                    {
3583                        SourcePollOutcome::Completed(result) => result,
3584                        SourcePollOutcome::Deadline => {
3585                            lifecycle.cancelled(cancellation_policy);
3586                            lifecycle.fault_data_plane();
3587                            let error = source_operation_deadline_error(&src_name, "poll");
3588                            let _ = task_fault_tx.send(SourceFault {
3589                                source: Arc::from(src_name.as_str()),
3590                                error: error.to_string(),
3591                            });
3592                            break;
3593                        }
3594                        SourcePollOutcome::Shutdown => {
3595                            lifecycle.cancelled(cancellation_policy);
3596                            break;
3597                        }
3598                        #[cfg(feature = "cluster")]
3599                        SourcePollOutcome::ProcessAuthorityLost => {
3600                            lifecycle.authority_lost();
3601                            break;
3602                        }
3603                    };
3604
3605                    #[cfg(feature = "cluster")]
3606                    if !source_process_authority_is_live(task_process_authority.as_deref()) {
3607                        lifecycle.authority_lost();
3608                        break;
3609                    }
3610
3611                    match poll_result {
3612                        Ok(Some(batch)) => {
3613                            let checkpoint = match lifecycle.run_sync_hook(
3614                                &src_name,
3615                                "polled batch checkpoint capture",
3616                                poll_deadline,
3617                                cancellation_policy,
3618                                #[cfg(feature = "cluster")]
3619                                task_process_authority.as_deref(),
3620                                || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3621                            ) {
3622                                Ok(Some(checkpoint)) => checkpoint,
3623                                Ok(None) => {
3624                                    pending_batch = Some(batch);
3625                                    continue;
3626                                }
3627                                Err(error) => {
3628                                    lifecycle.fault_data_plane();
3629                                    let _ = task_fault_tx.send(SourceFault {
3630                                        source: Arc::from(src_name.as_str()),
3631                                        error: error.to_string(),
3632                                    });
3633                                    break;
3634                                }
3635                            };
3636                            let msg = SourceMsg::Batch {
3637                                source_idx: idx,
3638                                batch: batch.records,
3639                                checkpoint,
3640                            };
3641                            if !send_source_msg(
3642                                &task_tx,
3643                                msg,
3644                                &task_shutdown_clone,
3645                                #[cfg(feature = "cluster")]
3646                                task_process_authority.as_deref(),
3647                            )
3648                            .await
3649                            {
3650                                lifecycle.fault_data_plane();
3651                                break; // Coordinator dropped
3652                            }
3653                        }
3654                        Ok(None) => {
3655                            #[cfg(feature = "cluster")]
3656                            let drain_became_ready = if let Some(control) =
3657                                task_drain_control.as_ref()
3658                            {
3659                                let was_flushing =
3660                                    source_drain_flushing(active_source_drain.as_ref());
3661                                if let Err(error) = publish_source_drain_ready_fenced(
3662                                    connector.as_mut(),
3663                                    control,
3664                                    &mut active_source_drain,
3665                                    &src_name,
3666                                    cancellation_policy,
3667                                    &mut lifecycle,
3668                                    task_process_authority.as_deref(),
3669                                ) {
3670                                    lifecycle.fault_data_plane();
3671                                    let _ = task_fault_tx.send(SourceFault {
3672                                        source: Arc::from(src_name.as_str()),
3673                                        error: error.to_string(),
3674                                    });
3675                                    break;
3676                                }
3677                                was_flushing && !source_drain_flushing(active_source_drain.as_ref())
3678                            } else {
3679                                false
3680                            };
3681                            #[cfg(not(feature = "cluster"))]
3682                            let drain_became_ready = false;
3683                            if drain_became_ready {
3684                                continue;
3685                            }
3686                            if !wait_source_idle(
3687                                connector.as_mut(),
3688                                &mut epoch_committed_rx,
3689                                delivery_guarantee,
3690                                &src_name,
3691                                &task_fault_tx,
3692                                &task_shutdown_clone,
3693                                task_control_wake.as_deref(),
3694                                source_operation_timeout,
3695                                cancellation_policy,
3696                                &mut lifecycle,
3697                                #[cfg(feature = "cluster")]
3698                                task_process_authority.as_deref(),
3699                                poll_interval,
3700                            )
3701                            .await
3702                            {
3703                                break;
3704                            }
3705                        }
3706                        Err(e) if !e.is_transient() => {
3707                            lifecycle.fault_data_plane();
3708                            tracing::error!(source = %src_name, error = %e, "terminal poll error");
3709                            // Delivery semantics may permit dropping individual records, never a
3710                            // configured producer. Surface terminal source loss in every mode so
3711                            // the lifecycle cannot remain Running with incomplete input.
3712                            let _ = task_fault_tx.send(SourceFault {
3713                                source: Arc::from(src_name.as_str()),
3714                                error: e.to_string(),
3715                            });
3716                            break;
3717                        }
3718                        Err(e) => {
3719                            tracing::warn!(source = %src_name, error = %e, "poll error (retrying)");
3720                            if !wait_source_idle(
3721                                connector.as_mut(),
3722                                &mut epoch_committed_rx,
3723                                delivery_guarantee,
3724                                &src_name,
3725                                &task_fault_tx,
3726                                &task_shutdown_clone,
3727                                task_control_wake.as_deref(),
3728                                source_operation_timeout,
3729                                cancellation_policy,
3730                                &mut lifecycle,
3731                                #[cfg(feature = "cluster")]
3732                                task_process_authority.as_deref(),
3733                                poll_interval,
3734                            )
3735                            .await
3736                            {
3737                                break;
3738                            }
3739                        }
3740                    }
3741
3742                    let drain_flushing = {
3743                        #[cfg(feature = "cluster")]
3744                        {
3745                            source_drain_flushing(active_source_drain.as_ref())
3746                        }
3747                        #[cfg(not(feature = "cluster"))]
3748                        {
3749                            false
3750                        }
3751                    };
3752                    if !drain_flushing {
3753                        if let Some(barrier) =
3754                            pending_barrier.take().or_else(|| barrier_handle.poll())
3755                        {
3756                            let barrier_deadline =
3757                                tokio::time::Instant::now() + source_operation_timeout;
3758                            match lifecycle.run_sync_hook(
3759                                &src_name,
3760                                "post-poll barrier checkpoint capture",
3761                                barrier_deadline,
3762                                cancellation_policy,
3763                                #[cfg(feature = "cluster")]
3764                                task_process_authority.as_deref(),
3765                                || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3766                            ) {
3767                                Ok(Some(checkpoint)) => {
3768                                    let msg = SourceMsg::Barrier {
3769                                        source_idx: idx,
3770                                        barrier,
3771                                        checkpoint,
3772                                    };
3773                                    if !send_source_msg(
3774                                        &task_tx,
3775                                        msg,
3776                                        &task_shutdown_clone,
3777                                        #[cfg(feature = "cluster")]
3778                                        task_process_authority.as_deref(),
3779                                    )
3780                                    .await
3781                                    {
3782                                        lifecycle.fault_data_plane();
3783                                        break;
3784                                    }
3785                                    if !wait_source_barrier_release(
3786                                        connector.as_mut(),
3787                                        &mut epoch_committed_rx,
3788                                        &mut barrier_release_rx,
3789                                        delivery_guarantee,
3790                                        &src_name,
3791                                        &task_fault_tx,
3792                                        &task_shutdown_clone,
3793                                        source_operation_timeout,
3794                                        cancellation_policy,
3795                                        &mut lifecycle,
3796                                        #[cfg(feature = "cluster")]
3797                                        task_process_authority.as_deref(),
3798                                        poll_interval,
3799                                        barrier,
3800                                    )
3801                                    .await
3802                                    {
3803                                        break;
3804                                    }
3805                                }
3806                                Ok(None) => pending_barrier = Some(barrier),
3807                                Err(error) => {
3808                                    lifecycle.fault_data_plane();
3809                                    let _ = task_fault_tx.send(SourceFault {
3810                                        source: Arc::from(src_name.as_str()),
3811                                        error: error.to_string(),
3812                                    });
3813                                    break;
3814                                }
3815                            }
3816                        }
3817                    }
3818                }
3819
3820                #[cfg(feature = "cluster")]
3821                if !source_process_authority_is_live(task_process_authority.as_deref()) {
3822                    lifecycle.authority_lost();
3823                }
3824                #[cfg(feature = "cluster")]
3825                let may_flush_on_shutdown =
3826                    lifecycle.may_poll_or_ack() && active_source_drain.is_none();
3827                #[cfg(not(feature = "cluster"))]
3828                let may_flush_on_shutdown = lifecycle.may_poll_or_ack();
3829
3830                let shutdown_deadline = tokio::time::Instant::now() + SHUTDOWN_DRAIN_BUDGET;
3831                if may_flush_on_shutdown {
3832                    // Tail polling, durable acknowledgement, and close share one absolute
3833                    // shutdown budget. Unflushed rows resume from the committed offset.
3834                    let mut tail_poll_allowed = true;
3835                    if let Some(batch) = pending_batch.take() {
3836                        match lifecycle.run_sync_hook(
3837                            &src_name,
3838                            "shutdown pending batch checkpoint capture",
3839                            shutdown_deadline,
3840                            cancellation_policy,
3841                            #[cfg(feature = "cluster")]
3842                            task_process_authority.as_deref(),
3843                            || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3844                        ) {
3845                            Ok(Some(checkpoint)) => {
3846                                if task_tx
3847                                    .try_send(SourceMsg::Batch {
3848                                        source_idx: idx,
3849                                        batch: batch.records,
3850                                        checkpoint,
3851                                    })
3852                                    .is_err()
3853                                {
3854                                    lifecycle.fault_data_plane();
3855                                    tail_poll_allowed = false;
3856                                }
3857                            }
3858                            Ok(None) => tail_poll_allowed = false,
3859                            Err(error) => {
3860                                lifecycle.fault_data_plane();
3861                                tail_poll_allowed = false;
3862                                let _ = task_fault_tx.send(SourceFault {
3863                                    source: Arc::from(src_name.as_str()),
3864                                    error: error.to_string(),
3865                                });
3866                            }
3867                        }
3868                    }
3869                    while tail_poll_allowed
3870                        && lifecycle.may_poll_or_ack()
3871                        && tokio::time::Instant::now() < shutdown_deadline
3872                    {
3873                        let poll_result = match run_source_operation(
3874                            shutdown_deadline,
3875                            #[cfg(feature = "cluster")]
3876                            task_process_authority.as_deref(),
3877                            || connector.poll_batch(max_poll),
3878                        )
3879                        .await
3880                        {
3881                            SourceOperationOutcome::Completed(result) => Some(result),
3882                            SourceOperationOutcome::Deadline => {
3883                                lifecycle.cancelled(cancellation_policy);
3884                                None
3885                            }
3886                            #[cfg(feature = "cluster")]
3887                            SourceOperationOutcome::ProcessAuthorityLost => {
3888                                lifecycle.authority_lost();
3889                                None
3890                            }
3891                        };
3892                        if !lifecycle.may_invoke_connector() {
3893                            break;
3894                        }
3895                        match poll_result {
3896                            Some(Ok(Some(batch))) => {
3897                                let checkpoint = match lifecycle.run_sync_hook(
3898                                    &src_name,
3899                                    "shutdown tail checkpoint capture",
3900                                    shutdown_deadline,
3901                                    cancellation_policy,
3902                                    #[cfg(feature = "cluster")]
3903                                    task_process_authority.as_deref(),
3904                                    || try_source_checkpoint(connector.as_ref(), assignment_scoped),
3905                                ) {
3906                                    Ok(Some(checkpoint)) => checkpoint,
3907                                    Ok(None) => break,
3908                                    Err(error) => {
3909                                        lifecycle.fault_data_plane();
3910                                        let _ = task_fault_tx.send(SourceFault {
3911                                            source: Arc::from(src_name.as_str()),
3912                                            error: error.to_string(),
3913                                        });
3914                                        break;
3915                                    }
3916                                };
3917                                let msg = SourceMsg::Batch {
3918                                    source_idx: idx,
3919                                    batch: batch.records,
3920                                    checkpoint,
3921                                };
3922                                if task_tx.try_send(msg).is_err() {
3923                                    lifecycle.fault_data_plane();
3924                                    break;
3925                                }
3926                                if tokio::time::Instant::now() >= shutdown_deadline {
3927                                    break;
3928                                }
3929                            }
3930                            Some(Err(error)) if !error.is_transient() => {
3931                                lifecycle.fault_data_plane();
3932                                let _ = task_fault_tx.send(SourceFault {
3933                                    source: Arc::from(src_name.as_str()),
3934                                    error: error.to_string(),
3935                                });
3936                                break;
3937                            }
3938                            Some(Ok(None) | Err(_)) | None => break,
3939                        }
3940                    }
3941                }
3942
3943                if lifecycle.may_poll_or_ack() {
3944                    // Drain EpochCommitted broadcasts before close so a durable tail settled
3945                    // during shutdown is acknowledged upstream. The watch retains the newest
3946                    // value; waiting for another change here would consume the close budget.
3947                    while matches!(epoch_committed_rx.has_changed(), Ok(true)) {
3948                        if !acknowledge_latest_source_commit(
3949                            connector.as_mut(),
3950                            &mut epoch_committed_rx,
3951                            delivery_guarantee,
3952                            &src_name,
3953                            &task_fault_tx,
3954                            shutdown_deadline,
3955                            cancellation_policy,
3956                            &mut lifecycle,
3957                            #[cfg(feature = "cluster")]
3958                            task_process_authority.as_deref(),
3959                        )
3960                        .await
3961                        {
3962                            break;
3963                        }
3964                    }
3965                }
3966
3967                if lifecycle.may_invoke_connector() {
3968                    match run_source_operation(
3969                        shutdown_deadline,
3970                        #[cfg(feature = "cluster")]
3971                        task_process_authority.as_deref(),
3972                        || connector.close(),
3973                    )
3974                    .await
3975                    {
3976                        SourceOperationOutcome::Completed(Ok(())) => {}
3977                        SourceOperationOutcome::Completed(Err(error)) => {
3978                            tracing::warn!(source = %src_name, %error, "source close error");
3979                        }
3980                        SourceOperationOutcome::Deadline => {
3981                            tracing::warn!(
3982                                source = %src_name,
3983                                "source close exceeded its shutdown deadline"
3984                            );
3985                        }
3986                        #[cfg(feature = "cluster")]
3987                        SourceOperationOutcome::ProcessAuthorityLost => {}
3988                    }
3989                }
3990            });
3991
3992            let arc_name: Arc<str> = Arc::from(src.name.as_str());
3993            let task = SourceTaskLease::supervise(
3994                Arc::clone(&arc_name),
3995                Arc::clone(&task_shutdown),
3996                Arc::clone(&expected_shutdown),
3997                join,
3998                actor_terminal,
3999                terminal_tasks,
4000                &source_runtime,
4001            );
4002            #[cfg(feature = "cluster")]
4003            if let Some(control) = drain_control {
4004                task.install_drain_control(control);
4005            }
4006            owned_source_tasks.lock().push(task.clone());
4007            source_handles.push(SourceHandle {
4008                recovery_cursor,
4009                task,
4010                startup_activation: Some(startup_activation_tx),
4011                barrier_injector,
4012                barrier_release_tx,
4013                epoch_committed_tx,
4014            });
4015            source_names.push(arc_name);
4016            task_fence.handoff();
4017        }
4018
4019        Ok(Self {
4020            config,
4021            rx,
4022            source_fault_rx,
4023            source_handles,
4024            source_names,
4025            shutdown,
4026            terminal_shutdown: tokio_util::sync::CancellationToken::new(),
4027            pending_barrier: PendingBarrier::new(),
4028            last_checkpoint: Instant::now(),
4029            checkpoint_retry_not_before: None,
4030            checkpoint_retry_backoff: Duration::ZERO,
4031            source_batches_buf: FxHashMap::default(),
4032            parked_source_msg: None,
4033            pending_watermark_batches: Vec::new(),
4034            barrier_seen: FxHashSet::default(),
4035            pending_offsets: vec![None; committed_offsets.len()],
4036            replay_pending: false,
4037            committed_offsets,
4038            control_rx,
4039            checkpoint_complete_rx: None,
4040            force_ckpt_rx: None,
4041            manual_waiting: Vec::new(),
4042            manual_active: None,
4043            checkpoint_in_flight: Arc::new(AtomicU64::new(0)),
4044            last_published_checkpoint: None,
4045            coordinated_commit_admission: None,
4046            public_generation: None,
4047            #[cfg(feature = "cluster")]
4048            process_authority: source_process_authority,
4049        })
4050    }
4051
4052    /// Wire in the callback's admission counter so the coordinator gates new barriers.
4053    pub(crate) fn with_checkpoint_admission(mut self, in_flight: Arc<AtomicU64>) -> Self {
4054        self.checkpoint_in_flight = in_flight;
4055        self
4056    }
4057
4058    pub(crate) fn with_coordinated_commit_admission(
4059        mut self,
4060        admission: Option<crate::checkpoint_coordinator::CoordinatedCommitAdmission>,
4061    ) -> Self {
4062        self.coordinated_commit_admission = admission;
4063        self
4064    }
4065
4066    pub(crate) fn with_checkpoint_complete_rx(
4067        mut self,
4068        rx: crossfire::AsyncRx<crossfire::mpsc::Array<CheckpointCompletion>>,
4069    ) -> Self {
4070        self.checkpoint_complete_rx = Some(rx);
4071        self
4072    }
4073
4074    pub(crate) fn with_force_checkpoint_rx(mut self, rx: crate::db::ForceCheckpointRx) -> Self {
4075        self.force_ckpt_rx = Some(rx);
4076        self
4077    }
4078
4079    pub(crate) fn with_terminal_shutdown(
4080        mut self,
4081        shutdown: tokio_util::sync::CancellationToken,
4082    ) -> Self {
4083        self.terminal_shutdown = shutdown;
4084        self
4085    }
4086
4087    fn drain_manual_requests(&mut self) {
4088        let Some(rx) = self.force_ckpt_rx.as_ref() else {
4089            return;
4090        };
4091        while let Ok(reply) = rx.try_recv() {
4092            self.manual_waiting.push(reply);
4093        }
4094    }
4095
4096    fn activate_manual_attempt(&mut self, attempt: CheckpointAttempt) {
4097        if self.manual_waiting.is_empty() {
4098            return;
4099        }
4100        debug_assert!(self.manual_active.is_none());
4101        self.manual_active = Some(ManualCheckpointAttempt {
4102            attempt,
4103            replies: std::mem::take(&mut self.manual_waiting),
4104        });
4105    }
4106
4107    fn fail_waiting_manual(&mut self, error: impl Into<String>) {
4108        let error = error.into();
4109        for reply in self.manual_waiting.drain(..) {
4110            reply.send(Err(DbError::Checkpoint(error.clone())));
4111        }
4112    }
4113
4114    fn finish_manual_success(
4115        &mut self,
4116        attempt: CheckpointAttempt,
4117        result: &crate::checkpoint_coordinator::CheckpointResult,
4118    ) {
4119        let Some(active) = self.manual_active.take() else {
4120            return;
4121        };
4122        if active.attempt != attempt {
4123            self.manual_active = Some(active);
4124            return;
4125        }
4126        let completed = CheckpointAttempt::new(result.epoch, result.checkpoint_id);
4127        if completed != attempt || !result.success {
4128            let reason = format!(
4129                "manual checkpoint terminal result mismatch: admitted epoch={} id={}, \
4130                 completed epoch={} id={} success={}",
4131                attempt.epoch,
4132                attempt.checkpoint_id,
4133                completed.epoch,
4134                completed.checkpoint_id,
4135                result.success,
4136            );
4137            for reply in active.replies {
4138                reply.send(Err(DbError::Checkpoint(reason.clone())));
4139            }
4140            return;
4141        }
4142        for reply in active.replies {
4143            reply.send(Ok(result.clone()));
4144        }
4145    }
4146
4147    fn fail_manual_attempt(&mut self, attempt: CheckpointAttempt, error: impl Into<String>) {
4148        let Some(active) = self.manual_active.take() else {
4149            return;
4150        };
4151        if active.attempt != attempt {
4152            self.manual_active = Some(active);
4153            return;
4154        }
4155        let error = error.into();
4156        for reply in active.replies {
4157            reply.send(Err(DbError::Checkpoint(error.clone())));
4158        }
4159    }
4160
4161    fn fail_all_manual(&mut self, error: &str) {
4162        self.fail_waiting_manual(error);
4163        if let Some(active) = self.manual_active.take() {
4164            for reply in active.replies {
4165                reply.send(Err(DbError::Checkpoint(error.to_owned())));
4166            }
4167        }
4168    }
4169
4170    async fn cleanup_checkpoint_attempt(
4171        callback: &mut impl PipelineCallback,
4172        cleanup_owner: CheckpointCleanupOwner,
4173        attempt: CheckpointAttempt,
4174        reason: &str,
4175        assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
4176    ) -> Result<(), String> {
4177        match cleanup_owner {
4178            CheckpointCleanupOwner::Originator => {
4179                callback
4180                    .abandon_checkpoint_attempt(attempt, reason, assignment_fence)
4181                    .await
4182            }
4183            CheckpointCleanupOwner::Follower => {
4184                callback
4185                    .cancel_source_barrier_attempt(attempt, reason)
4186                    .await
4187            }
4188        }
4189    }
4190
4191    async fn cancel_pending_barrier_for_stop(
4192        &mut self,
4193        callback: &mut impl PipelineCallback,
4194        reason: &str,
4195        release_sources: bool,
4196    ) -> Result<(), String> {
4197        let was_active = self.pending_barrier.active;
4198        let assignment_fence = self.pending_barrier.assignment_fence.clone();
4199        let attempt = self.pending_barrier.take_active_attempt();
4200        self.barrier_seen.clear();
4201
4202        match attempt {
4203            Some((attempt, cleanup_owner)) => {
4204                tracing::info!(
4205                    checkpoint_id = attempt.checkpoint_id,
4206                    epoch = attempt.epoch,
4207                    reason,
4208                    "abandoning checkpoint interrupted before source alignment"
4209                );
4210                if cleanup_owner == CheckpointCleanupOwner::Originator {
4211                    self.cancel_local_source_barriers(CheckpointBarrier::new(
4212                        attempt.checkpoint_id,
4213                        attempt.epoch,
4214                    ));
4215                }
4216                Self::cleanup_checkpoint_attempt(
4217                    callback,
4218                    cleanup_owner,
4219                    attempt,
4220                    reason,
4221                    assignment_fence,
4222                )
4223                .await?;
4224                if release_sources {
4225                    self.release_source_barrier_attempt(attempt);
4226                }
4227                callback.record_checkpoint_failure(attempt.checkpoint_id, reason);
4228                self.fail_manual_attempt(
4229                    attempt,
4230                    format!("manual checkpoint was interrupted: {reason}"),
4231                );
4232            }
4233            None if was_active => {
4234                callback.record_checkpoint_admission_failure(
4235                    "active source barrier had no exact reserved attempt during shutdown",
4236                );
4237                return Err(
4238                    "active source barrier had no exact reserved attempt during shutdown".into(),
4239                );
4240            }
4241            None => {}
4242        }
4243        Ok(())
4244    }
4245
4246    /// Settle every captured durable tail before source or sink lifecycle teardown.
4247    ///
4248    /// The counter is claimed synchronously before a tail is spawned. A tail sends its terminal
4249    /// completion before dropping the claim, so waiting for zero and then draining the channel
4250    /// preserves exact source acknowledgements, public barriers, and manual replies. The tick
4251    /// handles tails that legitimately terminate without a completion (cluster followers) and
4252    /// avoids relying on a channel event after the atomic reaches zero.
4253    async fn settle_checkpoint_tails(
4254        &mut self,
4255        callback: &mut impl PipelineCallback,
4256    ) -> Option<String> {
4257        let mut continuation_fault = None;
4258        let deadline = Instant::now() + SHUTDOWN_CHECKPOINT_TAIL_TIMEOUT;
4259        let mut tails_aborted = false;
4260        loop {
4261            self.drain_manual_requests();
4262            self.fail_waiting_manual("pipeline is stopping; no new checkpoint can be admitted");
4263
4264            while let Some(completion) = self
4265                .checkpoint_complete_rx
4266                .as_ref()
4267                .and_then(|rx| rx.try_recv().ok())
4268            {
4269                if let Some(error) = self.handle_checkpoint_completion(completion, callback) {
4270                    continuation_fault.get_or_insert(error);
4271                }
4272            }
4273
4274            if self.checkpoint_in_flight.load(Ordering::Acquire) == 0 {
4275                break;
4276            }
4277
4278            if Instant::now() >= deadline {
4279                let pending = self.checkpoint_in_flight.load(Ordering::Acquire);
4280                let reason = format!(
4281                    "checkpoint durable-tail shutdown drain timed out after \
4282                     {SHUTDOWN_CHECKPOINT_TAIL_TIMEOUT:?} with {pending} attempt(s) still in \
4283                     flight; cancelling tails for recovery"
4284                );
4285                continuation_fault.get_or_insert(reason);
4286                if let Err(error) = callback.settle_checkpoint_tail_tasks(true).await {
4287                    continuation_fault.get_or_insert(error);
4288                }
4289                tails_aborted = true;
4290                break;
4291            }
4292
4293            let completion = if let Some(rx) = self.checkpoint_complete_rx.as_mut() {
4294                let tick = SHUTDOWN_COMPLETION_TICK
4295                    .min(deadline.saturating_duration_since(Instant::now()));
4296                match tokio::time::timeout(tick, rx.recv()).await {
4297                    Ok(Ok(completion)) => Some(completion),
4298                    Ok(Err(_)) => {
4299                        tokio::time::sleep(SHUTDOWN_COMPLETION_TICK).await;
4300                        None
4301                    }
4302                    Err(_) => None,
4303                }
4304            } else {
4305                tokio::time::sleep(SHUTDOWN_COMPLETION_TICK).await;
4306                None
4307            };
4308            if let Some(completion) = completion {
4309                if let Some(error) = self.handle_checkpoint_completion(completion, callback) {
4310                    continuation_fault.get_or_insert(error);
4311                }
4312            }
4313        }
4314
4315        if !tails_aborted {
4316            if let Err(error) = callback.settle_checkpoint_tail_tasks(false).await {
4317                continuation_fault.get_or_insert(error);
4318            }
4319        }
4320
4321        // A sender enqueues its completion before dropping the in-flight guard. Once the counter
4322        // reaches zero, drain the enqueue that may have raced with our last atomic load.
4323        while let Some(completion) = self
4324            .checkpoint_complete_rx
4325            .as_ref()
4326            .and_then(|rx| rx.try_recv().ok())
4327        {
4328            if let Some(error) = self.handle_checkpoint_completion(completion, callback) {
4329                continuation_fault.get_or_insert(error);
4330            }
4331        }
4332        self.drain_manual_requests();
4333        self.fail_all_manual("pipeline stopped before the checkpoint reached a terminal result");
4334        continuation_fault
4335    }
4336
4337    fn handle_checkpoint_completion(
4338        &mut self,
4339        completion: CheckpointCompletion,
4340        callback: &mut impl PipelineCallback,
4341    ) -> Option<String> {
4342        let attempt = completion.attempt();
4343        match completion {
4344            CheckpointCompletion::Committed {
4345                result,
4346                source_checkpoints,
4347                ..
4348            } => {
4349                let completed = CheckpointAttempt::new(result.epoch, result.checkpoint_id);
4350                if !result.success || completed != attempt {
4351                    let reason = format!(
4352                        "checkpoint terminal identity mismatch: admitted epoch={} id={}, \
4353                         completed epoch={} id={} success={}",
4354                        attempt.epoch,
4355                        attempt.checkpoint_id,
4356                        completed.epoch,
4357                        completed.checkpoint_id,
4358                        result.success,
4359                    );
4360                    callback.abort_subscription_cut(attempt);
4361                    self.fail_manual_attempt(attempt, &reason);
4362                    callback.record_checkpoint_failure(attempt.checkpoint_id, &reason);
4363                } else {
4364                    if self.last_published_checkpoint.is_some_and(|last| {
4365                        last.relation_to(attempt) != CheckpointAttemptRelation::Older
4366                    }) {
4367                        let last = self.last_published_checkpoint.unwrap();
4368                        let reason = format!(
4369                            "checkpoint completion is not strictly newer: last published epoch={} id={}, \
4370                             received epoch={} id={}",
4371                            last.epoch,
4372                            last.checkpoint_id,
4373                            attempt.epoch,
4374                            attempt.checkpoint_id,
4375                        );
4376                        callback.abort_subscription_cut(attempt);
4377                        self.fail_manual_attempt(attempt, &reason);
4378                        callback.record_checkpoint_failure(attempt.checkpoint_id, &reason);
4379                        return Some(reason);
4380                    }
4381                    #[cfg(feature = "cluster")]
4382                    if let Err(error) =
4383                        self.require_process_authority("durable checkpoint publication")
4384                    {
4385                        let reason = error.to_string();
4386                        callback.abort_subscription_cut(attempt);
4387                        self.fail_manual_attempt(attempt, &reason);
4388                        callback.record_checkpoint_continuation_fault(attempt, &reason);
4389                        return Some(reason);
4390                    }
4391                    let continuation_error = result.continuation_error().map(str::to_owned);
4392                    // Ordering is semantic: N reached its durable point, so source and public
4393                    // acknowledgements for N must be published even when N+1 cannot be opened.
4394                    self.last_published_checkpoint = Some(attempt);
4395                    let publication_error = callback.publish_barrier(attempt).err();
4396                    self.broadcast_epoch_committed(attempt.epoch, &source_checkpoints);
4397                    self.finish_manual_success(attempt, &result);
4398                    self.advance_checkpoint_cadence();
4399                    let continuation_error = publication_error.or(continuation_error);
4400                    if let Some(reason) = continuation_error.as_deref() {
4401                        callback.record_checkpoint_continuation_fault(attempt, reason);
4402                    }
4403                    return continuation_error;
4404                }
4405            }
4406            CheckpointCompletion::Failed { error, .. } => {
4407                callback.abort_subscription_cut(attempt);
4408                self.fail_manual_attempt(attempt, &error);
4409                callback.record_checkpoint_failure(attempt.checkpoint_id, &error);
4410                self.advance_checkpoint_cadence();
4411            }
4412        }
4413        None
4414    }
4415
4416    /// Run the coordinator loop until shutdown or a fatal cycle fault.
4417    ///
4418    /// Cycle priority: (1) shutdown, (2) drain + SQL, (3) barrier alignment,
4419    /// (4) checkpointing, (5) control, (6) barrier timeout.
4420    pub async fn run<C: PipelineCallback>(self, callback: C) -> ExitReason {
4421        self.run_inner(callback, None).await
4422    }
4423
4424    /// Run the coordinator and report when its control loop is ready.
4425    ///
4426    /// Pipeline startup and coordinated recovery use this stronger boundary: constructing the
4427    /// compute runtime is not enough, because recovery must not acknowledge a node before barrier
4428    /// injection and manual-checkpoint control are installed on the live loop.
4429    pub(crate) async fn run_with_ready<C: PipelineCallback>(
4430        self,
4431        callback: C,
4432        ready: crossfire::oneshot::TxOneshot<Result<(), String>>,
4433    ) -> ExitReason {
4434        self.run_inner(callback, Some(ready)).await
4435    }
4436
4437    async fn wait_for_cycle<C: PipelineCallback>(
4438        &mut self,
4439        callback: &mut C,
4440        state: &mut CoordinatorRunState,
4441    ) -> CoordinatorWait {
4442        if let Err(error) = callback.prepare_source_intake() {
4443            state.fault = Some(format!(
4444                "source recovery handoff could not be installed before intake: {error}"
4445            ));
4446            return CoordinatorWait::stop();
4447        }
4448        let intake_paused = callback.intake_paused();
4449        if intake_paused || self.replay_pending {
4450            let _ = callback.is_recovering();
4451        }
4452        let external_commit_paused = self
4453            .coordinated_commit_admission
4454            .as_ref()
4455            .is_some_and(|admission| !admission.can_admit());
4456        let replay_ready = self.replay_pending && !external_commit_paused && !intake_paused;
4457        let parked_ready = !self.replay_pending
4458            && !external_commit_paused
4459            && !intake_paused
4460            && self.parked_source_msg.is_some();
4461        let mut retrying_replay = false;
4462        let mut checkpoint_control_due = false;
4463
4464        let message = tokio::select! {
4465            biased;
4466            () = self.terminal_shutdown.cancelled() => return CoordinatorWait::stop(),
4467            () = self.shutdown.notified() => return CoordinatorWait::stop(),
4468            Some(source_fault) = self.source_fault_rx.recv() => {
4469                state.fault = Some(format!(
4470                    "source '{}' fault: {}",
4471                    source_fault.source, source_fault.error
4472                ));
4473                return CoordinatorWait::stop();
4474            }
4475            Some(completion) = async {
4476                if let Some(ref mut rx) = self.checkpoint_complete_rx {
4477                    rx.recv().await.ok()
4478                } else {
4479                    futures::future::pending::<Option<CheckpointCompletion>>().await
4480                }
4481            } => {
4482                if let Some(error) = self.handle_checkpoint_completion(completion, callback) {
4483                    state.fault = Some(error);
4484                    return CoordinatorWait::stop();
4485                }
4486                if !state.checkpoint_control_pending {
4487                    return CoordinatorWait::continue_loop();
4488                }
4489                checkpoint_control_due = true;
4490                if state.checkpoint_control_wake.is_some() {
4491                    state.checkpoint_control_poll_at =
4492                        tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
4493                }
4494                None
4495            }
4496            Some(reply) = async {
4497                if let Some(ref mut rx) = self.force_ckpt_rx {
4498                    rx.recv().await.ok()
4499                } else {
4500                    futures::future::pending::<Option<ForceCheckpointReply>>().await
4501                }
4502            } => {
4503                self.manual_waiting.push(reply);
4504                None
4505            },
4506            () = async {
4507                match state.checkpoint_control_wake.as_mut() {
4508                    Some(wake) => wake.wait_until(state.checkpoint_control_poll_at).await,
4509                    None => std::future::pending().await,
4510                }
4511            }, if !state.checkpoint_control_pending && !callback.is_leader() => {
4512                state.checkpoint_control_pending = true;
4513                checkpoint_control_due = true;
4514                if state.checkpoint_control_wake.is_some() {
4515                    state.checkpoint_control_poll_at =
4516                        tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
4517                }
4518                None
4519            },
4520            () = tokio::time::sleep_until(state.checkpoint_control_poll_at),
4521                if state.checkpoint_control_pending && !callback.is_leader() =>
4522            {
4523                checkpoint_control_due = true;
4524                if state.checkpoint_control_wake.is_some() {
4525                    state.checkpoint_control_poll_at =
4526                        tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
4527                }
4528                None
4529            },
4530            () = std::future::ready(()), if replay_ready => {
4531                retrying_replay = true;
4532                None
4533            },
4534            () = async {
4535                if let Some(notify) = state.coordinated_commit_progress.as_ref() {
4536                    notify.notified().await;
4537                } else {
4538                    futures::future::pending::<()>().await;
4539                }
4540            } => None,
4541            () = std::future::ready(()), if parked_ready => self.parked_source_msg.take(),
4542            msg = self.rx.recv(),
4543                if state.source_channel_expected && !external_commit_paused && !intake_paused =>
4544            {
4545                if let Ok(message) = msg {
4546                    if !state.batch_window.is_zero() {
4547                        let authority_lost = wait_coordinator_delay(
4548                            state.batch_window,
4549                            #[cfg(feature = "cluster")]
4550                            self.process_authority.as_deref(),
4551                        )
4552                        .await;
4553                        if authority_lost {
4554                            state.fault = Some(
4555                                "cluster process lease expired during source batch window".into(),
4556                            );
4557                            return CoordinatorWait::stop();
4558                        }
4559                    }
4560                    Some(message)
4561                } else {
4562                    state.fault = Some("all configured source tasks exited unexpectedly".into());
4563                    return CoordinatorWait::stop();
4564                }
4565            }
4566            authority_lost = wait_coordinator_delay(
4567                IDLE_TIMEOUT,
4568                #[cfg(feature = "cluster")]
4569                self.process_authority.as_deref(),
4570            ) => {
4571                if authority_lost {
4572                    state.fault =
4573                        Some("cluster process lease expired while coordinator was idle".into());
4574                    return CoordinatorWait::stop();
4575                }
4576                None
4577            },
4578        };
4579
4580        CoordinatorWait::cycle(CoordinatorWake {
4581            message,
4582            retrying_replay,
4583            checkpoint_control_due,
4584            gates: CoordinatorGates {
4585                intake_paused,
4586                external_commit_paused,
4587            },
4588        })
4589    }
4590
4591    async fn service_background_work<C: PipelineCallback>(
4592        &mut self,
4593        callback: &mut C,
4594        state: &mut CoordinatorRunState,
4595        checkpoint_control_due: bool,
4596    ) -> bool {
4597        let started = Instant::now();
4598        if self.replay_pending && self.pending_barrier.active {
4599            if let Err(error) = self
4600                .cancel_pending_barrier_for_stop(
4601                    callback,
4602                    "operator input remained deferred before source barrier alignment",
4603                    true,
4604                )
4605                .await
4606            {
4607                state.fault = Some(error);
4608                return false;
4609            }
4610        }
4611
4612        for (source_idx, barrier, checkpoint) in &state.barriers {
4613            match self
4614                .handle_barrier(*source_idx, barrier, checkpoint, callback)
4615                .await
4616            {
4617                Ok(()) => {}
4618                Err(CycleError::Halt(reason)) => {
4619                    tracing::warn!(%reason, "[LDB-3022] checkpoint drain halted the pipeline");
4620                    state.halted = true;
4621                    return false;
4622                }
4623                Err(CycleError::Fatal(reason) | CycleError::Recovery(reason)) => {
4624                    state.fault = Some(reason);
4625                    return false;
4626                }
4627            }
4628        }
4629        if self.terminal_shutdown.is_cancelled() {
4630            return false;
4631        }
4632        if let Some(reason) = callback.take_pipeline_fault() {
4633            self.discard_pending_offsets();
4634            tracing::error!(
4635                reason = %reason,
4636                "[LDB-3024] pipeline consistency fault; stopping for recovery"
4637            );
4638            state.fault = Some(reason);
4639            return false;
4640        }
4641
4642        let within_budget =
4643            started.elapsed() < Duration::from_nanos(self.config.background_budget_ns);
4644        let follower_control_ready =
4645            state.checkpoint_control_pending && checkpoint_control_due && !callback.is_leader();
4646        let checkpoint_work_due =
4647            callback.is_leader() || follower_control_ready || !self.manual_waiting.is_empty();
4648        if !self.replay_pending && checkpoint_work_due && (follower_control_ready || within_budget)
4649        {
4650            let control_serviced = self.maybe_checkpoint(callback).await;
4651            if follower_control_ready {
4652                #[cfg(feature = "cluster")]
4653                if let Some(wake) = state.checkpoint_control_wake.as_ref() {
4654                    if control_serviced {
4655                        state.checkpoint_control_pending = false;
4656                        state.checkpoint_control_poll_at =
4657                            tokio::time::Instant::now() + wake.fallback();
4658                    } else {
4659                        state.checkpoint_control_poll_at =
4660                            tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
4661                    }
4662                }
4663                #[cfg(not(feature = "cluster"))]
4664                if state.checkpoint_control_wake.is_some() {
4665                    state.checkpoint_control_poll_at = tokio::time::Instant::now()
4666                        + if control_serviced {
4667                            CheckpointControlWake::fallback()
4668                        } else {
4669                            CheckpointControlWake::capacity_retry()
4670                        };
4671                    state.checkpoint_control_pending &= !control_serviced;
4672                }
4673            }
4674            if let Some(reason) = callback.take_pipeline_fault() {
4675                self.discard_pending_offsets();
4676                tracing::error!(
4677                    reason = %reason,
4678                    "[LDB-3024] checkpoint control fault; stopping for recovery"
4679                );
4680                state.fault = Some(reason);
4681                return false;
4682            }
4683        }
4684
4685        while let Ok(message) = self.control_rx.try_recv() {
4686            #[cfg(feature = "cluster")]
4687            if let Err(error) = self.require_process_authority("pipeline control mutation") {
4688                state.fault = Some(error.to_string());
4689                return false;
4690            }
4691            callback.apply_control(message);
4692        }
4693
4694        if self.pending_barrier.active
4695            && self.pending_barrier.started_at.elapsed() > self.config.checkpoint_timeout
4696        {
4697            if let Err(error) = self
4698                .cancel_pending_barrier_for_stop(callback, "source barrier alignment timeout", true)
4699                .await
4700            {
4701                state.fault = Some(error);
4702                return false;
4703            }
4704        }
4705        true
4706    }
4707
4708    async fn run_inner<C: PipelineCallback>(
4709        mut self,
4710        mut callback: C,
4711        ready: Option<crossfire::oneshot::TxOneshot<Result<(), String>>>,
4712    ) -> ExitReason {
4713        /// Maximum messages to drain per cycle before yielding for background work.
4714        const MAX_DRAIN_PER_CYCLE: usize = 10_000;
4715
4716        let injectors = self
4717            .source_handles
4718            .iter()
4719            .map(SourceHandle::barrier_control)
4720            .collect();
4721        callback.set_barrier_injectors(injectors);
4722        let cancelled_before_ready = self.terminal_shutdown.is_cancelled();
4723        if let Some(ready) = ready {
4724            if cancelled_before_ready {
4725                ready.send(Err(
4726                    "pipeline runtime generation was cancelled before readiness".into(),
4727                ));
4728            } else {
4729                ready.send(Ok(()));
4730            }
4731        }
4732        if !cancelled_before_ready && !self.terminal_shutdown.is_cancelled() {
4733            // Readiness is the linearization point: source tasks are released only after it is
4734            // published, so none can enqueue a batch, barrier, or fault on the pre-ready side.
4735            for handle in &mut self.source_handles {
4736                if let Some(activation) = handle.startup_activation.take() {
4737                    activation.send(());
4738                }
4739            }
4740        }
4741
4742        let mut state = CoordinatorRunState {
4743            batch_window: self.config.batch_window,
4744            coordinated_commit_progress: self
4745                .coordinated_commit_admission
4746                .as_ref()
4747                .map(crate::checkpoint_coordinator::CoordinatedCommitAdmission::progress_notify),
4748            checkpoint_control_wake: callback.checkpoint_control_wake(),
4749            checkpoint_control_poll_at: tokio::time::Instant::now(),
4750            checkpoint_control_pending: false,
4751            barriers: Vec::new(),
4752            fault: None,
4753            halted: false,
4754            source_channel_expected: !self.source_names.is_empty(),
4755        };
4756
4757        loop {
4758            if self.terminal_shutdown.is_cancelled() {
4759                break;
4760            }
4761            let wait = self.wait_for_cycle(&mut callback, &mut state).await;
4762            match wait.action {
4763                CoordinatorWaitAction::Cycle => {}
4764                CoordinatorWaitAction::Continue => continue,
4765                CoordinatorWaitAction::Stop => break,
4766            }
4767            let CoordinatorWake {
4768                message: msg,
4769                retrying_replay,
4770                checkpoint_control_due,
4771                gates:
4772                    CoordinatorGates {
4773                        intake_paused,
4774                        external_commit_paused,
4775                    },
4776            } = wait.wake;
4777            // A progress wake is edge-triggered; recompute the gate on the next loop before
4778            // touching deferred/open-epoch data. Completion branches above already `continue`.
4779            if external_commit_paused && msg.is_none() {
4780                continue;
4781            }
4782            // Recheck after the await: recovery may have closed the gate after this loop removed
4783            // a message from the source FIFO. Keep that message ahead of later FIFO entries so a
4784            // transient close/reopen cannot silently lose it. A fenced shutdown still discards
4785            // all open-epoch data below, where recovery owns the rewind.
4786            if intake_paused || callback.intake_paused() {
4787                if let Some(message) = msg {
4788                    if self.parked_source_msg.is_some() {
4789                        state.fault = Some(
4790                            "source intake gate race exceeded its single parked-message slot"
4791                                .into(),
4792                        );
4793                        break;
4794                    }
4795                    self.parked_source_msg = Some(message);
4796                }
4797                continue;
4798            }
4799
4800            self.source_batches_buf.clear();
4801            self.reset_barrier_seen_for_cycle();
4802            if !retrying_replay && !self.replay_pending {
4803                self.discard_pending_offsets();
4804            }
4805            state.barriers.clear();
4806            let mut cycle_events: u64 = 0;
4807            let cycle_start = Instant::now();
4808
4809            let had_data = msg.is_some();
4810            if let Some(first_msg) = msg {
4811                if let Err(error) = self.process_msg(
4812                    first_msg,
4813                    &mut callback,
4814                    &mut state.barriers,
4815                    &mut cycle_events,
4816                ) {
4817                    state.fault = Some(error);
4818                }
4819            }
4820            if state.fault.is_some() {
4821                self.discard_pending_offsets();
4822                break;
4823            }
4824
4825            // Coalesce additional buffered messages; stop at count, time budget, or backpressure.
4826            let mut drain_count = 0;
4827            let drain_budget = Duration::from_nanos(self.config.drain_budget_ns);
4828            // `is_backpressured()` bumps a counter, so call it only on active wakeups rather than
4829            // idle timeouts.
4830            let backpressured = had_data && callback.is_backpressured();
4831            if backpressured {
4832                tracing::debug!("operator graph backpressured — skipping drain");
4833            }
4834            while !backpressured
4835                && drain_count < MAX_DRAIN_PER_CYCLE
4836                && cycle_start.elapsed() < drain_budget
4837            {
4838                match self.rx.try_recv() {
4839                    Ok(msg) => {
4840                        if let Err(error) = self.process_msg(
4841                            msg,
4842                            &mut callback,
4843                            &mut state.barriers,
4844                            &mut cycle_events,
4845                        ) {
4846                            state.fault = Some(error);
4847                            break;
4848                        }
4849                        drain_count += 1;
4850                    }
4851                    Err(_) => break,
4852                }
4853            }
4854            if let Ok(source_fault) = self.source_fault_rx.try_recv() {
4855                state.fault = Some(format!(
4856                    "source '{}' fault: {}",
4857                    source_fault.source, source_fault.error
4858                ));
4859            }
4860            if state.fault.is_some() {
4861                self.discard_pending_offsets();
4862                break;
4863            }
4864            #[cfg(feature = "cluster")]
4865            if let Err(error) = self.require_process_authority("folding a drained source cycle") {
4866                self.discard_pending_offsets();
4867                state.fault = Some(error.to_string());
4868                break;
4869            }
4870
4871            for (name, batch) in self.pending_watermark_batches.drain(..) {
4872                callback.extract_watermark(&name, &batch);
4873            }
4874
4875            if !self.replay_pending {
4876                callback.tick_idle_watermark();
4877            }
4878
4879            // Run on idle wakeups too when operators have deferred input; otherwise
4880            // deferred data stalls once the source goes quiet.
4881            if !self.source_batches_buf.is_empty()
4882                || self.replay_pending
4883                || callback.has_deferred_input()
4884            {
4885                let wm = callback.current_watermark();
4886                #[cfg(feature = "cluster")]
4887                if let Err(error) = self.require_process_authority("operator execution") {
4888                    self.discard_pending_offsets();
4889                    state.fault = Some(error.to_string());
4890                    break;
4891                }
4892                match callback.execute_cycle(&self.source_batches_buf, wm).await {
4893                    Ok(out) => {
4894                        // Exactly-once / coordinated recovery rewinds the whole pipeline, so
4895                        // don't partial-commit siblings — recover instead.
4896                        if out.any_failed && callback.fault_on_cycle_error() {
4897                            self.discard_pending_offsets();
4898                            tracing::error!(
4899                                "[LDB-3021] failure domain faulted; faulting for recovery"
4900                            );
4901                            state.fault = Some("isolated domain fault (exactly-once)".to_string());
4902                            break;
4903                        }
4904                        if let Err(error) = self.publish_cycle_outputs(&mut callback, &out).await {
4905                            let reason = error.to_string();
4906                            tracing::error!(
4907                                error = %reason,
4908                                "cycle output publication failed; faulting for recovery"
4909                            );
4910                            state.fault = Some(reason);
4911                            break;
4912                        }
4913                        if out.any_failed {
4914                            callback.note_cycle_error();
4915                            tracing::warn!(
4916                                "[LDB-3020] failure domain dropped (best-effort: continuing)"
4917                            );
4918                        }
4919                    }
4920                    Err(e) => {
4921                        self.discard_pending_offsets();
4922                        match e {
4923                            CycleError::Recovery(msg) => {
4924                                tracing::error!(
4925                                    error = %msg,
4926                                    "shared pipeline infrastructure failed; faulting for recovery"
4927                                );
4928                                state.fault = Some(msg);
4929                                break;
4930                            }
4931                            // Shutdown already signaled; restarting would just re-trip it.
4932                            CycleError::Halt(msg) => {
4933                                tracing::warn!(reason = %msg, "[LDB-3022] cycle halted");
4934                                break;
4935                            }
4936                            // Continuing would drop the drained rows (EO gap), so fault for
4937                            // recovery under exactly-once or coordinated recovery.
4938                            CycleError::Fatal(msg) if callback.fault_on_cycle_error() => {
4939                                tracing::error!(
4940                                    error = %msg,
4941                                    "[LDB-3021] fatal SQL cycle error; faulting for recovery"
4942                                );
4943                                state.fault = Some(msg);
4944                                break;
4945                            }
4946                            // Best effort: drop the bad cycle and continue.
4947                            CycleError::Fatal(msg) => {
4948                                callback.note_cycle_error();
4949                                tracing::warn!(
4950                                    error = %msg,
4951                                    "[LDB-3020] SQL cycle error (best-effort: continuing)"
4952                                );
4953                            }
4954                        }
4955                    }
4956                }
4957                let elapsed_ns =
4958                    u64::try_from(cycle_start.elapsed().as_nanos()).unwrap_or(u64::MAX);
4959                callback.record_cycle(cycle_events, 0, elapsed_ns);
4960
4961                if elapsed_ns >= self.config.cycle_budget_ns {
4962                    tracing::debug!(
4963                        elapsed_ms = elapsed_ns / 1_000_000,
4964                        budget_ms = self.config.cycle_budget_ns / 1_000_000,
4965                        "cycle budget exceeded — proceeding to background work"
4966                    );
4967                }
4968            }
4969
4970            if !self
4971                .service_background_work(&mut callback, &mut state, checkpoint_control_due)
4972                .await
4973            {
4974                break;
4975            }
4976        }
4977
4978        self.finish_run(&mut callback, state.fault).await
4979    }
4980
4981    async fn finish_run<C: PipelineCallback>(
4982        &mut self,
4983        callback: &mut C,
4984        mut fault: Option<String>,
4985    ) -> ExitReason {
4986        // Every exit below is coordinator-owned. Mark it before tail settlement so source-task
4987        // guards cannot turn an intentional teardown into a second runtime fault.
4988        for handle in &self.source_handles {
4989            handle.task.mark_expected_shutdown();
4990        }
4991
4992        // Stop is an admission fence. Cancel alignment before waiting for captured tails: an
4993        // unaligned attempt has no tail and therefore cannot make the in-flight counter progress.
4994        self.drain_manual_requests();
4995        self.fail_waiting_manual("pipeline is stopping; no new checkpoint can be admitted");
4996        let interrupted_reason = if fault.is_some() {
4997            "pipeline fault interrupted source barrier alignment"
4998        } else {
4999            "pipeline shutdown interrupted source barrier alignment"
5000        };
5001        if let Err(error) = self
5002            .cancel_pending_barrier_for_stop(callback, interrupted_reason, false)
5003            .await
5004        {
5005            fault.get_or_insert(error);
5006        }
5007
5008        // Captured tails own durable state and may still need to publish source acknowledgements.
5009        // Settling them while sources and sinks remain open prevents close from racing commit.
5010        if let Some(error) = self.settle_checkpoint_tails(callback).await {
5011            fault.get_or_insert(error);
5012        }
5013        if let Some(reason) = callback.take_pipeline_fault() {
5014            fault.get_or_insert(reason);
5015        }
5016
5017        self.stop_source_barrier_holds();
5018        for handle in &self.source_handles {
5019            handle.task.notify_shutdown();
5020        }
5021
5022        let intake_fenced = callback.intake_paused();
5023        self.source_batches_buf.clear();
5024        self.pending_watermark_batches.clear();
5025        self.barrier_seen.clear();
5026        if intake_fenced || !self.replay_pending {
5027            self.discard_pending_offsets();
5028        }
5029        let mut drain_events = 0_u64;
5030
5031        // A message parked by the intake-gate race is open-epoch data.
5032        if let Some(msg) = self.parked_source_msg.take() {
5033            if fault.is_none() && !intake_fenced {
5034                if let Some(reason) = self.process_shutdown_msg(msg, callback, &mut drain_events) {
5035                    fault = Some(reason);
5036                    self.source_batches_buf.clear();
5037                    self.pending_watermark_batches.clear();
5038                    self.discard_pending_offsets();
5039                }
5040            }
5041        }
5042
5043        // Closing the watch senders releases source tasks after they consume any exact commit
5044        // broadcast settled above. Keep draining their data channel while they finish so a task
5045        // blocked on a full channel cannot deadlock shutdown.
5046        let mut stopping_sources = Vec::with_capacity(self.source_handles.len());
5047        for handle in std::mem::take(&mut self.source_handles) {
5048            let SourceHandle {
5049                task,
5050                epoch_committed_tx,
5051                ..
5052            } = handle;
5053            drop(epoch_committed_tx);
5054            stopping_sources.push(task);
5055        }
5056
5057        let source_deadline = tokio::time::Instant::now() + SHUTDOWN_JOIN_TIMEOUT;
5058        let mut source_channel_closed = false;
5059        while stopping_sources.iter().any(|task| !task.is_finished())
5060            && tokio::time::Instant::now() < source_deadline
5061        {
5062            while let Ok(msg) = self.rx.try_recv() {
5063                if fault.is_none() && !intake_fenced {
5064                    if let Some(reason) =
5065                        self.process_shutdown_msg(msg, callback, &mut drain_events)
5066                    {
5067                        fault = Some(reason);
5068                        self.source_batches_buf.clear();
5069                        self.pending_watermark_batches.clear();
5070                        self.discard_pending_offsets();
5071                    }
5072                }
5073            }
5074
5075            if stopping_sources.iter().all(SourceTaskLease::is_finished) {
5076                break;
5077            }
5078
5079            let tick = SHUTDOWN_COMPLETION_TICK
5080                .min(source_deadline.saturating_duration_since(tokio::time::Instant::now()));
5081            if source_channel_closed {
5082                // A disconnected receive is immediately ready. Keep yielding so the stable task
5083                // actor and tracker proofs can publish terminal completion on a current-thread
5084                // runtime.
5085                tokio::time::sleep(tick).await;
5086                continue;
5087            }
5088            match tokio::time::timeout(tick, self.rx.recv()).await {
5089                Ok(Ok(msg)) if fault.is_none() && !intake_fenced => {
5090                    if let Some(reason) =
5091                        self.process_shutdown_msg(msg, callback, &mut drain_events)
5092                    {
5093                        fault = Some(reason);
5094                        self.source_batches_buf.clear();
5095                        self.pending_watermark_batches.clear();
5096                        self.discard_pending_offsets();
5097                    }
5098                }
5099                Ok(Err(_)) => source_channel_closed = true,
5100                Ok(Ok(_)) | Err(_) => {}
5101            }
5102        }
5103
5104        for source in stopping_sources {
5105            Self::reap_source_task(source);
5106        }
5107
5108        // Capture messages enqueued immediately before the last source task exited.
5109        while let Ok(msg) = self.rx.try_recv() {
5110            if fault.is_none() && !intake_fenced {
5111                if let Some(reason) = self.process_shutdown_msg(msg, callback, &mut drain_events) {
5112                    fault = Some(reason);
5113                    self.source_batches_buf.clear();
5114                    self.pending_watermark_batches.clear();
5115                    self.discard_pending_offsets();
5116                }
5117            }
5118        }
5119
5120        #[cfg(feature = "cluster")]
5121        if fault.is_none() && !intake_fenced {
5122            if let Err(error) = self.require_process_authority("folding the shutdown source drain")
5123            {
5124                self.source_batches_buf.clear();
5125                self.pending_watermark_batches.clear();
5126                self.discard_pending_offsets();
5127                fault = Some(error.to_string());
5128            }
5129        }
5130
5131        if fault.is_none() && !intake_fenced {
5132            for (name, batch) in self.pending_watermark_batches.drain(..) {
5133                callback.extract_watermark(&name, &batch);
5134            }
5135            callback.tick_idle_watermark();
5136            if !self.source_batches_buf.is_empty()
5137                || self.replay_pending
5138                || callback.has_deferred_input()
5139            {
5140                let cycle_start = Instant::now();
5141                let wm = callback.current_watermark();
5142                #[cfg(feature = "cluster")]
5143                if let Err(error) = self.require_process_authority(
5144                    "operator execution during the shutdown source drain",
5145                ) {
5146                    self.discard_pending_offsets();
5147                    fault = Some(error.to_string());
5148                }
5149                if fault.is_none() {
5150                    match callback.execute_cycle(&self.source_batches_buf, wm).await {
5151                        Ok(out) if out.any_failed && callback.fault_on_cycle_error() => {
5152                            self.discard_pending_offsets();
5153                            fault = Some(
5154                        "isolated domain fault during shutdown drain under replay guarantee"
5155                            .to_string(),
5156                    );
5157                        }
5158                        Ok(out) => match self.publish_cycle_outputs(callback, &out).await {
5159                            Ok(()) => {
5160                                if out.any_failed {
5161                                    callback.note_cycle_error();
5162                                }
5163                            }
5164                            Err(error) => {
5165                                fault = Some(format!(
5166                            "cycle output publication failed during shutdown drain: {error}"
5167                        ));
5168                            }
5169                        },
5170                        Err(CycleError::Halt(reason)) => {
5171                            self.discard_pending_offsets();
5172                            tracing::warn!(%reason, "[LDB-3022] cycle halted during shutdown drain");
5173                        }
5174                        Err(CycleError::Recovery(reason)) => {
5175                            self.discard_pending_offsets();
5176                            fault = Some(format!(
5177                        "shared pipeline infrastructure failed during shutdown drain: {reason}"
5178                    ));
5179                        }
5180                        Err(CycleError::Fatal(reason)) if callback.fault_on_cycle_error() => {
5181                            self.discard_pending_offsets();
5182                            fault = Some(format!(
5183                                "fatal SQL cycle error during shutdown drain: {reason}"
5184                            ));
5185                        }
5186                        Err(CycleError::Fatal(reason)) => {
5187                            self.discard_pending_offsets();
5188                            callback.note_cycle_error();
5189                            tracing::warn!(%reason, "[LDB-3020] SQL cycle error during shutdown drain");
5190                        }
5191                    }
5192                }
5193                let elapsed_ns =
5194                    u64::try_from(cycle_start.elapsed().as_nanos()).unwrap_or(u64::MAX);
5195                callback.record_cycle(drain_events, 0, elapsed_ns);
5196            }
5197        }
5198
5199        // Captured tails are settled and no more checkpoints can be admitted. Keep sink actors
5200        // open while the designated committer publishes every already-sealed exact cut. Open-
5201        // epoch rows above are intentionally excluded and replay after restart.
5202        if fault.is_none() {
5203            if let Err(error) = self.drain_coordinated_commits().await {
5204                if callback.fault_on_cycle_error() {
5205                    fault = Some(error);
5206                } else {
5207                    callback.note_cycle_error();
5208                    tracing::warn!(%error, "coordinated commit drain failed during shutdown");
5209                }
5210            }
5211        }
5212
5213        // Resolve the durable open-epoch witness before close terminates the actor that owns its
5214        // rollback. On failure, keep actors live: lifecycle teardown retains their stable handles
5215        // and retries settlement before issuing close.
5216        let sink_epoch_settled = match callback.settle_sink_epoch_for_shutdown().await {
5217            Ok(()) => true,
5218            Err(error) => {
5219                match fault.as_mut() {
5220                    Some(existing) => {
5221                        existing.push_str("; sink epoch settlement also failed: ");
5222                        existing.push_str(&error);
5223                    }
5224                    None => fault = Some(format!("sink epoch settlement failed: {error}")),
5225                }
5226                false
5227            }
5228        };
5229
5230        // No final snapshot is synthesized: open-epoch rows deliberately replay from the last
5231        // committed cut. Sink close confirms queued writes and releases connector resources only
5232        // after durable epoch ownership is settled.
5233        if sink_epoch_settled {
5234            if let Err(close_error) = callback.close_sinks().await {
5235                if callback.fault_on_cycle_error() {
5236                    match fault.as_mut() {
5237                        Some(existing) => {
5238                            existing.push_str("; sink shutdown also failed: ");
5239                            existing.push_str(&close_error);
5240                        }
5241                        None => fault = Some(format!("sink shutdown failed: {close_error}")),
5242                    }
5243                } else {
5244                    callback.note_cycle_error();
5245                    tracing::warn!(
5246                        error = %close_error,
5247                        "sink shutdown failed under best-effort delivery"
5248                    );
5249                }
5250            }
5251        }
5252
5253        let exit = fault.map_or(ExitReason::Shutdown, ExitReason::Fault);
5254        let reason = match &exit {
5255        ExitReason::Shutdown => {
5256            "pipeline stopped; discard subscription rows after the last committed progress frontier"
5257        }
5258        ExitReason::Fault(error) => error,
5259    };
5260        callback.invalidate_subscriptions(reason);
5261        exit
5262    }
5263    async fn drain_coordinated_commits(&mut self) -> Result<(), String> {
5264        let Some(admission) = self.coordinated_commit_admission.as_ref() else {
5265            return Ok(());
5266        };
5267        let deadline = Instant::now() + COORDINATED_COMMIT_SHUTDOWN_TIMEOUT;
5268        loop {
5269            let (known, pending, _) = admission.state();
5270            if known && pending == 0 {
5271                return Ok(());
5272            }
5273            let remaining = deadline.saturating_duration_since(Instant::now());
5274            if remaining.is_zero() {
5275                return Err(format!(
5276                    "coordinated external commit drain timed out after \
5277                     {COORDINATED_COMMIT_SHUTDOWN_TIMEOUT:?} (lag_known={known}, \
5278                     pending={pending}); durable markers remain for recovery"
5279                ));
5280            }
5281            let progress = admission.progress_notify();
5282            let notified = progress.notified();
5283            tokio::pin!(notified);
5284            admission.wake_committer();
5285            if tokio::time::timeout(remaining, &mut notified)
5286                .await
5287                .is_err()
5288            {
5289                let (known, pending, _) = admission.state();
5290                return Err(format!(
5291                    "coordinated external commit drain timed out after \
5292                     {COORDINATED_COMMIT_SHUTDOWN_TIMEOUT:?} (lag_known={known}, \
5293                     pending={pending}); durable markers remain for recovery"
5294                ));
5295            }
5296        }
5297    }
5298
5299    fn stage_batch(
5300        &mut self,
5301        source_idx: usize,
5302        batch: RecordBatch,
5303        checkpoint: SourceCheckpoint,
5304        callback: &mut impl PipelineCallback,
5305        cycle_events: &mut u64,
5306    ) {
5307        if source_idx < self.pending_offsets.len() {
5308            self.pending_offsets[source_idx] = Some(checkpoint);
5309        }
5310
5311        if let Some(name) = self.source_names.get(source_idx) {
5312            {
5313                *cycle_events += batch.num_rows() as u64;
5314            }
5315            // Filter against the pre-drain watermark. Extraction is deferred until after all
5316            // batches are filtered so one batch cannot make the next batch appear late.
5317            if let Some(filtered) = callback.filter_late_rows(name, &batch) {
5318                self.source_batches_buf
5319                    .entry(Arc::clone(name))
5320                    .or_default()
5321                    .push(filtered);
5322            }
5323            self.pending_watermark_batches
5324                .push((Arc::clone(name), batch));
5325        }
5326    }
5327
5328    /// Process one source message under the exact source-barrier ordering invariant.
5329    fn process_msg(
5330        &mut self,
5331        msg: SourceMsg,
5332        callback: &mut impl PipelineCallback,
5333        barriers: &mut Vec<(usize, CheckpointBarrier, SourceCheckpoint)>,
5334        cycle_events: &mut u64,
5335    ) -> Result<(), String> {
5336        match msg {
5337            SourceMsg::Batch {
5338                source_idx,
5339                batch,
5340                checkpoint,
5341            } => {
5342                if self.barrier_seen.contains(&source_idx) {
5343                    return Err(format!(
5344                        "source {} emitted data after its checkpoint barrier without an exact release",
5345                        self.source_names
5346                            .get(source_idx)
5347                            .map_or("<unknown>", AsRef::as_ref)
5348                    ));
5349                }
5350                self.stage_batch(source_idx, batch, checkpoint, callback, cycle_events);
5351            }
5352            SourceMsg::Barrier {
5353                source_idx,
5354                barrier,
5355                checkpoint,
5356            } => {
5357                let attempt = CheckpointAttempt::new(barrier.epoch, barrier.checkpoint_id);
5358                if !self.pending_barrier.active || self.pending_barrier.attempt != Some(attempt) {
5359                    tracing::debug!(
5360                        source_idx,
5361                        checkpoint_id = barrier.checkpoint_id,
5362                        epoch = barrier.epoch,
5363                        "ignoring stale or cancelled source barrier"
5364                    );
5365                    self.release_source_barrier_for(source_idx, attempt);
5366                    return Ok(());
5367                }
5368                tracing::debug!(
5369                    source_idx,
5370                    checkpoint_id = barrier.checkpoint_id,
5371                    "coordinator received source barrier"
5372                );
5373                self.barrier_seen.insert(source_idx);
5374                barriers.push((source_idx, barrier, checkpoint));
5375            }
5376        }
5377        Ok(())
5378    }
5379
5380    /// Process a message after checkpoint admission has closed.
5381    ///
5382    /// No shutdown checkpoint exists, so every remaining batch belongs to an uncommitted open
5383    /// epoch. Barriers are control records for attempts that have already been cancelled and are
5384    /// ignored; they must never affect later open-epoch data.
5385    fn process_shutdown_msg(
5386        &mut self,
5387        msg: SourceMsg,
5388        callback: &mut impl PipelineCallback,
5389        cycle_events: &mut u64,
5390    ) -> Option<String> {
5391        match msg {
5392            SourceMsg::Batch {
5393                source_idx,
5394                batch,
5395                checkpoint,
5396            } => {
5397                self.stage_batch(source_idx, batch, checkpoint, callback, cycle_events);
5398                None
5399            }
5400            SourceMsg::Barrier {
5401                source_idx,
5402                barrier,
5403                ..
5404            } => {
5405                tracing::debug!(
5406                    source_idx,
5407                    checkpoint_id = barrier.checkpoint_id,
5408                    epoch = barrier.epoch,
5409                    "ignoring checkpoint barrier during shutdown drain"
5410                );
5411                None
5412            }
5413        }
5414    }
5415
5416    /// Per-source committed offsets keyed by source name, reflecting the last successful cycle.
5417    /// Follower control uses this stable cut rather than advancing without source positions.
5418    fn current_source_offsets(&self) -> FxHashMap<String, SourceCheckpoint> {
5419        self.committed_offsets
5420            .iter()
5421            .enumerate()
5422            .filter_map(|(idx, cp)| {
5423                if !self
5424                    .source_handles
5425                    .get(idx)
5426                    .is_none_or(|handle| handle.recovery_cursor)
5427                {
5428                    return None;
5429                }
5430                cp.as_ref().and_then(|c| {
5431                    self.source_names
5432                        .get(idx)
5433                        .map(|name| (name.to_string(), c.clone()))
5434                })
5435            })
5436            .collect()
5437    }
5438
5439    /// Merge staged offsets into `committed_offsets` after successful cycle publication.
5440    fn commit_pending_offsets(&mut self) {
5441        for (i, pending) in self.pending_offsets.iter_mut().enumerate() {
5442            if let Some(cp) = pending.take() {
5443                self.committed_offsets[i] = Some(cp);
5444            }
5445        }
5446    }
5447
5448    /// Publish materialized views, streams, and sink work before advancing source cursors.
5449    /// Publication failure is a shared-runtime consistency fault, so every mode recovers.
5450    async fn publish_cycle_outputs(
5451        &mut self,
5452        callback: &mut impl PipelineCallback,
5453        outcome: &CycleOutcome,
5454    ) -> Result<(), CycleError> {
5455        #[cfg(feature = "cluster")]
5456        if let Err(error) = self.require_process_authority("materialized-view publication") {
5457            self.discard_pending_offsets();
5458            return Err(error);
5459        }
5460        if let Err(error) = callback.update_mv_stores(&outcome.results) {
5461            self.discard_pending_offsets();
5462            return Err(CycleError::Recovery(format!(
5463                "materialized-view publication failed: {error}"
5464            )));
5465        }
5466        #[cfg(feature = "cluster")]
5467        if let Err(error) = self.require_process_authority("stream publication") {
5468            self.discard_pending_offsets();
5469            return Err(error);
5470        }
5471        if let Err(error) = callback.push_to_streams(&outcome.results) {
5472            self.discard_pending_offsets();
5473            return Err(CycleError::Recovery(format!(
5474                "stream publication failed: {error}"
5475            )));
5476        }
5477
5478        #[cfg(feature = "cluster")]
5479        if let Err(error) = self.require_process_authority("sink publication") {
5480            self.discard_pending_offsets();
5481            return Err(error);
5482        }
5483        if let Err(error) = callback.write_to_sinks(&outcome.results, None).await {
5484            self.discard_pending_offsets();
5485            return Err(error);
5486        }
5487
5488        // A sink command admitted under authority may still be queued when the lease is fenced.
5489        // Recheck before advancing the in-memory source cursor; the checkpoint path separately
5490        // FIFO-fences every sink before persisting it.
5491        #[cfg(feature = "cluster")]
5492        if let Err(error) = self.require_process_authority("source cursor advancement") {
5493            self.discard_pending_offsets();
5494            return Err(error);
5495        }
5496        self.settle_pending_offsets(&outcome.failed_sources, &outcome.deferred_sources);
5497        self.replay_pending = outcome.any_deferred;
5498        Ok(())
5499    }
5500
5501    /// Settle one graph cycle without allowing a cursor to overtake graph-retained input.
5502    /// Failure takes precedence if a source appears in both sets: best-effort isolation drops the
5503    /// failed cycle, whereas a pure deferral must retain the exact staged cursor for its retry.
5504    fn settle_pending_offsets(
5505        &mut self,
5506        failed: &FxHashSet<Arc<str>>,
5507        deferred: &FxHashSet<Arc<str>>,
5508    ) {
5509        if failed.is_empty() && deferred.is_empty() {
5510            self.commit_pending_offsets();
5511            return;
5512        }
5513        for (i, pending) in self.pending_offsets.iter_mut().enumerate() {
5514            let in_failed_domain = self
5515                .source_names
5516                .get(i)
5517                .is_some_and(|name| failed.contains(name));
5518            if in_failed_domain {
5519                *pending = None;
5520            } else if self
5521                .source_names
5522                .get(i)
5523                .is_some_and(|name| deferred.contains(name))
5524            {
5525                // Retain the cursor alongside the graph's buffered input.
5526            } else if let Some(cp) = pending.take() {
5527                self.committed_offsets[i] = Some(cp);
5528            }
5529        }
5530    }
5531
5532    /// Discard staged offsets when cycle execution or publication fails.
5533    fn discard_pending_offsets(&mut self) {
5534        for slot in &mut self.pending_offsets {
5535            *slot = None;
5536        }
5537    }
5538
5539    /// Reset per-cycle barrier tracking at cycle start. While a multi-source barrier is still
5540    /// aligning, retain sources already held at the cut so any protocol violation fails closed.
5541    fn reset_barrier_seen_for_cycle(&mut self) {
5542        self.barrier_seen.clear();
5543        if self.pending_barrier.active {
5544            self.barrier_seen
5545                .extend(self.pending_barrier.sources_aligned.iter().copied());
5546        }
5547    }
5548
5549    fn capture_replayable_barrier_cursor(
5550        &mut self,
5551        source_idx: usize,
5552        checkpoint: &SourceCheckpoint,
5553    ) {
5554        if self
5555            .source_handles
5556            .get(source_idx)
5557            .is_some_and(|handle| !handle.recovery_cursor)
5558        {
5559            return;
5560        }
5561        if let Some(name) = self.source_names.get(source_idx) {
5562            self.pending_barrier
5563                .source_checkpoints
5564                .insert(name.to_string(), checkpoint.clone());
5565        }
5566    }
5567
5568    async fn handle_aligned_checkpoint_outcome(
5569        &mut self,
5570        callback: &mut impl PipelineCallback,
5571        outcome: BarrierOutcome,
5572        context: AlignedCheckpointContext,
5573        source_checkpoints: &FxHashMap<String, SourceCheckpoint>,
5574    ) -> Result<(), String> {
5575        let AlignedCheckpointContext {
5576            cleanup_owner,
5577            attempt,
5578            started_at,
5579            assignment_fence,
5580        } = context;
5581        let authoritative_abort = matches!(&outcome, BarrierOutcome::Aborted);
5582        let (cleanup_reason, manual_reason, record_failure) = match outcome {
5583            BarrierOutcome::Committed(epoch) if epoch == attempt.epoch => {
5584                #[cfg(feature = "cluster")]
5585                if let Err(error) = self.require_process_authority("aligned checkpoint publication")
5586                {
5587                    let reason = error.to_string();
5588                    callback.abort_subscription_cut(attempt);
5589                    self.fail_manual_attempt(attempt, &reason);
5590                    return Err(reason);
5591                }
5592                let publication_error = callback.publish_barrier(attempt).err();
5593                self.broadcast_epoch_committed(epoch, source_checkpoints);
5594                self.finish_manual_success(
5595                    attempt,
5596                    &crate::checkpoint_coordinator::CheckpointResult {
5597                        success: true,
5598                        checkpoint_id: attempt.checkpoint_id,
5599                        epoch: attempt.epoch,
5600                        duration: started_at.elapsed(),
5601                        error: None,
5602                        failure_disposition: None,
5603                    },
5604                );
5605                return publication_error.map_or(Ok(()), Err);
5606            }
5607            BarrierOutcome::Async => {
5608                return Ok(());
5609            }
5610            BarrierOutcome::Committed(epoch) => {
5611                let reason = format!(
5612                    "checkpoint callback committed epoch {epoch} for reserved epoch {}",
5613                    attempt.epoch
5614                );
5615                (reason.clone(), reason, true)
5616            }
5617            BarrierOutcome::Skipped(reason) => {
5618                tracing::debug!(
5619                    checkpoint_id = attempt.checkpoint_id,
5620                    epoch = attempt.epoch,
5621                    reason = %reason,
5622                    "barrier checkpoint skipped"
5623                );
5624                (
5625                    reason.to_string(),
5626                    format!("manual checkpoint skipped: {reason}"),
5627                    false,
5628                )
5629            }
5630            BarrierOutcome::CancelledBeforeCapture => {
5631                tracing::info!(
5632                    checkpoint_id = attempt.checkpoint_id,
5633                    epoch = attempt.epoch,
5634                    "barrier checkpoint topology closed before state capture"
5635                );
5636                let reason = "checkpoint topology closed before state capture";
5637                (reason.into(), format!("manual {reason}"), false)
5638            }
5639            BarrierOutcome::Aborted => {
5640                tracing::info!(
5641                    checkpoint_id = attempt.checkpoint_id,
5642                    epoch = attempt.epoch,
5643                    "barrier checkpoint was authoritatively aborted"
5644                );
5645                let reason =
5646                    "checkpoint was aborted by authoritative cluster control before state capture";
5647                (reason.into(), format!("manual {reason}"), false)
5648            }
5649            BarrierOutcome::Failed => {
5650                tracing::warn!(
5651                    checkpoint_id = attempt.checkpoint_id,
5652                    epoch = attempt.epoch,
5653                    "barrier checkpoint failed"
5654                );
5655                (
5656                    "barrier-aligned checkpoint failed before durable tail".into(),
5657                    "manual barrier-aligned checkpoint failed before the durable tail".into(),
5658                    true,
5659                )
5660            }
5661        };
5662        callback.abort_subscription_cut(attempt);
5663        if authoritative_abort && cleanup_owner == CheckpointCleanupOwner::Follower {
5664            callback.resolve_authoritative_follower_abort(attempt)?;
5665        } else {
5666            Self::cleanup_checkpoint_attempt(
5667                callback,
5668                cleanup_owner,
5669                attempt,
5670                &cleanup_reason,
5671                assignment_fence,
5672            )
5673            .await?;
5674        }
5675        if record_failure {
5676            callback.record_checkpoint_failure(attempt.checkpoint_id, &cleanup_reason);
5677        }
5678        self.fail_manual_attempt(attempt, manual_reason);
5679        Ok(())
5680    }
5681
5682    /// Handle a barrier from a source.
5683    async fn handle_barrier(
5684        &mut self,
5685        source_idx: usize,
5686        barrier: &CheckpointBarrier,
5687        barrier_checkpoint: &SourceCheckpoint,
5688        callback: &mut impl PipelineCallback,
5689    ) -> Result<(), CycleError> {
5690        let barrier_attempt = CheckpointAttempt::new(barrier.epoch, barrier.checkpoint_id);
5691        if !self.pending_barrier.active || self.pending_barrier.attempt != Some(barrier_attempt) {
5692            self.release_source_barrier_for(source_idx, barrier_attempt);
5693            return Ok(());
5694        }
5695        #[cfg(feature = "cluster")]
5696        self.require_process_authority("source barrier handling")
5697            .map_err(|error| CycleError::Recovery(error.to_string()))?;
5698
5699        self.capture_replayable_barrier_cursor(source_idx, barrier_checkpoint);
5700
5701        self.pending_barrier.sources_aligned.insert(source_idx);
5702
5703        if self.pending_barrier.sources_aligned.len() >= self.pending_barrier.sources_total {
5704            let checkpoints = std::mem::take(&mut self.pending_barrier.source_checkpoints);
5705            // Clone for fan-out so each source gets the exact checkpoint that was persisted.
5706            let fan_out = checkpoints.clone();
5707            let attempt = barrier_attempt;
5708            let attempt_started = self.pending_barrier.started_at;
5709            let assignment_fence = self.pending_barrier.assignment_fence.clone();
5710            let cleanup_owner = self.pending_barrier.cleanup_owner;
5711            self.pending_barrier.clear();
5712            let attempt_deadline =
5713                tokio::time::Instant::from_std(attempt_started) + self.config.checkpoint_timeout;
5714            if let Err(error) = callback
5715                .drain_checkpoint_edges_until(attempt_deadline)
5716                .await
5717            {
5718                let error = match error {
5719                    CycleError::Halt(error) => CycleError::Halt(error),
5720                    CycleError::Fatal(error) | CycleError::Recovery(error) => {
5721                        CycleError::Recovery(error)
5722                    }
5723                };
5724                let reason = error.to_string();
5725                self.handle_aligned_checkpoint_outcome(
5726                    callback,
5727                    BarrierOutcome::Failed,
5728                    AlignedCheckpointContext {
5729                        cleanup_owner,
5730                        attempt,
5731                        started_at: attempt_started,
5732                        assignment_fence,
5733                    },
5734                    &fan_out,
5735                )
5736                .await
5737                .map_err(|cleanup| {
5738                    CycleError::Recovery(format!("{reason}; checkpoint cleanup failed: {cleanup}"))
5739                })?;
5740                return Err(error);
5741            }
5742            #[cfg(feature = "cluster")]
5743            if let Err(error) = self.require_process_authority("aligned checkpoint capture") {
5744                let reason = error.to_string();
5745                let cleanup = Self::cleanup_checkpoint_attempt(
5746                    callback,
5747                    cleanup_owner,
5748                    attempt,
5749                    &reason,
5750                    assignment_fence.clone(),
5751                )
5752                .await;
5753                self.fail_manual_attempt(attempt, &reason);
5754                self.release_source_barrier_attempt(attempt);
5755                cleanup.map_err(|cleanup| {
5756                    CycleError::Recovery(format!("{reason}; checkpoint cleanup failed: {cleanup}"))
5757                })?;
5758                return Err(CycleError::Recovery(reason));
5759            }
5760            if let Err(error) = callback.reserve_subscription_cut(attempt) {
5761                self.handle_aligned_checkpoint_outcome(
5762                    callback,
5763                    BarrierOutcome::Failed,
5764                    AlignedCheckpointContext {
5765                        cleanup_owner,
5766                        attempt,
5767                        started_at: attempt_started,
5768                        assignment_fence,
5769                    },
5770                    &fan_out,
5771                )
5772                .await
5773                .map_err(|cleanup| {
5774                    CycleError::Recovery(format!("{error}; checkpoint cleanup failed: {cleanup}"))
5775                })?;
5776                return Err(CycleError::Recovery(error));
5777            }
5778            #[cfg(feature = "cluster")]
5779            if let Err(error) = self.require_process_authority("aligned checkpoint capture start") {
5780                let reason = error.to_string();
5781                callback.abort_subscription_cut(attempt);
5782                let cleanup = Self::cleanup_checkpoint_attempt(
5783                    callback,
5784                    cleanup_owner,
5785                    attempt,
5786                    &reason,
5787                    assignment_fence.clone(),
5788                )
5789                .await;
5790                self.fail_manual_attempt(attempt, &reason);
5791                self.release_source_barrier_attempt(attempt);
5792                cleanup.map_err(|cleanup| {
5793                    CycleError::Recovery(format!("{reason}; checkpoint cleanup failed: {cleanup}"))
5794                })?;
5795                return Err(CycleError::Recovery(reason));
5796            }
5797            let outcome = callback
5798                .checkpoint_with_barrier(
5799                    checkpoints,
5800                    attempt,
5801                    attempt_started,
5802                    assignment_fence.clone(),
5803                )
5804                .await;
5805            let topology_cancelled = matches!(&outcome, BarrierOutcome::CancelledBeforeCapture);
5806            let durable_tail_pending = matches!(&outcome, BarrierOutcome::Async);
5807            self.handle_aligned_checkpoint_outcome(
5808                callback,
5809                outcome,
5810                AlignedCheckpointContext {
5811                    cleanup_owner,
5812                    attempt,
5813                    started_at: attempt_started,
5814                    assignment_fence,
5815                },
5816                &fan_out,
5817            )
5818            .await
5819            .map_err(CycleError::Recovery)?;
5820            if let Some(error) = callback.take_pipeline_fault() {
5821                return Err(CycleError::Recovery(error));
5822            }
5823            // Capture or exact cleanup has completed. A cleanup failure or sticky replay fault
5824            // returns above and deliberately leaves the sources held for coordinated recovery.
5825            self.release_source_barrier_attempt(attempt);
5826            if topology_cancelled && cleanup_owner == CheckpointCleanupOwner::Originator {
5827                self.defer_checkpoint_until_topology_ready();
5828            } else if !topology_cancelled && !durable_tail_pending {
5829                self.advance_checkpoint_cadence();
5830            }
5831        }
5832        Ok(())
5833    }
5834
5835    fn checkpoint_capacity_available(&self) -> bool {
5836        !self.pending_barrier.active && self.checkpoint_in_flight.load(Ordering::Acquire) < 1
5837    }
5838
5839    fn advance_checkpoint_cadence(&mut self) {
5840        self.last_checkpoint = Instant::now();
5841        self.checkpoint_retry_not_before = None;
5842        self.checkpoint_retry_backoff = Duration::ZERO;
5843    }
5844
5845    fn defer_checkpoint_until_topology_ready(&mut self) {
5846        let backoff = if self.checkpoint_retry_backoff.is_zero() {
5847            CHECKPOINT_RETRY_BASE
5848        } else {
5849            self.checkpoint_retry_backoff
5850                .saturating_mul(2)
5851                .min(CHECKPOINT_RETRY_MAX)
5852        };
5853        self.checkpoint_retry_backoff = backoff;
5854        self.checkpoint_retry_not_before = Some(Instant::now() + backoff);
5855    }
5856
5857    async fn checkpoint_admission(
5858        &mut self,
5859        callback: &mut impl PipelineCallback,
5860    ) -> Option<CheckpointAdmission> {
5861        // Requests arriving after a manual attempt was admitted belong to a later cut. Never let
5862        // an intervening periodic attempt consume them or attach them to the active attempt.
5863        if !self.manual_waiting.is_empty() && self.manual_active.is_some() {
5864            return None;
5865        }
5866        let manual = !self.manual_waiting.is_empty();
5867        let leader = callback.is_leader();
5868        if manual && !leader {
5869            self.fail_waiting_manual("only the cluster leader may admit a manual checkpoint");
5870            return None;
5871        }
5872
5873        let interval = leader
5874            && self
5875                .config
5876                .checkpoint_schedule
5877                .periodic_interval()
5878                .is_some_and(|value| self.last_checkpoint.elapsed() >= value);
5879        let retry_ready = self
5880            .checkpoint_retry_not_before
5881            .is_none_or(|deadline| Instant::now() >= deadline);
5882        if !manual && (!interval || !retry_ready) {
5883            return None;
5884        }
5885
5886        // Every trigger observes the same recovery and assignment fence. Periodic work remains
5887        // due through `last_checkpoint`; a caller waiting on a manual request gets a prompt
5888        // rejection without burning an exact attempt ID.
5889        if callback.is_recovering() {
5890            if manual {
5891                self.fail_waiting_manual(
5892                    "manual checkpoint rejected while coordinated recovery is in progress",
5893                );
5894            }
5895            return None;
5896        }
5897        let assignment_fence = match callback.checkpoint_assignment_for_admission().await {
5898            CheckpointAssignmentAdmission::Ready(fence) => fence,
5899            CheckpointAssignmentAdmission::Deferred(reason) => {
5900                tracing::debug!(reason = %reason, "checkpoint admission waits for stable topology");
5901                self.defer_checkpoint_until_topology_ready();
5902                if manual {
5903                    self.fail_waiting_manual(format!(
5904                        "[LDB-6056] manual checkpoint rejected: {reason}"
5905                    ));
5906                }
5907                return None;
5908            }
5909            CheckpointAssignmentAdmission::Fault(reason) => {
5910                callback.record_checkpoint_admission_failure(&reason);
5911                if manual {
5912                    self.fail_waiting_manual(format!(
5913                        "[LDB-6056] manual checkpoint rejected: {reason}"
5914                    ));
5915                }
5916                return None;
5917            }
5918        };
5919        if !self.checkpoint_capacity_available() {
5920            return None;
5921        }
5922        if let Some(admission) = &self.coordinated_commit_admission {
5923            if !admission.can_admit() {
5924                let (known, pending, cap) = admission.state();
5925                warn_external_commit_cap_throttled(known, pending, cap);
5926                return None;
5927            }
5928        }
5929        Some(CheckpointAdmission {
5930            manual,
5931            assignment_fence,
5932        })
5933    }
5934
5935    async fn reserve_prepared_checkpoint_attempt(
5936        &mut self,
5937        callback: &mut impl PipelineCallback,
5938        admission: &CheckpointAdmission,
5939        attempt_started: Instant,
5940    ) -> Result<CheckpointAttempt, String> {
5941        let attempt = callback.reserve_checkpoint_attempt(attempt_started).await?;
5942        tracing::info!(
5943            checkpoint_id = attempt.checkpoint_id,
5944            epoch = attempt.epoch,
5945            "checkpoint attempt reserved"
5946        );
5947        #[cfg(feature = "cluster")]
5948        if let Err(error) = self.require_process_authority("checkpoint prepare publication") {
5949            let reason = error.to_string();
5950            callback
5951                .abandon_checkpoint_attempt(attempt, &reason, admission.assignment_fence.clone())
5952                .await
5953                .map_err(|cleanup| {
5954                    format!("{reason}; reserved checkpoint cleanup failed: {cleanup}")
5955                })?;
5956            return Err(reason);
5957        }
5958        if let Err(error) = callback
5959            .publish_checkpoint_prepare(
5960                attempt,
5961                attempt_started,
5962                admission.assignment_fence.clone(),
5963            )
5964            .await
5965        {
5966            if let Err(cleanup_error) = callback
5967                .abandon_checkpoint_attempt(attempt, &error, admission.assignment_fence.clone())
5968                .await
5969            {
5970                return Err(format!(
5971                    "{error}; reserved checkpoint cleanup failed: {cleanup_error}"
5972                ));
5973            }
5974            return Err(error);
5975        }
5976        #[cfg(feature = "cluster")]
5977        if let Err(error) = self.require_process_authority("checkpoint prepare completion") {
5978            let reason = error.to_string();
5979            callback
5980                .abandon_checkpoint_attempt(attempt, &reason, admission.assignment_fence.clone())
5981                .await
5982                .map_err(|cleanup| {
5983                    format!("{reason}; prepared checkpoint cleanup failed: {cleanup}")
5984                })?;
5985            return Err(reason);
5986        }
5987        Ok(attempt)
5988    }
5989
5990    async fn handle_source_less_checkpoint_outcome(
5991        &mut self,
5992        callback: &mut impl PipelineCallback,
5993        admission: &CheckpointAdmission,
5994        attempt: CheckpointAttempt,
5995        outcome: BarrierOutcome,
5996    ) -> Result<(), String> {
5997        let (cleanup_reason, manual_reason, record_failure) = match outcome {
5998            BarrierOutcome::Committed(epoch) if epoch == attempt.epoch => {
5999                #[cfg(feature = "cluster")]
6000                if let Err(error) =
6001                    self.require_process_authority("source-less checkpoint publication")
6002                {
6003                    let reason = error.to_string();
6004                    callback.abort_subscription_cut(attempt);
6005                    self.fail_manual_attempt(attempt, &reason);
6006                    return Err(reason);
6007                }
6008                let publication_error = callback.publish_barrier(attempt).err();
6009                self.broadcast_epoch_committed(epoch, &FxHashMap::default());
6010                self.finish_manual_success(
6011                    attempt,
6012                    &crate::checkpoint_coordinator::CheckpointResult {
6013                        success: true,
6014                        checkpoint_id: attempt.checkpoint_id,
6015                        epoch: attempt.epoch,
6016                        duration: Duration::ZERO,
6017                        error: None,
6018                        failure_disposition: None,
6019                    },
6020                );
6021                return publication_error.map_or(Ok(()), Err);
6022            }
6023            BarrierOutcome::Committed(epoch) => {
6024                let reason = format!(
6025                    "checkpoint callback committed epoch {epoch} for reserved epoch {}",
6026                    attempt.epoch
6027                );
6028                (reason.clone(), reason, true)
6029            }
6030            BarrierOutcome::Async => return Ok(()),
6031            BarrierOutcome::Skipped(reason) => {
6032                tracing::debug!(
6033                    checkpoint_id = attempt.checkpoint_id,
6034                    epoch = attempt.epoch,
6035                    reason = %reason,
6036                    "source-less checkpoint skipped"
6037                );
6038                (
6039                    reason.to_string(),
6040                    format!("manual checkpoint skipped: {reason}"),
6041                    false,
6042                )
6043            }
6044            BarrierOutcome::CancelledBeforeCapture => {
6045                let reason = "checkpoint topology closed before state capture";
6046                (reason.into(), format!("manual {reason}"), false)
6047            }
6048            BarrierOutcome::Aborted => {
6049                tracing::info!(
6050                    checkpoint_id = attempt.checkpoint_id,
6051                    epoch = attempt.epoch,
6052                    "source-less checkpoint was authoritatively aborted"
6053                );
6054                let reason =
6055                    "checkpoint was aborted by authoritative cluster control before state capture";
6056                (reason.into(), format!("manual {reason}"), false)
6057            }
6058            BarrierOutcome::Failed => (
6059                "source-less checkpoint failed before durable tail".into(),
6060                "manual source-less checkpoint failed before the durable tail".into(),
6061                true,
6062            ),
6063        };
6064        callback.abort_subscription_cut(attempt);
6065        Self::cleanup_checkpoint_attempt(
6066            callback,
6067            CheckpointCleanupOwner::Originator,
6068            attempt,
6069            &cleanup_reason,
6070            admission.assignment_fence.clone(),
6071        )
6072        .await?;
6073        if record_failure {
6074            callback.record_checkpoint_failure(attempt.checkpoint_id, &cleanup_reason);
6075        }
6076        self.fail_manual_attempt(attempt, manual_reason);
6077        Ok(())
6078    }
6079
6080    async fn admit_source_less_checkpoint(
6081        &mut self,
6082        callback: &mut impl PipelineCallback,
6083        admission: &CheckpointAdmission,
6084    ) {
6085        let attempt_started = Instant::now();
6086        let attempt = match self
6087            .reserve_prepared_checkpoint_attempt(callback, admission, attempt_started)
6088            .await
6089        {
6090            Ok(attempt) => attempt,
6091            Err(error) => {
6092                callback.record_checkpoint_admission_failure(&error);
6093                if admission.manual {
6094                    self.fail_waiting_manual(format!(
6095                        "manual checkpoint attempt reservation failed: {error}"
6096                    ));
6097                }
6098                return;
6099            }
6100        };
6101        self.complete_prepared_source_less_checkpoint(
6102            callback,
6103            admission,
6104            attempt,
6105            attempt_started,
6106        )
6107        .await;
6108    }
6109
6110    async fn complete_prepared_source_less_checkpoint(
6111        &mut self,
6112        callback: &mut impl PipelineCallback,
6113        admission: &CheckpointAdmission,
6114        attempt: CheckpointAttempt,
6115        attempt_started: Instant,
6116    ) {
6117        if admission.manual {
6118            self.activate_manual_attempt(attempt);
6119        }
6120        let attempt_deadline =
6121            tokio::time::Instant::from_std(attempt_started) + self.config.checkpoint_timeout;
6122        if let Err(error) = callback
6123            .drain_checkpoint_edges_until(attempt_deadline)
6124            .await
6125        {
6126            tracing::error!(%error, "source-less checkpoint graph drain failed");
6127            if let Err(cleanup_error) = self
6128                .handle_source_less_checkpoint_outcome(
6129                    callback,
6130                    admission,
6131                    attempt,
6132                    BarrierOutcome::Failed,
6133                )
6134                .await
6135            {
6136                callback.record_checkpoint_failure(
6137                    attempt.checkpoint_id,
6138                    &format!("checkpoint cleanup failed after graph drain: {cleanup_error}"),
6139                );
6140            }
6141            self.advance_checkpoint_cadence();
6142            return;
6143        }
6144        #[cfg(feature = "cluster")]
6145        if let Err(error) = self.require_process_authority("source-less checkpoint capture") {
6146            let reason = error.to_string();
6147            let cleanup = Self::cleanup_checkpoint_attempt(
6148                callback,
6149                CheckpointCleanupOwner::Originator,
6150                attempt,
6151                &reason,
6152                admission.assignment_fence.clone(),
6153            )
6154            .await;
6155            self.fail_manual_attempt(attempt, &reason);
6156            callback.record_checkpoint_failure(attempt.checkpoint_id, &reason);
6157            if let Err(cleanup_error) = cleanup {
6158                callback.record_checkpoint_failure(
6159                    attempt.checkpoint_id,
6160                    &format!("{reason}; checkpoint cleanup failed: {cleanup_error}"),
6161                );
6162            }
6163            self.advance_checkpoint_cadence();
6164            return;
6165        }
6166        if let Err(error) = callback.reserve_subscription_cut(attempt) {
6167            tracing::error!(%error, "source-less subscription cut reservation failed");
6168            if let Err(cleanup_error) = self
6169                .handle_source_less_checkpoint_outcome(
6170                    callback,
6171                    admission,
6172                    attempt,
6173                    BarrierOutcome::Failed,
6174                )
6175                .await
6176            {
6177                callback.record_checkpoint_failure(
6178                    attempt.checkpoint_id,
6179                    &format!("{error}; checkpoint cleanup failed: {cleanup_error}"),
6180                );
6181            }
6182            self.advance_checkpoint_cadence();
6183            return;
6184        }
6185        #[cfg(feature = "cluster")]
6186        if let Err(error) = self.require_process_authority("source-less checkpoint capture start") {
6187            let reason = error.to_string();
6188            callback.abort_subscription_cut(attempt);
6189            let cleanup = Self::cleanup_checkpoint_attempt(
6190                callback,
6191                CheckpointCleanupOwner::Originator,
6192                attempt,
6193                &reason,
6194                admission.assignment_fence.clone(),
6195            )
6196            .await;
6197            self.fail_manual_attempt(attempt, &reason);
6198            callback.record_checkpoint_failure(attempt.checkpoint_id, &reason);
6199            if let Err(cleanup_error) = cleanup {
6200                callback.record_checkpoint_failure(
6201                    attempt.checkpoint_id,
6202                    &format!("{reason}; checkpoint cleanup failed: {cleanup_error}"),
6203                );
6204            }
6205            self.advance_checkpoint_cadence();
6206            return;
6207        }
6208        let outcome = callback
6209            .checkpoint_with_barrier(
6210                FxHashMap::default(),
6211                attempt,
6212                attempt_started,
6213                admission.assignment_fence.clone(),
6214            )
6215            .await;
6216        let retry_after_topology_change =
6217            matches!(&outcome, BarrierOutcome::CancelledBeforeCapture);
6218        let durable_tail_pending = matches!(&outcome, BarrierOutcome::Async);
6219        if let Err(error) = self
6220            .handle_source_less_checkpoint_outcome(callback, admission, attempt, outcome)
6221            .await
6222        {
6223            callback.record_checkpoint_continuation_fault(attempt, &error);
6224        }
6225        if retry_after_topology_change {
6226            self.defer_checkpoint_until_topology_ready();
6227        } else if !durable_tail_pending {
6228            self.advance_checkpoint_cadence();
6229        }
6230    }
6231
6232    async fn admit_source_barrier_checkpoint(
6233        &mut self,
6234        callback: &mut impl PipelineCallback,
6235        admission: &CheckpointAdmission,
6236    ) {
6237        if self
6238            .source_handles
6239            .iter()
6240            .any(|handle| !handle.barrier_injector.can_trigger())
6241        {
6242            tracing::debug!(
6243                "checkpoint admission deferred: a source barrier injector is still busy"
6244            );
6245            return;
6246        }
6247
6248        let attempt_started = Instant::now();
6249        let attempt = match self
6250            .reserve_prepared_checkpoint_attempt(callback, admission, attempt_started)
6251            .await
6252        {
6253            Ok(attempt) => attempt,
6254            Err(error) => {
6255                callback.record_checkpoint_admission_failure(&error);
6256                if admission.manual {
6257                    self.fail_waiting_manual(format!(
6258                        "manual checkpoint attempt reservation failed: {error}"
6259                    ));
6260                }
6261                return;
6262            }
6263        };
6264        self.inject_prepared_source_barrier_attempt(callback, admission, attempt, attempt_started)
6265            .await;
6266    }
6267
6268    async fn inject_prepared_source_barrier_attempt(
6269        &mut self,
6270        callback: &mut impl PipelineCallback,
6271        admission: &CheckpointAdmission,
6272        attempt: CheckpointAttempt,
6273        attempt_started: Instant,
6274    ) {
6275        if admission.manual {
6276            self.activate_manual_attempt(attempt);
6277        }
6278        #[cfg(feature = "cluster")]
6279        if let Err(error) = self.require_process_authority("source barrier injection") {
6280            let reason = error.to_string();
6281            let cleanup = Self::cleanup_checkpoint_attempt(
6282                callback,
6283                CheckpointCleanupOwner::Originator,
6284                attempt,
6285                &reason,
6286                admission.assignment_fence.clone(),
6287            )
6288            .await;
6289            self.fail_manual_attempt(attempt, &reason);
6290            callback.record_checkpoint_failure(attempt.checkpoint_id, &reason);
6291            if let Err(cleanup_error) = cleanup {
6292                callback.record_checkpoint_failure(
6293                    attempt.checkpoint_id,
6294                    &format!("{reason}; checkpoint cleanup failed: {cleanup_error}"),
6295                );
6296            }
6297            return;
6298        }
6299        self.pending_barrier.reset_with_assignment(
6300            attempt,
6301            self.source_handles.len(),
6302            admission.assignment_fence.clone(),
6303        );
6304        // Attempt time includes reservation, alignment, capture, quorum, and publication.
6305        self.pending_barrier.started_at = attempt_started;
6306        let barrier = CheckpointBarrier::new(attempt.checkpoint_id, attempt.epoch);
6307
6308        for handle in &self.source_handles {
6309            if !handle.barrier_injector.trigger(barrier) {
6310                self.pending_barrier.clear();
6311                self.cancel_local_source_barriers(barrier);
6312                let cleanup = Self::cleanup_checkpoint_attempt(
6313                    callback,
6314                    CheckpointCleanupOwner::Originator,
6315                    attempt,
6316                    "source barrier injection was rejected after preflight",
6317                    admission.assignment_fence.clone(),
6318                )
6319                .await;
6320                if cleanup.is_ok() {
6321                    self.release_source_barrier_attempt(attempt);
6322                } else if let Err(error) = cleanup {
6323                    callback.record_checkpoint_failure(
6324                        attempt.checkpoint_id,
6325                        &format!("source barrier injection cleanup failed: {error}"),
6326                    );
6327                }
6328                callback.record_checkpoint_failure(
6329                    attempt.checkpoint_id,
6330                    "source barrier injection was rejected after preflight",
6331                );
6332                self.fail_manual_attempt(
6333                    attempt,
6334                    "manual checkpoint source barrier injection was rejected after preflight",
6335                );
6336                return;
6337            }
6338        }
6339    }
6340
6341    /// Service periodic, manual, or leader-announced checkpoint admission.
6342    async fn maybe_checkpoint(&mut self, callback: &mut impl PipelineCallback) -> bool {
6343        self.drain_manual_requests();
6344        #[cfg(feature = "cluster")]
6345        if let Err(error) = self.require_process_authority("checkpoint admission") {
6346            let reason = error.to_string();
6347            callback.record_checkpoint_admission_failure(&reason);
6348            self.fail_waiting_manual(reason);
6349            return true;
6350        }
6351
6352        // Followers do not originate attempts. Preserve their resource cap while servicing the
6353        // leader's exact control announcement; leader/local admission applies its own cap below.
6354        if !callback.is_leader() {
6355            if !self.manual_waiting.is_empty() {
6356                self.fail_waiting_manual("only the cluster leader may admit a manual checkpoint");
6357            }
6358            if !self.checkpoint_capacity_available() {
6359                return false;
6360            }
6361            #[cfg(feature = "cluster")]
6362            if let Err(error) = self.require_process_authority("checkpoint control admission") {
6363                callback.record_checkpoint_admission_failure(&error.to_string());
6364                return true;
6365            }
6366            let outcome = callback
6367                .service_checkpoint_control(self.current_source_offsets())
6368                .await;
6369            #[cfg(feature = "cluster")]
6370            if let Err(error) =
6371                self.require_process_authority("follower checkpoint control application")
6372            {
6373                let authority_reason = error.to_string();
6374                match &outcome {
6375                    CheckpointControlOutcome::Started { attempt, .. }
6376                    | CheckpointControlOutcome::Failed { attempt, .. } => {
6377                        callback
6378                            .record_checkpoint_failure(attempt.checkpoint_id, &authority_reason);
6379                    }
6380                    CheckpointControlOutcome::AdmissionFailed { error } => callback
6381                        .record_checkpoint_admission_failure(&format!(
6382                            "{error}; {authority_reason}"
6383                        )),
6384                    CheckpointControlOutcome::Idle
6385                    | CheckpointControlOutcome::Aborted { .. }
6386                    | CheckpointControlOutcome::Cancelled { .. } => {
6387                        callback.record_checkpoint_admission_failure(&authority_reason);
6388                    }
6389                }
6390                return true;
6391            }
6392            match outcome {
6393                CheckpointControlOutcome::Idle => {}
6394                CheckpointControlOutcome::AdmissionFailed { error } => {
6395                    callback.record_checkpoint_admission_failure(&error);
6396                }
6397                CheckpointControlOutcome::Started { attempt, captured } => {
6398                    if !captured {
6399                        self.pending_barrier
6400                            .reset_follower(attempt, self.source_handles.len());
6401                    }
6402                }
6403                CheckpointControlOutcome::Aborted { attempt } => {
6404                    if self.pending_barrier.attempt == Some(attempt) {
6405                        self.pending_barrier.clear();
6406                        self.barrier_seen.clear();
6407                    }
6408                    self.release_source_barrier_attempt(attempt);
6409                    self.fail_manual_attempt(
6410                        attempt,
6411                        "manual checkpoint was aborted by authoritative cluster control",
6412                    );
6413                }
6414                CheckpointControlOutcome::Cancelled { attempt } => {
6415                    if self.pending_barrier.attempt == Some(attempt) {
6416                        self.pending_barrier.clear();
6417                        self.barrier_seen.clear();
6418                    }
6419                    self.release_source_barrier_attempt(attempt);
6420                    self.fail_manual_attempt(
6421                        attempt,
6422                        "manual checkpoint was cancelled after its shuffle scope closed",
6423                    );
6424                }
6425                CheckpointControlOutcome::Failed { attempt, error } => {
6426                    callback.record_checkpoint_failure(attempt.checkpoint_id, &error);
6427                }
6428            }
6429            return true;
6430        }
6431        let Some(admission) = self.checkpoint_admission(callback).await else {
6432            return true;
6433        };
6434        #[cfg(feature = "cluster")]
6435        if let Err(error) = self.require_process_authority("checkpoint attempt creation") {
6436            let reason = error.to_string();
6437            callback.record_checkpoint_admission_failure(&reason);
6438            self.fail_waiting_manual(reason);
6439            return true;
6440        }
6441        if self.source_handles.is_empty() {
6442            self.admit_source_less_checkpoint(callback, &admission)
6443                .await;
6444        } else {
6445            self.admit_source_barrier_checkpoint(callback, &admission)
6446                .await;
6447        }
6448        true
6449    }
6450}
6451
6452#[cfg(test)]
6453mod tests;