laminar_db/pipeline/streaming_coordinator/
execution.rs1use super::{
4 wait_coordinator_delay, CheckpointCompletion, CheckpointControlWake, CoordinatorGates,
5 CoordinatorRunState, CoordinatorWait, CoordinatorWaitAction, CoordinatorWake, CycleError,
6 Duration, ExitReason, ForceCheckpointRequest, Instant, PipelineCallback, SourceHandle,
7 StreamingCoordinator, IDLE_TIMEOUT,
8};
9
10impl StreamingCoordinator {
11 pub async fn run<C: PipelineCallback>(self, callback: C) -> ExitReason {
16 self.run_inner(callback, None).await
17 }
18
19 pub(crate) async fn run_with_ready<C: PipelineCallback>(
25 self,
26 callback: C,
27 ready: crossfire::oneshot::TxOneshot<Result<(), String>>,
28 ) -> ExitReason {
29 self.run_inner(callback, Some(ready)).await
30 }
31
32 pub(super) async fn wait_for_cycle<C: PipelineCallback>(
33 &mut self,
34 callback: &mut C,
35 state: &mut CoordinatorRunState,
36 ) -> CoordinatorWait {
37 if let Err(error) = callback.prepare_source_intake() {
38 state.fault = Some(format!(
39 "source recovery handoff could not be installed before intake: {error}"
40 ));
41 return CoordinatorWait::stop();
42 }
43 let gates = CoordinatorGates::capture(callback, self.replay_pending);
44 let replay_ready = self.replay_pending
45 && gates.compute_admitted()
46 && (self.manual_handoff_required || callback.has_runnable_deferred_input());
47 let parked_ready =
48 !self.replay_pending && gates.compute_admitted() && self.parked_source_msg.is_some();
49 let mut retrying_replay = false;
50 let mut checkpoint_control_due = false;
51
52 let message = tokio::select! {
53 biased;
54 () = self.terminal_shutdown.cancelled() => return CoordinatorWait::stop(),
55 () = self.shutdown.notified() => return CoordinatorWait::stop(),
56 Some(source_fault) = self.source_fault_rx.recv() => {
57 state.fault = Some(format!(
58 "source '{}' fault: {}",
59 source_fault.source, source_fault.error
60 ));
61 return CoordinatorWait::stop();
62 }
63 Some(completion) = async {
64 if let Some(ref mut rx) = self.checkpoint_complete_rx {
65 rx.recv().await.ok()
66 } else {
67 futures::future::pending::<Option<CheckpointCompletion>>().await
68 }
69 } => {
70 if let Some(error) = self.handle_checkpoint_completion(completion, callback) {
71 state.fault = Some(error);
72 return CoordinatorWait::stop();
73 }
74 if !state.checkpoint_control_pending {
75 return CoordinatorWait::continue_loop();
76 }
77 checkpoint_control_due = true;
78 if state.checkpoint_control_wake.is_some() {
79 state.checkpoint_control_poll_at =
80 tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
81 }
82 None
83 }
84 Some(reply) = async {
85 if let Some(ref mut rx) = self.force_ckpt_rx {
86 rx.recv().await.ok()
87 } else {
88 futures::future::pending::<Option<ForceCheckpointRequest>>().await
89 }
90 } => {
91 self.manual_waiting.push(reply);
92 None
93 },
94 () = async {
95 match state.checkpoint_control_wake.as_mut() {
96 Some(wake) => wake.wait_until(state.checkpoint_control_poll_at).await,
97 None => std::future::pending().await,
98 }
99 }, if !state.checkpoint_control_pending && !callback.is_leader() => {
100 state.checkpoint_control_pending = true;
101 checkpoint_control_due = true;
102 if state.checkpoint_control_wake.is_some() {
103 state.checkpoint_control_poll_at =
104 tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
105 }
106 None
107 },
108 () = tokio::time::sleep_until(state.checkpoint_control_poll_at),
109 if state.checkpoint_control_pending && !callback.is_leader() =>
110 {
111 checkpoint_control_due = true;
112 if state.checkpoint_control_wake.is_some() {
113 state.checkpoint_control_poll_at =
114 tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
115 }
116 None
117 },
118 () = std::future::ready(()), if replay_ready => {
119 retrying_replay = true;
120 None
121 },
122 () = std::future::ready(()), if parked_ready => self.parked_source_msg.take(),
123 msg = self.rx.recv(),
124 if state.source_channel_expected
125 && gates.compute_admitted() =>
126 {
127 if let Ok(message) = msg {
128 if !state.batch_window.is_zero() {
129 let authority_lost = wait_coordinator_delay(
130 state.batch_window,
131 #[cfg(feature = "cluster")]
132 self.process_authority.as_deref(),
133 )
134 .await;
135 if authority_lost {
136 state.fault = Some(
137 "cluster process lease expired during source batch window".into(),
138 );
139 return CoordinatorWait::stop();
140 }
141 }
142 Some(message)
143 } else {
144 state.fault = Some("all configured source tasks exited unexpectedly".into());
145 return CoordinatorWait::stop();
146 }
147 }
148 () = async {
149 match state.shuffle_work_wake.as_ref() {
150 Some(wake) => wake.notified().await,
151 None => std::future::pending().await,
152 }
153 }, if !gates.external_commit_backpressured => None,
154 authority_lost = wait_coordinator_delay(
155 IDLE_TIMEOUT,
156 #[cfg(feature = "cluster")]
157 self.process_authority.as_deref(),
158 ) => {
159 if authority_lost {
160 state.fault =
161 Some("cluster process lease expired while coordinator was idle".into());
162 return CoordinatorWait::stop();
163 }
164 None
165 },
166 };
167
168 CoordinatorWait::cycle(CoordinatorWake {
169 message,
170 retrying_replay,
171 checkpoint_control_due,
172 gates,
173 })
174 }
175
176 pub(super) async fn service_background_work<C: PipelineCallback>(
177 &mut self,
178 callback: &mut C,
179 state: &mut CoordinatorRunState,
180 checkpoint_control_due: bool,
181 ) -> bool {
182 if self.replay_pending && self.pending_barrier.active {
183 let deadline = self
184 .pending_barrier
185 .attempt_deadline(self.config.checkpoint_timeout);
186 if let Err(error) = callback.drain_checkpoint_edges_until(deadline).await {
187 let reason = error.to_string();
188 let terminal = matches!(&error, CycleError::Halt(_));
189 if terminal {
190 state.halt(reason.clone());
191 }
192 tracing::error!(%error, "checkpoint replay drain failed");
193 if let Err(cleanup) = self
194 .cancel_pending_barrier_for_stop(callback, &reason, true)
195 .await
196 {
197 if terminal {
198 tracing::warn!(
199 %cleanup,
200 "checkpoint cleanup also failed after a permanent replay-drain halt"
201 );
202 } else {
203 state.fault = Some(format!(
204 "{reason}; checkpoint cleanup failed after replay drain: {cleanup}"
205 ));
206 }
207 return false;
208 }
209 match error {
210 CycleError::Halt(_) => {}
211 CycleError::Fatal(_) | CycleError::Recovery(_) => state.fault = Some(reason),
212 }
213 return false;
214 }
215 if let Err(error) = self.commit_pending_offsets() {
216 state.fault = Some(error.to_string());
217 return false;
218 }
219 self.replay_pending = false;
220 }
221
222 for (source_idx, barrier, checkpoint) in &state.barriers {
223 match self
224 .handle_barrier(*source_idx, barrier, checkpoint, callback)
225 .await
226 {
227 Ok(()) => {}
228 Err(CycleError::Halt(reason)) => {
229 tracing::warn!(%reason, "[LDB-3022] checkpoint drain halted the pipeline");
230 state.halt(reason);
231 return false;
232 }
233 Err(CycleError::Fatal(reason) | CycleError::Recovery(reason)) => {
234 state.fault = Some(reason);
235 return false;
236 }
237 }
238 }
239 if self.terminal_shutdown.is_cancelled() {
240 return false;
241 }
242 if let Some(reason) = callback.take_pipeline_halt() {
243 self.discard_pending_offsets();
244 tracing::warn!(
245 reason = %reason,
246 "[LDB-3022] permanent pipeline error; stopping without recovery"
247 );
248 state.halt(reason);
249 return false;
250 }
251 if let Some(reason) = callback.take_pipeline_fault() {
252 self.discard_pending_offsets();
253 tracing::error!(
254 reason = %reason,
255 "[LDB-3024] pipeline consistency fault; stopping for recovery"
256 );
257 state.fault = Some(reason);
258 return false;
259 }
260
261 let follower_control_ready =
262 state.checkpoint_control_pending && checkpoint_control_due && !callback.is_leader();
263 let checkpoint_work_due =
264 callback.is_leader() || follower_control_ready || !self.manual_waiting.is_empty();
265 if checkpoint_work_due {
266 let terminal_shutdown = self.terminal_shutdown.clone();
273 let control_serviced = tokio::select! {
274 biased;
275 () = terminal_shutdown.cancelled() => return false,
276 serviced = self.maybe_checkpoint(callback) => serviced,
277 };
278 if follower_control_ready {
279 #[cfg(feature = "cluster")]
280 if let Some(wake) = state.checkpoint_control_wake.as_ref() {
281 if control_serviced {
282 state.checkpoint_control_pending = false;
283 state.checkpoint_control_poll_at =
284 tokio::time::Instant::now() + wake.fallback();
285 } else {
286 state.checkpoint_control_poll_at =
287 tokio::time::Instant::now() + CheckpointControlWake::capacity_retry();
288 }
289 }
290 #[cfg(not(feature = "cluster"))]
291 if state.checkpoint_control_wake.is_some() {
292 state.checkpoint_control_poll_at = tokio::time::Instant::now()
293 + if control_serviced {
294 CheckpointControlWake::fallback()
295 } else {
296 CheckpointControlWake::capacity_retry()
297 };
298 state.checkpoint_control_pending &= !control_serviced;
299 }
300 }
301 if let Some(reason) = callback.take_pipeline_halt() {
302 self.discard_pending_offsets();
303 tracing::warn!(
304 reason = %reason,
305 "[LDB-3022] checkpoint control observed a permanent pipeline error"
306 );
307 state.halt(reason);
308 return false;
309 }
310 if let Some(reason) = callback.take_pipeline_fault() {
311 self.discard_pending_offsets();
312 tracing::error!(
313 reason = %reason,
314 "[LDB-3024] checkpoint control fault; stopping for recovery"
315 );
316 state.fault = Some(reason);
317 return false;
318 }
319 }
320
321 while let Ok(message) = self.control_rx.try_recv() {
322 #[cfg(feature = "cluster")]
323 if let Err(error) = self.require_process_authority("pipeline control mutation") {
324 state.fault = Some(error.to_string());
325 return false;
326 }
327 callback.apply_control(message);
328 }
329
330 if self.pending_barrier.active
331 && tokio::time::Instant::now()
332 >= self
333 .pending_barrier
334 .attempt_deadline(self.config.checkpoint_timeout)
335 {
336 if let Err(error) = self
337 .cancel_pending_barrier_for_stop(callback, "source barrier alignment timeout", true)
338 .await
339 {
340 state.fault = Some(error);
341 return false;
342 }
343 }
344 true
345 }
346
347 pub(super) async fn run_inner<C: PipelineCallback>(
348 mut self,
349 mut callback: C,
350 ready: Option<crossfire::oneshot::TxOneshot<Result<(), String>>>,
351 ) -> ExitReason {
352 const MAX_DRAIN_PER_CYCLE: usize = 10_000;
354
355 let injectors = self
356 .source_handles
357 .iter()
358 .map(SourceHandle::barrier_control)
359 .collect();
360 callback.set_barrier_injectors(injectors);
361 let cancelled_before_ready = self.terminal_shutdown.is_cancelled();
362 if let Some(ready) = ready {
363 if cancelled_before_ready {
364 ready.send(Err(
365 "pipeline runtime generation was cancelled before readiness".into(),
366 ));
367 } else {
368 ready.send(Ok(()));
369 }
370 }
371 if !cancelled_before_ready && !self.terminal_shutdown.is_cancelled() {
372 for handle in &mut self.source_handles {
375 if let Some(activation) = handle.startup_activation.take() {
376 activation.send(());
377 }
378 }
379 }
380
381 let intake_was_paused = callback.intake_paused();
382 let mut state = CoordinatorRunState {
383 batch_window: self.config.batch_window,
384 checkpoint_control_wake: callback.checkpoint_control_wake(),
385 shuffle_work_wake: callback.shuffle_work_wake(),
386 checkpoint_control_poll_at: tokio::time::Instant::now(),
387 checkpoint_control_pending: false,
388 intake_was_paused,
389 barriers: Vec::new(),
390 fault: None,
391 halted: false,
392 halt_reason: None,
393 source_channel_expected: !self.source_names.is_empty(),
394 };
395
396 loop {
397 if self.terminal_shutdown.is_cancelled() {
398 break;
399 }
400 let wait = self.wait_for_cycle(&mut callback, &mut state).await;
401 match wait.action {
402 CoordinatorWaitAction::Cycle => {}
403 CoordinatorWaitAction::Continue => continue,
404 CoordinatorWaitAction::Stop => break,
405 }
406 let CoordinatorWake {
407 message: msg,
408 retrying_replay,
409 checkpoint_control_due,
410 gates,
411 } = wait.wake;
412 let intake_blocked = gates.intake_paused || callback.intake_paused();
417 self.observe_intake_gate_for_checkpoint_cadence(&mut state, intake_blocked);
418 if intake_blocked {
419 if !self
420 .service_paused_intake(&mut callback, &mut state, msg)
421 .await
422 {
423 break;
424 }
425 continue;
426 }
427 if gates.external_commit_backpressured {
428 if !self
429 .service_external_commit_backpressure(
430 &mut callback,
431 &mut state,
432 checkpoint_control_due,
433 )
434 .await
435 {
436 break;
437 }
438 continue;
439 }
440
441 if !self.replay_pending {
442 if let Err(error) = callback.pin_source_frontiers_for_new_cycle() {
443 state.fault = Some(format!(
444 "decision-bound source frontiers could not be pinned before intake: {error}"
445 ));
446 break;
447 }
448 }
449
450 self.source_batches_buf.clear();
451 self.reset_barrier_seen_for_cycle();
452 if !retrying_replay && !self.replay_pending {
453 self.discard_pending_offsets();
454 }
455 state.barriers.clear();
456 let mut cycle_events: u64 = 0;
457 let cycle_start = Instant::now();
458
459 let had_data = msg.is_some();
460 if let Some(first_msg) = msg {
461 if let Err(error) = self.process_msg(
462 first_msg,
463 &mut callback,
464 &mut state.barriers,
465 &mut cycle_events,
466 ) {
467 match error {
468 CycleError::Halt(reason) => {
469 tracing::warn!(%reason, "[LDB-3022] source staging halted");
470 state.halt(reason);
471 }
472 CycleError::Fatal(reason) | CycleError::Recovery(reason) => {
473 state.fault = Some(reason);
474 }
475 }
476 }
477 }
478 if state.halted || state.fault.is_some() {
479 self.discard_pending_offsets();
480 break;
481 }
482
483 let mut drain_count = 0;
485 let drain_budget = Duration::from_nanos(self.config.drain_budget_ns);
486 let backpressured = had_data && callback.is_backpressured();
489 if backpressured {
490 tracing::debug!("operator graph backpressured — skipping drain");
491 }
492 while !backpressured
493 && drain_count < MAX_DRAIN_PER_CYCLE
494 && cycle_start.elapsed() < drain_budget
495 {
496 match self.rx.try_recv() {
497 Ok(msg) => {
498 if let Err(error) = self.process_msg(
499 msg,
500 &mut callback,
501 &mut state.barriers,
502 &mut cycle_events,
503 ) {
504 match error {
505 CycleError::Halt(reason) => {
506 tracing::warn!(%reason, "[LDB-3022] source staging halted");
507 state.halt(reason);
508 }
509 CycleError::Fatal(reason) | CycleError::Recovery(reason) => {
510 state.fault = Some(reason);
511 }
512 }
513 break;
514 }
515 drain_count += 1;
516 }
517 Err(_) => break,
518 }
519 }
520 if let Ok(source_fault) = self.source_fault_rx.try_recv() {
521 state.fault = Some(format!(
522 "source '{}' fault: {}",
523 source_fault.source, source_fault.error
524 ));
525 }
526 if state.halted || state.fault.is_some() {
527 self.discard_pending_offsets();
528 break;
529 }
530 #[cfg(feature = "cluster")]
531 if let Err(error) = self.require_process_authority("folding a drained source cycle") {
532 self.discard_pending_offsets();
533 state.fault = Some(error.to_string());
534 break;
535 }
536
537 let staged_source_progress = !self.pending_watermark_batches.is_empty();
538 let watermark_result =
539 self.pending_watermark_batches
540 .drain(..)
541 .try_for_each(|pending| {
542 callback.reconcile_source_input_channels(
543 &pending.source_name,
544 pending.input_channels,
545 )?;
546 callback.extract_watermark(
547 &pending.source_name,
548 &pending.batch,
549 pending.admission_floor,
550 )
551 });
552 if let Err(error) = watermark_result {
553 self.discard_pending_offsets();
554 match error {
555 CycleError::Halt(reason) => {
556 tracing::warn!(%reason, "[LDB-3022] watermark processing halted");
557 state.halt(reason);
558 }
559 CycleError::Fatal(reason) | CycleError::Recovery(reason) => {
560 state.fault = Some(reason);
561 }
562 }
563 break;
564 }
565
566 if !self.replay_pending {
567 callback.tick_idle_watermark();
568 }
569
570 if !self.source_batches_buf.is_empty()
573 || staged_source_progress
574 || self.replay_pending
575 || callback.has_deferred_input()
576 {
577 let wm = callback.current_watermark();
578 #[cfg(feature = "cluster")]
579 if let Err(error) = self.require_process_authority("operator execution") {
580 self.discard_pending_offsets();
581 state.fault = Some(error.to_string());
582 break;
583 }
584 let execute_started = Instant::now();
585 let execute_result = callback.execute_cycle(&self.source_batches_buf, wm).await;
586 let execute_ns =
587 u64::try_from(execute_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
588 match execute_result {
589 Ok(out) => {
590 if out.any_failed && callback.fault_on_cycle_error() {
593 self.discard_pending_offsets();
594 tracing::error!(
595 "[LDB-3021] failure domain faulted; faulting for recovery"
596 );
597 state.fault =
598 Some("isolated domain fault (durable delivery)".to_string());
599 break;
600 }
601 let publication =
602 match self.publish_cycle_outputs(&mut callback, &out).await {
603 Ok(publication) => publication,
604 Err(CycleError::Halt(reason)) => {
605 tracing::warn!(
606 %reason,
607 "[LDB-3022] cycle output publication halted"
608 );
609 state.halt(reason);
610 break;
611 }
612 Err(CycleError::Recovery(reason) | CycleError::Fatal(reason)) => {
613 tracing::error!(
614 error = %reason,
615 "cycle output publication failed; faulting for recovery"
616 );
617 state.fault = Some(reason);
618 break;
619 }
620 };
621 callback.record_cycle_phases(
622 execute_ns,
623 publication.output_store_ns,
624 publication.sink_enqueue_ns,
625 );
626 if out.any_failed {
627 callback.note_cycle_error();
628 tracing::warn!(
629 "[LDB-3020] failure domain dropped (best-effort: continuing)"
630 );
631 }
632 }
633 Err(e) => {
634 self.discard_pending_offsets();
635 match e {
636 CycleError::Recovery(msg) => {
637 tracing::error!(
638 error = %msg,
639 "shared pipeline infrastructure failed; faulting for recovery"
640 );
641 state.fault = Some(msg);
642 break;
643 }
644 CycleError::Halt(msg) => {
646 tracing::warn!(reason = %msg, "[LDB-3022] cycle halted");
647 state.halt(msg);
648 break;
649 }
650 CycleError::Fatal(msg) if callback.fault_on_cycle_error() => {
653 tracing::error!(
654 error = %msg,
655 "[LDB-3021] fatal SQL cycle error; faulting for recovery"
656 );
657 state.fault = Some(msg);
658 break;
659 }
660 CycleError::Fatal(msg) => {
662 callback.note_cycle_error();
663 tracing::warn!(
664 error = %msg,
665 "[LDB-3020] SQL cycle error (best-effort: continuing)"
666 );
667 }
668 }
669 }
670 }
671 let elapsed_ns =
672 u64::try_from(cycle_start.elapsed().as_nanos()).unwrap_or(u64::MAX);
673 callback.record_cycle(cycle_events, 0, elapsed_ns);
674
675 if elapsed_ns >= self.config.cycle_budget_ns {
676 tracing::debug!(
677 elapsed_ms = elapsed_ns / 1_000_000,
678 budget_ms = self.config.cycle_budget_ns / 1_000_000,
679 "cycle budget exceeded — proceeding to background work"
680 );
681 }
682 }
683
684 if !self
685 .service_background_work(&mut callback, &mut state, checkpoint_control_due)
686 .await
687 {
688 break;
689 }
690 }
691
692 self.finish_run(&mut callback, state.fault, state.halted, state.halt_reason)
693 .await
694 }
695}