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