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};
15use laminar_connectors::checkpoint::SourceCheckpoint;
16#[cfg(test)]
17use laminar_connectors::checkpoint::SourceCheckpointDelta;
18use laminar_connectors::connector::{
19 schema_with_source_mutations_and_row_positions, schema_with_source_row_positions,
20 strip_source_row_positions, ConnectorCancellationPolicy, ConnectorTaskTracker,
21 DeliveryGuarantee, SourceBatch, SourceBatchCursor, SourceCheckpointUnavailablePolicy,
22 SourceConnector, SourceConsistency, SourceContract, SourceInputMode, SourcePosition,
23 SourceRowPositionCapability, SourceStart, SOURCE_MUTATION_COLUMN,
24};
25#[cfg(feature = "cluster")]
26use laminar_connectors::connector::{
27 SourceDrainOutcome, SourceDrainRequest, SourceDrainResolution,
28};
29use laminar_connectors::error::ConnectorError;
30#[cfg(feature = "cluster")]
31use laminar_core::checkpoint::{
32 AssignmentDrainId, AssignmentDrainTransition, CheckpointParticipant,
33};
34use laminar_core::checkpoint::{CheckpointAttempt, CheckpointAttemptRelation};
35use laminar_core::checkpoint::{CheckpointBarrier, CheckpointBarrierInjector};
36#[cfg(feature = "cluster")]
37use laminar_core::cluster::control::ClusterController;
38use rustc_hash::{FxHashMap, FxHashSet};
39
40use super::callback::{
41 BarrierOutcome, CheckpointAssignmentAdmission, CheckpointCompletion, CheckpointControlOutcome,
42 CheckpointControlWake, CycleError, CycleOutcome, PipelineCallback, PortHeadroom,
43 SourceBarrierControl, SourceBarrierSignal, SourceRegistration,
44};
45#[cfg(test)]
46use super::config::CheckpointSchedule;
47use super::config::PipelineConfig;
48use crate::catalog::{schema_has_reserved_mutation_columns, validate_source_batch};
49use crate::connector_task_fence::{ConnectorTaskFenceRegistration, OwnedConnectorTaskFences};
50use crate::error::DbError;
51
52pub(crate) use flow_metrics::CoordinatorFlowMetrics;
53use source_channel::{QueuedSourceMsg, SourceMsgRx, SourceMsgTx};
54type ControlMsgRx = AsyncRx<mpsc::Array<super::ControlMsg>>;
55type ForceCheckpointRequest = crate::db::ForceCheckpointRequest;
56
57#[derive(Clone, Copy, Debug)]
58struct CyclePublicationDurations {
59 output_store_ns: u64,
60 sink_enqueue_ns: u64,
61}
62
63mod barrier_handling;
64mod checkpoint_admission;
65mod checkpoint_lifecycle;
66mod construction;
67mod cycle;
68mod drain_ownership;
69mod execution;
70mod flow_metrics;
71mod intake_backpressure;
72use intake_backpressure::SourceAdmission;
73mod shutdown;
74mod source_actor;
75mod source_channel;
76mod source_drain;
77mod source_lifecycle;
78mod source_runtime;
79
80use source_drain::{acknowledge_latest_source_commit, take_batch_cursor, try_source_checkpoint};
81#[cfg(feature = "cluster")]
82use source_drain::{
83 apply_latest_source_drain_command_fenced, publish_source_drain_ready_fenced,
84 resolve_pending_source_drain_fenced, source_drain_flushing, source_drain_held,
85};
86#[cfg(feature = "cluster")]
87use source_lifecycle::{
88 check_source_sync_fence, source_operation_authority_error, wait_source_drain_hold,
89};
90use source_lifecycle::{
91 poll_source_once, run_source_operation, source_operation_deadline_error, start_source_once,
92 wait_source_barrier_release, wait_source_idle, SourceConnectorLifecycle,
93 SourceOperationOutcome, SourcePollOutcome, SourceStartFailure, SourceStartOutcome,
94};
95use source_runtime::{
96 send_source_msg, spawn_source_actor, wait_coordinator_delay, SourceFault, SourceMsg,
97 SourceTaskExitGuard, StreamingCoordinatorGeneration,
98};
99#[cfg(feature = "cluster")]
100use source_runtime::{
101 source_process_authority_is_live, validate_source_drain_receipts, ActiveSourceDrain,
102 PendingSourceDrainResolution, SourceDrainCommand, SourceDrainCommandPolicy,
103 SourceDrainLeaseControl, SourceDrainReceipt, SourceDrainTaskStatus, SourceProcessAuthority,
104};
105
106#[cfg(all(test, feature = "cluster"))]
107pub(crate) use drain_ownership::install_replacement_source_drain_task_for_test;
108#[cfg(feature = "cluster")]
109pub(crate) use drain_ownership::{
110 owned_source_drain_resolved, prepare_owned_source_drain, resolve_owned_source_drain,
111};
112pub use source_runtime::StreamingCoordinatorRuntime;
113pub(crate) use source_runtime::{OwnedSourceTasks, SourceTaskLease};
114
115struct SourceHandle {
116 recovery_cursor: bool,
119 task: SourceTaskLease,
120 startup_activation: Option<crossfire::oneshot::TxOneshot<()>>,
123 barrier_injector: CheckpointBarrierInjector,
124 barrier_release_tx: tokio::sync::watch::Sender<Option<SourceBarrierSignal>>,
126 epoch_committed_tx: tokio::sync::watch::Sender<Option<(u64, SourceCheckpoint)>>,
130}
131
132pub(crate) struct TrackedSourceRegistration {
133 source: SourceRegistration,
134 contract: SourceContract,
135 expected_schema: arrow_schema::SchemaRef,
136 positioned_schema: arrow_schema::SchemaRef,
137 mutation_schema: arrow_schema::SchemaRef,
138 primary_key: Vec<String>,
139 primary_key_indices: Vec<usize>,
140 schema_admitted: bool,
141 admitted_non_append_mode: Option<SourceInputMode>,
142 task_fence: ConnectorTaskFenceRegistration,
143}
144
145pub(crate) const MUTATION_SOURCE_NOT_ADMITTED: &str =
146 "[LDB-5039] mutation sources require an exclusively admitted stateful operator route";
147
148pub(crate) fn admit_append_only_source(
149 contract: SourceContract,
150 has_reserved_mutation_columns: bool,
151) -> Result<(), &'static str> {
152 if contract.input_mode == SourceInputMode::AppendOnly && !has_reserved_mutation_columns {
153 Ok(())
154 } else {
155 Err(MUTATION_SOURCE_NOT_ADMITTED)
156 }
157}
158
159impl TrackedSourceRegistration {
160 fn metadata_schemas(
161 source_name: &str,
162 contract: SourceContract,
163 expected_schema: &arrow_schema::SchemaRef,
164 ) -> Result<(arrow_schema::SchemaRef, arrow_schema::SchemaRef), DbError> {
165 let map_error = |error| {
166 DbError::Config(format!(
167 "source '{source_name}' has an invalid source-metadata schema: {error}"
168 ))
169 };
170 let positioned = schema_with_source_row_positions(expected_schema).map_err(map_error)?;
171 let mutations =
172 schema_with_source_mutations_and_row_positions(expected_schema).map_err(map_error)?;
173 if contract.row_positions == SourceRowPositionCapability::OrderedDeterministic {
174 Ok((positioned, mutations))
175 } else {
176 Ok((Arc::clone(expected_schema), Arc::clone(expected_schema)))
177 }
178 }
179
180 fn resolve_contract(source: &SourceRegistration) -> Result<SourceContract, DbError> {
181 let contract = source.connector.contract(&source.config).map_err(|error| {
182 DbError::Config(format!(
183 "source '{}' (type '{}') has an invalid contract: {error}",
184 source.name,
185 source.config.connector_type()
186 ))
187 })?;
188 Ok(contract)
189 }
190
191 pub(crate) fn capture(
192 source: SourceRegistration,
193 owned: &OwnedConnectorTaskFences,
194 ) -> Result<Self, DbError> {
195 let contract = Self::resolve_contract(&source)?;
196 let expected_schema = source.connector.schema();
197 let (positioned_schema, mutation_schema) =
198 Self::metadata_schemas(&source.name, contract, &expected_schema)?;
199 let task_fence = ConnectorTaskFenceRegistration::capture_registered(
200 Arc::<str>::from(format!("source:{}", source.name)),
201 source.connector.terminal_task_tracker(),
202 owned,
203 );
204 Ok(Self {
205 source,
206 contract,
207 expected_schema,
208 positioned_schema,
209 mutation_schema,
210 primary_key: Vec::new(),
211 primary_key_indices: Vec::new(),
212 schema_admitted: false,
213 admitted_non_append_mode: None,
214 task_fence,
215 })
216 }
217
218 pub(crate) fn from_captured(
219 source: SourceRegistration,
220 task_fence: ConnectorTaskFenceRegistration,
221 ) -> Result<Self, DbError> {
222 let contract = Self::resolve_contract(&source)?;
223 let expected_schema = source.connector.schema();
224 let (positioned_schema, mutation_schema) =
225 Self::metadata_schemas(&source.name, contract, &expected_schema)?;
226 Ok(Self {
227 source,
228 contract,
229 expected_schema,
230 positioned_schema,
231 mutation_schema,
232 primary_key: Vec::new(),
233 primary_key_indices: Vec::new(),
234 schema_admitted: false,
235 admitted_non_append_mode: None,
236 task_fence,
237 })
238 }
239
240 pub(crate) fn with_admitted_schema(
241 mut self,
242 expected_schema: arrow_schema::SchemaRef,
243 primary_key: Vec<String>,
244 ) -> Result<Self, DbError> {
245 let primary_key_indices = primary_key
246 .iter()
247 .map(|column| {
248 expected_schema.index_of(column).map_err(|_| {
249 DbError::Config(format!(
250 "source '{}' primary-key column '{column}' is absent from its admitted schema",
251 self.name
252 ))
253 })
254 })
255 .collect::<Result<Vec<_>, _>>()?;
256 self.expected_schema = expected_schema;
257 (self.positioned_schema, self.mutation_schema) =
258 Self::metadata_schemas(&self.name, self.contract, &self.expected_schema)?;
259 self.primary_key = primary_key;
260 self.primary_key_indices = primary_key_indices;
261 self.schema_admitted = true;
262 Ok(self)
263 }
264
265 pub(crate) const fn contract(&self) -> SourceContract {
266 self.contract
267 }
268
269 pub(crate) fn with_keyed_upsert_mutations(mut self) -> Self {
271 debug_assert_eq!(self.contract.input_mode, SourceInputMode::KeyedUpsert);
272 self.admitted_non_append_mode = Some(SourceInputMode::KeyedUpsert);
273 self
274 }
275
276 pub(crate) fn with_full_changelog(mut self) -> Self {
278 debug_assert_eq!(self.contract.input_mode, SourceInputMode::FullChangelog);
279 self.admitted_non_append_mode = Some(SourceInputMode::FullChangelog);
280 self
281 }
282
283 pub(crate) fn with_ordered_interval_input_mode(
284 mut self,
285 mode: SourceInputMode,
286 ) -> Result<Self, DbError> {
287 if mode == SourceInputMode::AppendOnly || self.contract.input_mode != mode {
288 return Err(DbError::Config(format!(
289 "source '{}' lost its admitted bounded-interval input mode",
290 self.name
291 )));
292 }
293 if self
294 .admitted_non_append_mode
295 .is_some_and(|admitted| admitted != mode)
296 {
297 return Err(DbError::Config(format!(
298 "source '{}' has conflicting stateful mutation routes",
299 self.name
300 )));
301 }
302 self.admitted_non_append_mode = Some(mode);
303 Ok(self)
304 }
305
306 fn has_reserved_mutation_columns(&self) -> bool {
307 schema_has_reserved_mutation_columns(self.expected_schema.as_ref())
308 }
309}
310
311fn prepare_encoded_source_batch(
312 source_name: &str,
313 expected_schema: &arrow_schema::SchemaRef,
314 positioned_schema: &arrow_schema::SchemaRef,
315 mutation_schema: &arrow_schema::SchemaRef,
316 primary_key: &[String],
317 primary_key_indices: &[usize],
318 capability: SourceRowPositionCapability,
319 batch: SourceBatch,
320) -> Result<RecordBatch, laminar_core::streaming::StreamingError> {
321 validate_source_batch(
322 source_name,
323 expected_schema,
324 primary_key,
325 primary_key_indices,
326 &batch.records,
327 )?;
328 batch
329 .into_records_with_metadata(capability, positioned_schema, mutation_schema)
330 .map_err(|error| {
331 laminar_core::streaming::StreamingError::InvalidConfig(format!(
332 "source '{source_name}' emitted invalid source metadata: {error}"
333 ))
334 })
335}
336
337impl std::ops::Deref for TrackedSourceRegistration {
338 type Target = SourceRegistration;
339
340 fn deref(&self) -> &Self::Target {
341 &self.source
342 }
343}
344
345impl std::ops::DerefMut for TrackedSourceRegistration {
346 fn deref_mut(&mut self) -> &mut Self::Target {
347 &mut self.source
348 }
349}
350
351struct PreparedSourceGeneration {
352 registration: TrackedSourceRegistration,
353}
354
355impl SourceHandle {
356 fn barrier_control(&self) -> SourceBarrierControl {
357 SourceBarrierControl::new(
358 self.barrier_injector.clone(),
359 self.barrier_release_tx.clone(),
360 )
361 }
362}
363
364#[derive(Debug)]
366pub enum ExitReason {
367 Shutdown,
369 Halt(String),
372 Fault(String),
374}
375
376struct PendingWatermarkBatch {
377 source_name: Arc<str>,
378 batch: RecordBatch,
379 admission_floor: i64,
380 input_channels: Option<Arc<[Vec<u8>]>>,
381}
382
383pub struct StreamingCoordinator {
385 config: PipelineConfig,
386 rx: SourceMsgRx,
387 source_fault_rx: tokio::sync::mpsc::UnboundedReceiver<SourceFault>,
388 source_handles: Vec<SourceHandle>,
389 source_names: Vec<Arc<str>>,
390 source_input_modes: Vec<Option<SourceInputMode>>,
391 shutdown: Arc<tokio::sync::Notify>,
392 terminal_shutdown: tokio_util::sync::CancellationToken,
393 pending_barrier: PendingBarrier,
394 last_checkpoint: Instant,
395 checkpoint_retry_not_before: Option<Instant>,
396 checkpoint_retry_backoff: Duration,
397 source_batches_buf: FxHashMap<Arc<str>, Vec<RecordBatch>>,
398 parked_source_msg: Option<QueuedSourceMsg>,
402 source_admission: SourceAdmission,
403 pending_watermark_batches: Vec<PendingWatermarkBatch>,
404 barrier_seen: FxHashSet<usize>,
407 committed_offsets: Vec<Option<SourceCheckpoint>>,
409 pending_offsets: Vec<Option<SourceBatchCursor>>,
412 replay_pending: bool,
415 control_rx: ControlMsgRx,
416 checkpoint_complete_rx:
417 Option<crossfire::AsyncRx<crossfire::mpsc::Array<CheckpointCompletion>>>,
418 force_ckpt_rx: Option<crate::db::ForceCheckpointRx>,
420 manual_waiting: Vec<ForceCheckpointRequest>,
421 manual_handoff_required: bool,
423 manual_active: Option<ManualCheckpointAttempt>,
425 checkpoint_in_flight: Arc<AtomicU64>,
427 last_published_checkpoint: Option<CheckpointAttempt>,
430 #[cfg(feature = "cluster")]
431 process_authority: Option<Arc<SourceProcessAuthority>>,
432 public_generation: Option<StreamingCoordinatorGeneration>,
433 intake_stats: Arc<flow_metrics::IntakeStats>,
434}
435
436#[allow(clippy::struct_excessive_bools)]
439struct CoordinatorRunState {
440 batch_window: Duration,
441 checkpoint_control_wake: Option<CheckpointControlWake>,
442 shuffle_work_wake: Option<Arc<tokio::sync::Notify>>,
443 checkpoint_control_poll_at: tokio::time::Instant,
444 checkpoint_control_pending: bool,
445 intake_was_paused: bool,
448 barriers: Vec<(usize, CheckpointBarrier, SourceCheckpoint)>,
449 fault: Option<String>,
450 halted: bool,
451 halt_reason: Option<String>,
452 source_channel_expected: bool,
453}
454
455impl CoordinatorRunState {
456 fn halt(&mut self, reason: impl Into<String>) {
457 self.halted = true;
458 self.halt_reason.get_or_insert_with(|| reason.into());
459 self.fault = None;
460 }
461}
462
463#[derive(Default)]
464struct CoordinatorWake {
465 message: Option<QueuedSourceMsg>,
466 checkpoint_control_due: bool,
467 gates: CoordinatorGates,
468}
469
470#[derive(Default)]
471struct CoordinatorGates {
472 intake_paused: bool,
473 external_output_backpressured: bool,
474}
475
476enum CoordinatorWaitAction {
477 Cycle,
478 Continue,
479 Stop,
480}
481
482struct CoordinatorWait {
483 action: CoordinatorWaitAction,
484 wake: CoordinatorWake,
485}
486
487impl CoordinatorWait {
488 fn cycle(wake: CoordinatorWake) -> Self {
489 Self {
490 action: CoordinatorWaitAction::Cycle,
491 wake,
492 }
493 }
494
495 fn continue_loop() -> Self {
496 Self {
497 action: CoordinatorWaitAction::Continue,
498 wake: CoordinatorWake::default(),
499 }
500 }
501
502 fn stop() -> Self {
503 Self {
504 action: CoordinatorWaitAction::Stop,
505 wake: CoordinatorWake::default(),
506 }
507 }
508}
509
510impl Drop for StreamingCoordinator {
511 fn drop(&mut self) {
512 for handle in &self.source_handles {
516 handle.task.request_shutdown();
517 }
518 self.intake_stats.close();
519 drop(self.public_generation.take());
521 }
522}
523
524struct ManualCheckpointAttempt {
526 attempt: CheckpointAttempt,
527 flags: u64,
528 requests: Vec<ForceCheckpointRequest>,
529}
530
531struct CheckpointAdmission {
532 manual: bool,
533 flags: u64,
534 assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
535 assignment_guard: Option<tokio::sync::OwnedMutexGuard<()>>,
539 deadline: tokio::time::Instant,
540}
541
542#[derive(Clone, Copy, Debug, PartialEq, Eq)]
543enum CheckpointCleanupOwner {
544 Originator,
546 Follower,
548}
549
550struct AlignedCheckpointContext {
551 cleanup_owner: CheckpointCleanupOwner,
552 attempt: CheckpointAttempt,
553 started_at: Instant,
554 flags: u64,
555 assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
556}
557
558struct PendingBarrier {
560 attempt: Option<CheckpointAttempt>,
561 sources_total: usize,
562 sources_aligned: FxHashSet<usize>,
563 source_checkpoints: FxHashMap<String, SourceCheckpoint>,
564 started_at: Instant,
565 deadline: Option<tokio::time::Instant>,
566 active: bool,
567 flags: u64,
568 assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
569 cleanup_owner: CheckpointCleanupOwner,
570}
571
572impl PendingBarrier {
573 fn new() -> Self {
574 Self {
575 attempt: None,
576 sources_total: 0,
577 sources_aligned: FxHashSet::default(),
578 source_checkpoints: FxHashMap::default(),
579 started_at: Instant::now(),
580 deadline: None,
581 active: false,
582 flags: laminar_core::checkpoint::flags::NONE,
583 assignment_fence: None,
584 cleanup_owner: CheckpointCleanupOwner::Originator,
585 }
586 }
587
588 #[cfg(test)]
589 fn reset(&mut self, attempt: CheckpointAttempt, sources_total: usize) {
590 self.reset_with_assignment(
591 attempt,
592 sources_total,
593 laminar_core::checkpoint::flags::NONE,
594 None,
595 None,
596 );
597 }
598
599 fn reset_follower(&mut self, attempt: CheckpointAttempt, sources_total: usize, flags: u64) {
600 self.reset_inner(
601 attempt,
602 sources_total,
603 flags,
604 None,
605 CheckpointCleanupOwner::Follower,
606 );
607 }
608
609 fn reset_with_assignment(
610 &mut self,
611 attempt: CheckpointAttempt,
612 sources_total: usize,
613 flags: u64,
614 assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
615 deadline: Option<tokio::time::Instant>,
616 ) {
617 self.reset_inner(
618 attempt,
619 sources_total,
620 flags,
621 assignment_fence,
622 CheckpointCleanupOwner::Originator,
623 );
624 self.deadline = deadline;
625 }
626
627 fn attempt_deadline(&self, checkpoint_timeout: Duration) -> tokio::time::Instant {
628 self.deadline
629 .unwrap_or_else(|| tokio::time::Instant::from_std(self.started_at) + checkpoint_timeout)
630 }
631
632 fn reset_inner(
633 &mut self,
634 attempt: CheckpointAttempt,
635 sources_total: usize,
636 flags: u64,
637 assignment_fence: Option<laminar_core::cluster::control::CheckpointAssignmentFence>,
638 cleanup_owner: CheckpointCleanupOwner,
639 ) {
640 self.attempt = Some(attempt);
641 self.sources_total = sources_total;
642 self.sources_aligned.clear();
643 self.source_checkpoints.clear();
644 self.started_at = Instant::now();
645 self.deadline = None;
646 self.active = true;
647 self.flags = flags;
648 self.assignment_fence = assignment_fence;
649 self.cleanup_owner = cleanup_owner;
650 }
651
652 fn take_active_attempt(&mut self) -> Option<(CheckpointAttempt, CheckpointCleanupOwner)> {
654 if !self.active {
655 return None;
656 }
657 let cleanup_owner = self.cleanup_owner;
658 self.active = false;
659 self.sources_total = 0;
660 self.sources_aligned.clear();
661 self.source_checkpoints.clear();
662 self.flags = laminar_core::checkpoint::flags::NONE;
663 self.assignment_fence = None;
664 self.deadline = None;
665 self.cleanup_owner = CheckpointCleanupOwner::Originator;
666 self.attempt.take().map(|attempt| (attempt, cleanup_owner))
667 }
668
669 fn clear(&mut self) {
670 self.active = false;
671 self.attempt = None;
672 self.sources_total = 0;
673 self.sources_aligned.clear();
674 self.source_checkpoints.clear();
675 self.flags = laminar_core::checkpoint::flags::NONE;
676 self.assignment_fence = None;
677 self.deadline = None;
678 self.cleanup_owner = CheckpointCleanupOwner::Originator;
679 }
680}
681
682pub(crate) const IDLE_TIMEOUT: Duration = Duration::from_millis(100);
684
685const CHECKPOINT_RETRY_BASE: Duration = Duration::from_millis(100);
687const CHECKPOINT_RETRY_MAX: Duration = Duration::from_secs(5);
688
689const SHUTDOWN_DRAIN_BUDGET: Duration = Duration::from_secs(2);
691
692const SHUTDOWN_JOIN_TIMEOUT: Duration = Duration::from_secs(8);
694
695const SHUTDOWN_COMPLETION_TICK: Duration = Duration::from_millis(10);
699
700#[cfg(test)]
701mod tests;