1use 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#[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
113async 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
132enum SourceMsg {
135 Batch {
136 source_idx: usize,
137 batch: RecordBatch,
138 checkpoint: SourceCheckpoint,
140 },
141 Barrier {
142 source_idx: usize,
143 barrier: CheckpointBarrier,
144 checkpoint: SourceCheckpoint,
145 },
146}
147
148async 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
178struct SourceFault {
181 source: Arc<str>,
182 error: String,
183}
184
185struct 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
262impl<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 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 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#[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 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#[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 #[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 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
946struct SourceHandle {
948 recovery_cursor: bool,
951 task: SourceTaskLease,
952 startup_activation: Option<crossfire::oneshot::TxOneshot<()>>,
955 barrier_injector: CheckpointBarrierInjector,
956 barrier_release_tx: tokio::sync::watch::Sender<Option<SourceBarrierSignal>>,
958 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#[derive(Debug)]
1016pub enum ExitReason {
1017 Shutdown,
1019 Fault(String),
1021}
1022
1023pub 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 parked_source_msg: Option<SourceMsg>,
1040 pending_watermark_batches: Vec<(Arc<str>, RecordBatch)>,
1041 barrier_seen: FxHashSet<usize>,
1044 committed_offsets: Vec<Option<SourceCheckpoint>>,
1046 pending_offsets: Vec<Option<SourceCheckpoint>>,
1049 replay_pending: bool,
1052 control_rx: ControlMsgRx,
1053 checkpoint_complete_rx:
1054 Option<crossfire::AsyncRx<crossfire::mpsc::Array<CheckpointCompletion>>>,
1055 force_ckpt_rx: Option<crate::db::ForceCheckpointRx>,
1057 manual_waiting: Vec<ForceCheckpointReply>,
1058 manual_active: Option<ManualCheckpointAttempt>,
1060 checkpoint_in_flight: Arc<AtomicU64>,
1062 last_published_checkpoint: Option<CheckpointAttempt>,
1065 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 for handle in &self.source_handles {
1138 handle.task.request_shutdown();
1139 }
1140 drop(self.public_generation.take());
1142 }
1143}
1144
1145struct 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 Originator,
1160 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
1171struct 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 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
1268const IDLE_TIMEOUT: Duration = Duration::from_millis(100);
1270
1271const CHECKPOINT_RETRY_BASE: Duration = Duration::from_millis(100);
1273const CHECKPOINT_RETRY_MAX: Duration = Duration::from_secs(5);
1274
1275const SHUTDOWN_DRAIN_BUDGET: Duration = Duration::from_secs(2);
1277
1278const SHUTDOWN_JOIN_TIMEOUT: Duration = Duration::from_secs(8);
1280
1281const SHUTDOWN_COMPLETION_TICK: Duration = Duration::from_millis(10);
1284
1285const SHUTDOWN_CHECKPOINT_TAIL_TIMEOUT: Duration = Duration::from_secs(8);
1289
1290const 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
1326async 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 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 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 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
1978async 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
2153async 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
2322async 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 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 drop(task);
2540 return;
2541 }
2542 task.log_terminal_outcome();
2543 drop(task);
2544 }
2545
2546 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 if task_gate.load(std::sync::atomic::Ordering::Acquire) && !drain_flushing {
3479 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; }
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 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 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 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 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 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 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 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 pub async fn run<C: PipelineCallback>(self, callback: C) -> ExitReason {
4421 self.run_inner(callback, None).await
4422 }
4423
4424 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 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 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 if external_commit_paused && msg.is_none() {
4780 continue;
4781 }
4782 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 let mut drain_count = 0;
4827 let drain_budget = Duration::from_nanos(self.config.drain_budget_ns);
4828 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 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 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 CycleError::Halt(msg) => {
4933 tracing::warn!(reason = %msg, "[LDB-3022] cycle halted");
4934 break;
4935 }
4936 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 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 for handle in &self.source_handles {
4989 handle.task.mark_expected_shutdown();
4990 }
4991
4992 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 } else if let Some(cp) = pending.take() {
5527 self.committed_offsets[i] = Some(cp);
5528 }
5529 }
5530 }
5531
5532 fn discard_pending_offsets(&mut self) {
5534 for slot in &mut self.pending_offsets {
5535 *slot = None;
5536 }
5537 }
5538
5539 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 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 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 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 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 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 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 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 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;