1use super::metrics::{FusedMetricsRun, MetricUnits};
27use super::*;
28use crate::MetricsLevel;
29use crate::stream::async_boundary::{
30 AsyncBoundaryMessage as AsyncLinearMessage, RactorBoundaryCommand, RactorBoundarySourceActor,
31 RactorBoundarySourceState, ractor_boundary_runtime,
32};
33use crate::stream::scalar::{ScalarChunkProcessor, ScalarChunkStep};
34use crate::{Attribute, Fusion};
35use std::time::Instant;
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub(crate) enum ExecutorMode {
55 #[default]
57 Auto,
58 ErasedOnly,
60 TypedOnly,
62}
63
64#[derive(Clone, Debug)]
65struct PendingTierChange {
66 requested_attribute: Option<Attribute>,
67 reason: FusedTierChangeReason,
68 affected_nodes: Vec<String>,
69}
70
71#[derive(Clone, Debug)]
72struct ExecutorSelection {
73 mode: ExecutorMode,
74 pending_tier_changes: Vec<PendingTierChange>,
75 typed_only_nodes: Vec<String>,
76 stage_attributes: Vec<FusedNodeAttributes>,
77}
78
79impl ExecutorSelection {
80 fn affected_nodes(&self) -> Vec<String> {
81 self.stage_attributes
82 .iter()
83 .map(|node| node.effective_name.clone())
84 .collect()
85 }
86
87 fn tier_changes(&self, selected_tier: FusedExecutorTier) -> Vec<FusedTierChange> {
88 self.pending_tier_changes
89 .iter()
90 .map(|change| FusedTierChange {
91 requested_attribute: change.requested_attribute.clone(),
92 reason: change.reason,
93 selected_tier,
94 affected_nodes: change.affected_nodes.clone(),
95 })
96 .collect()
97 }
98
99 fn add_auto_erased_fallback(&self) -> Vec<PendingTierChange> {
100 let mut changes = self.pending_tier_changes.clone();
101 changes.push(PendingTierChange {
102 requested_attribute: None,
103 reason: FusedTierChangeReason::AutoTypedUnsupportedFallback,
104 affected_nodes: self.affected_nodes(),
105 });
106 changes
107 }
108
109 fn plan_report(&self, selected_tier: FusedExecutorTier) -> FusedPlanReport {
110 FusedPlanReport {
111 selected_tier,
112 stage_attributes: self.stage_attributes.clone(),
113 tier_changes: self.tier_changes(selected_tier),
114 }
115 }
116
117 fn erased_plan_report(self) -> FusedPlanReport {
118 let tier_changes = if self.mode == ExecutorMode::Auto {
119 self.add_auto_erased_fallback()
120 } else {
121 self.pending_tier_changes
122 }
123 .into_iter()
124 .map(|change| FusedTierChange {
125 requested_attribute: change.requested_attribute,
126 reason: change.reason,
127 selected_tier: FusedExecutorTier::Erased,
128 affected_nodes: change.affected_nodes,
129 })
130 .collect();
131
132 FusedPlanReport {
133 selected_tier: FusedExecutorTier::Erased,
134 stage_attributes: self.stage_attributes,
135 tier_changes,
136 }
137 }
138}
139
140impl<S: Shape> GraphBlueprint<S> {
141 fn resolve_executor_selection(
142 &self,
143 explicit: ExecutorMode,
144 ) -> StreamResult<ExecutorSelection> {
145 let stage_attributes = self.stage_attribute_reports();
146 let mut typed_only_nodes = Vec::new();
147 let mut erased_only_nodes = Vec::new();
148 for node in &stage_attributes {
149 node.attributes.validate().map_err(|error| {
150 StreamError::GraphValidation(format!(
151 "invalid attributes for node {}: {error}",
152 node.effective_name
153 ))
154 })?;
155 match node.attributes.fusion_hint() {
156 Some(Fusion::TypedOnly) => typed_only_nodes.push(node.effective_name.clone()),
157 Some(Fusion::ErasedOnly) => erased_only_nodes.push(node.effective_name.clone()),
158 Some(Fusion::Auto) | None => {}
159 }
160 }
161
162 if explicit != ExecutorMode::Auto {
163 return Ok(ExecutorSelection {
164 mode: explicit,
165 pending_tier_changes: Vec::new(),
166 typed_only_nodes: Vec::new(),
167 stage_attributes,
168 });
169 }
170
171 if !typed_only_nodes.is_empty() && !erased_only_nodes.is_empty() {
172 return Err(StreamError::GraphValidation(format!(
173 "conflicting Fusion attributes: typed_only node(s) [{}], erased_only node(s) [{}]",
174 typed_only_nodes.join(", "),
175 erased_only_nodes.join(", ")
176 )));
177 }
178
179 if !erased_only_nodes.is_empty() {
180 return Ok(ExecutorSelection {
181 mode: ExecutorMode::ErasedOnly,
182 pending_tier_changes: vec![PendingTierChange {
183 requested_attribute: Some(Attribute::Fusion(Fusion::ErasedOnly)),
184 reason: FusedTierChangeReason::FusionErasedOnlyRequested,
185 affected_nodes: erased_only_nodes,
186 }],
187 typed_only_nodes: Vec::new(),
188 stage_attributes,
189 });
190 }
191
192 if !typed_only_nodes.is_empty() {
193 return Ok(ExecutorSelection {
194 mode: ExecutorMode::TypedOnly,
195 pending_tier_changes: vec![PendingTierChange {
196 requested_attribute: Some(Attribute::Fusion(Fusion::TypedOnly)),
197 reason: FusedTierChangeReason::FusionTypedOnlyConstrained,
198 affected_nodes: typed_only_nodes.clone(),
199 }],
200 typed_only_nodes,
201 stage_attributes,
202 });
203 }
204
205 Ok(ExecutorSelection {
206 mode: ExecutorMode::Auto,
207 pending_tier_changes: Vec::new(),
208 typed_only_nodes: Vec::new(),
209 stage_attributes,
210 })
211 }
212
213 fn typed_unsupported_error(&self, selection: &ExecutorSelection) -> StreamError {
214 let nodes = if selection.typed_only_nodes.is_empty() {
215 selection.affected_nodes()
216 } else {
217 selection.typed_only_nodes.clone()
218 };
219 StreamError::GraphValidation(format!(
220 "typed executor does not support this graph shape; typed-only constraint affected node(s): {}",
221 nodes.join(", ")
222 ))
223 }
224}
225
226impl<In, Out> GraphBlueprint<FlowShape<In, Out>>
227where
228 In: Clone + Send + 'static,
229 Out: Send + 'static,
230{
231 pub fn run_with_input<I>(&self, input: I) -> StreamResult<Vec<Out>>
232 where
233 I: IntoIterator<Item = In>,
234 {
235 Ok(self
236 .run_with_input_report(input, FusedExecutionConfig::default())?
237 .output)
238 }
239
240 pub fn run_with_input_report<I>(
241 &self,
242 input: I,
243 config: FusedExecutionConfig,
244 ) -> StreamResult<FusedExecutionReport<Out>>
245 where
246 I: IntoIterator<Item = In>,
247 {
248 self.run_with_input_report_mode(input, config, ExecutorMode::Auto)
249 }
250
251 pub(crate) fn run_with_input_report_mode<I>(
252 &self,
253 input: I,
254 config: FusedExecutionConfig,
255 mode: ExecutorMode,
256 ) -> StreamResult<FusedExecutionReport<Out>>
257 where
258 I: IntoIterator<Item = In>,
259 {
260 let selection = self.resolve_executor_selection(mode)?;
261 let mut metrics = FusedMetricsRun::new(&selection.stage_attributes);
262 let mode = selection.mode;
263
264 if mode != ExecutorMode::ErasedOnly {
266 if let Some(plan) = try_typed_scalar_plan::<In, Out>(
267 &self.stages,
268 &self.edges,
269 self.shape.inlet().id(),
270 self.shape.outlet().id(),
271 ) {
272 let input = input.into_iter();
273 let mut output = Vec::with_capacity(input.size_hint().0);
274 let mut events = 0usize;
275 let enabled = metrics.enabled();
276 let timing = enabled && metrics.timing_enabled();
277 let started = timing.then(Instant::now);
278 let elements = plan.run(input, config, &mut events, |item| output.push(item))?;
279 let processing_time = started.map(|started| started.elapsed());
280 if enabled {
281 metrics.complete_linear_metrics(elements, processing_time);
282 }
283 return Ok(FusedExecutionReport::new(
284 output,
285 events,
286 0,
287 selection.plan_report(FusedExecutorTier::TypedLinear),
288 )
289 .with_node_metrics(metrics.snapshot()));
290 }
291 let linear_plan = try_typed_flow_plan::<In, Out>(
293 &self.stages,
294 &self.edges,
295 self.shape.inlet().id(),
296 self.shape.outlet().id(),
297 );
298 if let Some(plan) = linear_plan {
299 let input = input.into_iter();
300 let mut output = Vec::with_capacity(input.size_hint().0);
301 let mut events = 0usize;
302 let mut async_boundary_crossings = 0usize;
303 let enabled = metrics.enabled();
304 let timing = enabled && metrics.timing_enabled();
305 let mut elements = 0_u64;
306 let started = timing.then(Instant::now);
307 for item in input {
308 let out = plan.run_item_uninstrumented(
309 item,
310 config,
311 &mut events,
312 &mut async_boundary_crossings,
313 )?;
314 output.push(out);
315 elements += 1;
316 }
317 let processing_time = started.map(|started| started.elapsed());
318 if enabled {
319 metrics.complete_linear_metrics(elements, processing_time);
320 }
321 return Ok(FusedExecutionReport::new(
322 output,
323 events,
324 async_boundary_crossings,
325 selection.plan_report(FusedExecutorTier::TypedLinear),
326 )
327 .with_node_metrics(metrics.snapshot()));
328 }
329
330 let inlet_id = self.shape.inlet().id();
334 let outlet_id = self.shape.outlet().id();
335 let metrics_enabled = metrics.enabled();
336 let metrics_timing = metrics_enabled && metrics.timing_enabled();
337 if let Some(runner) = try_build_typed_acyclic_junction_dispatch::<In, Out>(
338 &self.stages,
339 &self.edges,
340 inlet_id,
341 outlet_id,
342 metrics_enabled,
343 metrics_timing,
344 ) {
345 let mut input_iter = input.into_iter();
346 let output = runner(&mut input_iter, &mut metrics)?;
347 return Ok(FusedExecutionReport::new(
348 output,
349 0,
350 0,
351 selection.plan_report(FusedExecutorTier::TypedAcyclicJunction),
352 )
353 .with_node_metrics(metrics.snapshot()));
354 }
355
356 let ms_plan = try_typed_merge_sequence_plan::<In, Out>(
358 &self.stages,
359 &self.edges,
360 self.shape.inlet().id(),
361 self.shape.outlet().id(),
362 );
363 if let Some(mut plan) = ms_plan {
364 let output = run_typed_merge_sequence(&mut plan, input)?;
365 return Ok(FusedExecutionReport::new(
366 output,
367 0,
368 0,
369 selection.plan_report(FusedExecutorTier::TypedMergeSequence),
370 ));
371 }
372
373 if let Some(runner) = try_build_typed_merge_latest_dispatch::<In, Out>(
387 &self.stages,
388 &self.edges,
389 inlet_id,
390 outlet_id,
391 ) {
392 let mut input_iter = input.into_iter();
393 let output = runner(&mut input_iter)?;
394 return Ok(FusedExecutionReport::new(
395 output,
396 0,
397 0,
398 selection.plan_report(FusedExecutorTier::TypedMergeLatest),
399 ));
400 }
401
402 if let Some(runner) = try_build_typed_cyclic_feedback_dispatch::<In, Out>(
405 &self.stages,
406 &self.edges,
407 inlet_id,
408 outlet_id,
409 ) {
410 let mut input_iter = input.into_iter();
411 let mut report = runner(&mut input_iter, config)?;
412 report.plan = selection.plan_report(FusedExecutorTier::TypedCyclicFeedback);
413 return Ok(report);
414 }
415
416 if mode == ExecutorMode::TypedOnly {
417 return Err(self.typed_unsupported_error(&selection));
418 }
419 }
420
421 let input = input.into_iter();
423 let mut executor = FusedExecutor::new(self, config);
424 let inlet = self.shape.inlet().id();
425 let outlet = self.shape.outlet().id();
426 let mut output = Vec::with_capacity(input.size_hint().0);
427
428 {
429 let mut output_sink = VecOutputSink {
430 output: &mut output,
431 };
432 executor.request(outlet, outlet, &mut output_sink)?;
433 executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
434 for item in input {
435 executor.deliver(inlet, datum(item), outlet, &mut output_sink)?;
436 executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
437 }
438 executor.complete(inlet, outlet, &mut output_sink)?;
439 executor.drain_timer_events_until_idle(outlet, &mut output_sink)?;
440 }
441
442 Ok(FusedExecutionReport::new(
443 output,
444 executor.events,
445 executor.async_boundary_crossings,
446 selection.erased_plan_report(),
447 )
448 .with_node_metrics(executor.metrics.snapshot()))
449 }
450
451 pub fn run_count_with_input<I>(&self, input: I) -> StreamResult<usize>
452 where
453 I: IntoIterator<Item = In>,
454 {
455 Ok(self
456 .run_count_with_input_report(input, FusedExecutionConfig::default())?
457 .result)
458 }
459
460 pub fn run_count_with_input_report<I>(
461 &self,
462 input: I,
463 config: FusedExecutionConfig,
464 ) -> StreamResult<FusedTerminalReport<usize>>
465 where
466 I: IntoIterator<Item = In>,
467 {
468 self.run_count_with_input_report_mode(input, config, ExecutorMode::Auto)
469 }
470
471 pub(crate) fn run_count_with_input_report_mode<I>(
472 &self,
473 input: I,
474 config: FusedExecutionConfig,
475 mode: ExecutorMode,
476 ) -> StreamResult<FusedTerminalReport<usize>>
477 where
478 I: IntoIterator<Item = In>,
479 {
480 let selection = self.resolve_executor_selection(mode)?;
481 let mut metrics = FusedMetricsRun::new(&selection.stage_attributes);
482 let mode = selection.mode;
483
484 if mode != ExecutorMode::ErasedOnly {
486 if let Some(plan) = try_typed_scalar_plan::<In, Out>(
487 &self.stages,
488 &self.edges,
489 self.shape.inlet().id(),
490 self.shape.outlet().id(),
491 ) {
492 let mut count = 0usize;
493 let mut events = 0usize;
494 let enabled = metrics.enabled();
495 let timing = enabled && metrics.timing_enabled();
496 let started = timing.then(Instant::now);
497 let elements = plan.run(input, config, &mut events, |_| count += 1)?;
498 let processing_time = started.map(|started| started.elapsed());
499 if enabled {
500 metrics.complete_linear_metrics(elements, processing_time);
501 }
502 return Ok(FusedTerminalReport::new(
503 count,
504 events,
505 0,
506 selection.plan_report(FusedExecutorTier::TypedLinear),
507 )
508 .with_node_metrics(metrics.snapshot()));
509 }
510 let plan = try_typed_flow_plan::<In, Out>(
511 &self.stages,
512 &self.edges,
513 self.shape.inlet().id(),
514 self.shape.outlet().id(),
515 );
516 if let Some(plan) = plan {
517 let input = input.into_iter();
518 let mut count = 0usize;
519 let mut events = 0usize;
520 let mut async_boundary_crossings = 0usize;
521 let enabled = metrics.enabled();
522 let timing = enabled && metrics.timing_enabled();
523 let started = timing.then(Instant::now);
524 for item in input {
525 plan.run_item_count_uninstrumented(
526 item,
527 config,
528 &mut events,
529 &mut async_boundary_crossings,
530 )?;
531 count += 1;
532 }
533 let processing_time = started.map(|started| started.elapsed());
534 if enabled {
535 metrics.complete_linear_metrics(count as u64, processing_time);
536 }
537 return Ok(FusedTerminalReport::new(
538 count,
539 events,
540 async_boundary_crossings,
541 selection.plan_report(FusedExecutorTier::TypedLinear),
542 )
543 .with_node_metrics(metrics.snapshot()));
544 } else if mode == ExecutorMode::TypedOnly {
545 return Err(self.typed_unsupported_error(&selection));
546 }
547 }
548
549 let mut executor = FusedExecutor::new(self, config);
551 let inlet = self.shape.inlet().id();
552 let outlet = self.shape.outlet().id();
553 let mut output_sink = CountOutputSink { count: 0 };
554
555 executor.request::<Out>(outlet, outlet, &mut output_sink)?;
556 executor.drain_timer_events_nonblocking::<Out>(outlet, &mut output_sink)?;
557 for item in input {
558 executor.deliver::<Out>(inlet, datum(item), outlet, &mut output_sink)?;
559 executor.drain_timer_events_nonblocking::<Out>(outlet, &mut output_sink)?;
560 }
561 executor.complete::<Out>(inlet, outlet, &mut output_sink)?;
562 executor.drain_timer_events_until_idle::<Out>(outlet, &mut output_sink)?;
563
564 Ok(FusedTerminalReport::new(
565 output_sink.count,
566 executor.events,
567 executor.async_boundary_crossings,
568 selection.erased_plan_report(),
569 )
570 .with_node_metrics(executor.metrics.snapshot()))
571 }
572
573 pub fn run_fold_with_input<I, Acc, F>(&self, input: I, zero: Acc, fold: F) -> StreamResult<Acc>
574 where
575 I: IntoIterator<Item = In>,
576 F: FnMut(Acc, Out) -> Acc,
577 {
578 Ok(self
579 .run_fold_with_input_report(input, zero, fold, FusedExecutionConfig::default())?
580 .result)
581 }
582
583 pub fn run_fold_with_input_report<I, Acc, F>(
584 &self,
585 input: I,
586 zero: Acc,
587 fold: F,
588 config: FusedExecutionConfig,
589 ) -> StreamResult<FusedTerminalReport<Acc>>
590 where
591 I: IntoIterator<Item = In>,
592 F: FnMut(Acc, Out) -> Acc,
593 {
594 self.run_fold_with_input_report_mode(input, zero, fold, config, ExecutorMode::Auto)
595 }
596
597 pub(crate) fn run_fold_with_input_report_mode<I, Acc, F>(
598 &self,
599 input: I,
600 zero: Acc,
601 mut fold: F,
602 config: FusedExecutionConfig,
603 mode: ExecutorMode,
604 ) -> StreamResult<FusedTerminalReport<Acc>>
605 where
606 I: IntoIterator<Item = In>,
607 F: FnMut(Acc, Out) -> Acc,
608 {
609 let selection = self.resolve_executor_selection(mode)?;
610 let mut metrics = FusedMetricsRun::new(&selection.stage_attributes);
611 let mode = selection.mode;
612
613 if mode != ExecutorMode::ErasedOnly {
615 if let Some(plan) = try_typed_scalar_plan::<In, Out>(
616 &self.stages,
617 &self.edges,
618 self.shape.inlet().id(),
619 self.shape.outlet().id(),
620 ) {
621 let mut accumulator = Some(zero);
622 let mut events = 0usize;
623 let enabled = metrics.enabled();
624 let timing = enabled && metrics.timing_enabled();
625 let started = timing.then(Instant::now);
626 let elements = plan.run(input, config, &mut events, |item| {
627 let current = accumulator
628 .take()
629 .expect("typed scalar fold accumulator is present");
630 accumulator = Some(fold(current, item));
631 })?;
632 let processing_time = started.map(|started| started.elapsed());
633 if enabled {
634 metrics.complete_linear_metrics(elements, processing_time);
635 }
636 return Ok(FusedTerminalReport::new(
637 accumulator.expect("typed scalar fold accumulator is present"),
638 events,
639 0,
640 selection.plan_report(FusedExecutorTier::TypedLinear),
641 )
642 .with_node_metrics(metrics.snapshot()));
643 }
644 let plan = try_typed_flow_plan::<In, Out>(
645 &self.stages,
646 &self.edges,
647 self.shape.inlet().id(),
648 self.shape.outlet().id(),
649 );
650 if let Some(plan) = plan {
651 let input = input.into_iter();
652 let mut accumulator = zero;
653 let mut events = 0usize;
654 let mut async_boundary_crossings = 0usize;
655 let enabled = metrics.enabled();
656 let timing = enabled && metrics.timing_enabled();
657 let mut elements = 0_u64;
658 let started = timing.then(Instant::now);
659 for item in input {
660 let out = plan.run_item_uninstrumented(
661 item,
662 config,
663 &mut events,
664 &mut async_boundary_crossings,
665 )?;
666 accumulator = fold(accumulator, out);
667 elements += 1;
668 }
669 let processing_time = started.map(|started| started.elapsed());
670 if enabled {
671 metrics.complete_linear_metrics(elements, processing_time);
672 }
673 return Ok(FusedTerminalReport::new(
674 accumulator,
675 events,
676 async_boundary_crossings,
677 selection.plan_report(FusedExecutorTier::TypedLinear),
678 )
679 .with_node_metrics(metrics.snapshot()));
680 } else if mode == ExecutorMode::TypedOnly {
681 return Err(self.typed_unsupported_error(&selection));
682 }
683 }
684
685 let mut executor = FusedExecutor::new(self, config);
687 let inlet = self.shape.inlet().id();
688 let outlet = self.shape.outlet().id();
689 let mut output_sink = FoldOutputSink {
690 accumulator: Some(zero),
691 fold,
692 };
693
694 executor.request(outlet, outlet, &mut output_sink)?;
695 executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
696 for item in input {
697 executor.deliver(inlet, datum(item), outlet, &mut output_sink)?;
698 executor.drain_timer_events_nonblocking(outlet, &mut output_sink)?;
699 }
700 executor.complete(inlet, outlet, &mut output_sink)?;
701 executor.drain_timer_events_until_idle(outlet, &mut output_sink)?;
702
703 Ok(FusedTerminalReport::new(
704 output_sink.finish(),
705 executor.events,
706 executor.async_boundary_crossings,
707 selection.erased_plan_report(),
708 )
709 .with_node_metrics(executor.metrics.snapshot()))
710 }
711
712 #[cfg_attr(not(test), allow(dead_code))]
722 pub(crate) fn run_with_input_mode<I>(
723 &self,
724 input: I,
725 mode: ExecutorMode,
726 ) -> StreamResult<Vec<Out>>
727 where
728 I: IntoIterator<Item = In>,
729 {
730 Ok(self
731 .run_with_input_report_mode(input, FusedExecutionConfig::default(), mode)?
732 .output)
733 }
734
735 #[allow(dead_code)]
739 pub(crate) fn run_count_with_input_mode<I>(
740 &self,
741 input: I,
742 mode: ExecutorMode,
743 ) -> StreamResult<usize>
744 where
745 I: IntoIterator<Item = In>,
746 {
747 Ok(self
748 .run_count_with_input_report_mode(input, FusedExecutionConfig::default(), mode)?
749 .result)
750 }
751
752 #[allow(dead_code)]
756 pub(crate) fn run_fold_with_input_mode<I, Acc, F>(
757 &self,
758 input: I,
759 zero: Acc,
760 fold: F,
761 mode: ExecutorMode,
762 ) -> StreamResult<Acc>
763 where
764 I: IntoIterator<Item = In>,
765 F: FnMut(Acc, Out) -> Acc,
766 {
767 Ok(self
768 .run_fold_with_input_report_mode(
769 input,
770 zero,
771 fold,
772 FusedExecutionConfig::default(),
773 mode,
774 )?
775 .result)
776 }
777}
778
779impl<T> GraphBlueprint<FlowShape<T, T>>
780where
781 T: Send + 'static,
782{
783 pub fn run_typed_linear_with_input<I>(&self, input: I) -> StreamResult<Vec<T>>
784 where
785 I: IntoIterator<Item = T>,
786 {
787 Ok(self
788 .run_typed_linear_with_input_report(input, FusedExecutionConfig::default())?
789 .output)
790 }
791
792 pub fn run_typed_linear_with_input_report<I>(
793 &self,
794 input: I,
795 config: FusedExecutionConfig,
796 ) -> StreamResult<FusedExecutionReport<T>>
797 where
798 I: IntoIterator<Item = T>,
799 {
800 let input = input.into_iter();
801 let mut metrics = self.new_explicit_metrics_run();
802 if let Some(plan) = try_typed_scalar_plan::<T, T>(
803 &self.stages,
804 &self.edges,
805 self.shape.inlet().id(),
806 self.shape.outlet().id(),
807 ) {
808 let mut output = Vec::with_capacity(input.size_hint().0);
809 let mut events = 0;
810 let enabled = metrics.enabled();
811 let timing = enabled && metrics.timing_enabled();
812 let started = timing.then(Instant::now);
813 let elements = plan.run(input, config, &mut events, |item| output.push(item))?;
814 let processing_time = started.map(|started| started.elapsed());
815 if enabled {
816 metrics.complete_linear_metrics(elements, processing_time);
817 }
818 return Ok(FusedExecutionReport::new(
819 output,
820 events,
821 0,
822 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
823 )
824 .with_node_metrics(metrics.snapshot()));
825 }
826 let plan = self.typed_linear_plan()?;
827 let mut output = Vec::with_capacity(input.size_hint().0);
828 let mut events = 0;
829 let mut async_boundary_crossings = 0;
830 let enabled = metrics.enabled();
831 let timing = enabled && metrics.timing_enabled();
832 let mut elements = 0_u64;
833 let started = timing.then(Instant::now);
834 for item in input {
835 let item = plan.run_item_uninstrumented(
836 item,
837 config,
838 &mut events,
839 &mut async_boundary_crossings,
840 )?;
841 output.push(item);
842 elements += 1;
843 }
844 let processing_time = started.map(|started| started.elapsed());
845 if enabled {
846 metrics.complete_linear_metrics(elements, processing_time);
847 }
848
849 Ok(FusedExecutionReport::new(
850 output,
851 events,
852 async_boundary_crossings,
853 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
854 )
855 .with_node_metrics(metrics.snapshot()))
856 }
857
858 pub fn run_typed_linear_count_with_input<I>(&self, input: I) -> StreamResult<usize>
859 where
860 I: IntoIterator<Item = T>,
861 {
862 Ok(self
863 .run_typed_linear_count_with_input_report(input, FusedExecutionConfig::default())?
864 .result)
865 }
866
867 pub fn run_typed_linear_count_with_input_report<I>(
868 &self,
869 input: I,
870 config: FusedExecutionConfig,
871 ) -> StreamResult<FusedTerminalReport<usize>>
872 where
873 I: IntoIterator<Item = T>,
874 {
875 let mut metrics = self.new_explicit_metrics_run();
876 let input = input.into_iter();
877 if let Some(plan) = try_typed_scalar_plan::<T, T>(
878 &self.stages,
879 &self.edges,
880 self.shape.inlet().id(),
881 self.shape.outlet().id(),
882 ) {
883 let mut count = 0;
884 let mut events = 0;
885 let enabled = metrics.enabled();
886 let timing = enabled && metrics.timing_enabled();
887 let started = timing.then(Instant::now);
888 let elements = plan.run(input, config, &mut events, |_| count += 1)?;
889 let processing_time = started.map(|started| started.elapsed());
890 if enabled {
891 metrics.complete_linear_metrics(elements, processing_time);
892 }
893 return Ok(FusedTerminalReport::new(
894 count,
895 events,
896 0,
897 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
898 )
899 .with_node_metrics(metrics.snapshot()));
900 }
901 let plan = self.typed_linear_plan()?;
902 let mut count = 0;
903 let mut events = 0;
904 let mut async_boundary_crossings = 0;
905 let enabled = metrics.enabled();
906 let timing = enabled && metrics.timing_enabled();
907 let started = timing.then(Instant::now);
908 for item in input {
909 drop(plan.run_item_uninstrumented(
910 item,
911 config,
912 &mut events,
913 &mut async_boundary_crossings,
914 )?);
915 count += 1;
916 }
917 let processing_time = started.map(|started| started.elapsed());
918 if enabled {
919 metrics.complete_linear_metrics(count as u64, processing_time);
920 }
921
922 Ok(FusedTerminalReport::new(
923 count,
924 events,
925 async_boundary_crossings,
926 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
927 )
928 .with_node_metrics(metrics.snapshot()))
929 }
930
931 pub fn run_typed_linear_fold_with_input<I, Acc, F>(
932 &self,
933 input: I,
934 zero: Acc,
935 fold: F,
936 ) -> StreamResult<Acc>
937 where
938 I: IntoIterator<Item = T>,
939 F: FnMut(Acc, T) -> Acc,
940 {
941 Ok(self
942 .run_typed_linear_fold_with_input_report(
943 input,
944 zero,
945 fold,
946 FusedExecutionConfig::default(),
947 )?
948 .result)
949 }
950
951 pub fn run_typed_linear_fold_with_input_report<I, Acc, F>(
952 &self,
953 input: I,
954 zero: Acc,
955 mut fold: F,
956 config: FusedExecutionConfig,
957 ) -> StreamResult<FusedTerminalReport<Acc>>
958 where
959 I: IntoIterator<Item = T>,
960 F: FnMut(Acc, T) -> Acc,
961 {
962 let mut metrics = self.new_explicit_metrics_run();
963 let input = input.into_iter();
964 if let Some(plan) = try_typed_scalar_plan::<T, T>(
965 &self.stages,
966 &self.edges,
967 self.shape.inlet().id(),
968 self.shape.outlet().id(),
969 ) {
970 let mut accumulator = Some(zero);
971 let mut events = 0;
972 let enabled = metrics.enabled();
973 let timing = enabled && metrics.timing_enabled();
974 let started = timing.then(Instant::now);
975 let elements = plan.run(input, config, &mut events, |item| {
976 let current = accumulator
977 .take()
978 .expect("typed scalar fold accumulator is present");
979 accumulator = Some(fold(current, item));
980 })?;
981 let processing_time = started.map(|started| started.elapsed());
982 if enabled {
983 metrics.complete_linear_metrics(elements, processing_time);
984 }
985 return Ok(FusedTerminalReport::new(
986 accumulator.expect("typed scalar fold accumulator is present"),
987 events,
988 0,
989 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
990 )
991 .with_node_metrics(metrics.snapshot()));
992 }
993 let plan = self.typed_linear_plan()?;
994 let mut accumulator = zero;
995 let mut events = 0;
996 let mut async_boundary_crossings = 0;
997 let enabled = metrics.enabled();
998 let timing = enabled && metrics.timing_enabled();
999 let mut elements = 0_u64;
1000 let started = timing.then(Instant::now);
1001 for item in input {
1002 let item = plan.run_item_uninstrumented(
1003 item,
1004 config,
1005 &mut events,
1006 &mut async_boundary_crossings,
1007 )?;
1008 accumulator = fold(accumulator, item);
1009 elements += 1;
1010 }
1011 let processing_time = started.map(|started| started.elapsed());
1012 if enabled {
1013 metrics.complete_linear_metrics(elements, processing_time);
1014 }
1015
1016 Ok(FusedTerminalReport::new(
1017 accumulator,
1018 events,
1019 async_boundary_crossings,
1020 self.plan_report(FusedExecutorTier::TypedLinear, Vec::new()),
1021 )
1022 .with_node_metrics(metrics.snapshot()))
1023 }
1024
1025 pub fn run_async_boundary_count_with_input_report<I>(
1032 &self,
1033 input: I,
1034 config: AsyncBoundaryExecutionConfig,
1035 ) -> StreamResult<FusedTerminalReport<usize>>
1036 where
1037 I: IntoIterator<Item = T> + Send,
1038 I::IntoIter: Send + 'static,
1039 {
1040 let segments = self.typed_linear_async_segments()?;
1041 let mut report = BoundaryCountExecutor::Ractor.run_count(input, segments, config)?;
1042 report.plan = self.plan_report(FusedExecutorTier::AsyncBoundary, Vec::new());
1043 Ok(report)
1044 }
1045
1046 fn new_explicit_metrics_run(&self) -> FusedMetricsRun {
1047 let graph_level = self.attributes.metrics_level_hint();
1048 let enabled = self.stages.iter().any(|stage| {
1049 stage
1050 .spec
1051 .attributes()
1052 .metrics_level_hint()
1053 .or(graph_level)
1054 .unwrap_or(MetricsLevel::Off)
1055 != MetricsLevel::Off
1056 });
1057 if enabled {
1058 FusedMetricsRun::new(&self.stage_attribute_reports())
1059 } else {
1060 FusedMetricsRun::disabled()
1061 }
1062 }
1063
1064 fn typed_linear_plan(&self) -> StreamResult<TypedLinearPlan<T>> {
1065 let graph_inlet = self.shape.inlet().id();
1066 let graph_outlet = self.shape.outlet().id();
1067 let type_id = TypeId::of::<T>();
1068 let mut current_inlet = graph_inlet;
1069 let mut seen = HashSet::new();
1070 let mut steps = Vec::new();
1071
1072 loop {
1073 let stage_index = self
1074 .stages
1075 .iter()
1076 .position(|stage| {
1077 stage
1078 .spec
1079 .inlets
1080 .iter()
1081 .any(|inlet| inlet.id() == current_inlet)
1082 })
1083 .ok_or_else(|| {
1084 StreamError::GraphValidation(format!(
1085 "typed linear fast path could not find inlet {}",
1086 current_inlet.as_usize()
1087 ))
1088 })?;
1089 if !seen.insert(stage_index) {
1090 return Err(StreamError::GraphValidation(
1091 "typed linear fast path does not support cycles".into(),
1092 ));
1093 }
1094
1095 let stage = &self.stages[stage_index];
1096 if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
1097 return Err(StreamError::GraphValidation(format!(
1098 "typed linear fast path requires single-inlet single-outlet stages; {} has {} inlet(s) and {} outlet(s)",
1099 stage.spec.name(),
1100 stage.spec.inlets.len(),
1101 stage.spec.outlets.len()
1102 )));
1103 }
1104 let inlet = &stage.spec.inlets[0];
1105 let outlet = &stage.spec.outlets[0];
1106 if inlet.type_id() != type_id || outlet.type_id() != type_id {
1107 return Err(StreamError::GraphValidation(format!(
1108 "typed linear fast path requires every port to use {}",
1109 type_name::<T>()
1110 )));
1111 }
1112
1113 let step = match &stage.spec.kind {
1114 StageKind::Identity | StageKind::Opaque => TypedLinearStep::Pass,
1115 StageKind::AsyncBoundary => TypedLinearStep::AsyncBoundary,
1116 StageKind::Map(map) => {
1117 if map.scalar_i64.is_some() {
1118 return Err(StreamError::GraphValidation(
1119 "typed linear item path cannot execute a scalar chunk stage".into(),
1120 ));
1121 }
1122 let mapper = map
1123 .typed
1124 .as_ref()
1125 .downcast_ref::<Arc<dyn Fn(T) -> T + Send + Sync>>()
1126 .ok_or_else(|| {
1127 StreamError::GraphValidation(format!(
1128 "typed linear fast path could not downcast map stage {}",
1129 stage.spec.name()
1130 ))
1131 })?;
1132 TypedLinearStep::Map(Arc::clone(mapper))
1133 }
1134 _ => {
1135 return Err(StreamError::GraphValidation(format!(
1136 "typed linear fast path does not support {}",
1137 stage.spec.name()
1138 )));
1139 }
1140 };
1141 steps.push(step);
1142 if outlet.id() == graph_outlet {
1143 break;
1144 }
1145 current_inlet = self
1146 .edges
1147 .iter()
1148 .find_map(|edge| (edge.outlet == outlet.id()).then_some(edge.inlet))
1149 .ok_or_else(|| {
1150 StreamError::GraphValidation(format!(
1151 "typed linear fast path could not follow outlet {}",
1152 outlet.id().as_usize()
1153 ))
1154 })?;
1155 }
1156
1157 if seen.len() != self.stages.len() {
1158 return Err(StreamError::GraphValidation(
1159 "typed linear fast path requires all stages to be on the result path".into(),
1160 ));
1161 }
1162
1163 Ok(TypedLinearPlan { steps })
1164 }
1165
1166 pub(super) fn typed_linear_async_segments(&self) -> StreamResult<TypedLinearSegments<T>> {
1167 let plan = self.typed_linear_plan()?;
1168 let mut segments = Vec::new();
1169 let mut current = Vec::new();
1170
1171 for step in plan.steps {
1172 match step {
1173 TypedLinearStep::AsyncBoundary => {
1174 segments.push(current);
1175 current = Vec::new();
1176 }
1177 step => current.push(step),
1178 }
1179 }
1180 segments.push(current);
1181
1182 if segments.len() == 1 {
1183 return Err(StreamError::GraphValidation(
1184 "async boundary execution requires at least one AsyncBoundary stage".into(),
1185 ));
1186 }
1187
1188 Ok(TypedLinearSegments { segments })
1189 }
1190}
1191
1192pub(super) struct TypedLinearPlan<T> {
1193 steps: Vec<TypedLinearStep<T>>,
1194}
1195
1196pub(super) struct TypedLinearSegments<T> {
1197 segments: Vec<Vec<TypedLinearStep<T>>>,
1198}
1199
1200pub(super) enum TypedLinearStep<T> {
1201 Pass,
1202 Map(Arc<dyn Fn(T) -> T + Send + Sync>),
1203 AsyncBoundary,
1204}
1205
1206impl<T> Clone for TypedLinearStep<T> {
1207 fn clone(&self) -> Self {
1208 match self {
1209 Self::Pass => Self::Pass,
1210 Self::Map(mapper) => Self::Map(Arc::clone(mapper)),
1211 Self::AsyncBoundary => Self::AsyncBoundary,
1212 }
1213 }
1214}
1215
1216impl<T> TypedLinearPlan<T> {
1217 fn run_item_uninstrumented(
1218 &self,
1219 mut item: T,
1220 config: FusedExecutionConfig,
1221 events: &mut usize,
1222 async_boundary_crossings: &mut usize,
1223 ) -> StreamResult<T>
1224 where
1225 T: Send + 'static,
1226 {
1227 for step in &self.steps {
1228 bump_fused_event(events, config)?;
1229 match step {
1230 TypedLinearStep::Pass => {}
1231 TypedLinearStep::Map(mapper) => item = mapper(item),
1232 TypedLinearStep::AsyncBoundary => *async_boundary_crossings += 1,
1233 }
1234 bump_fused_event(events, config)?;
1235 }
1236 Ok(item)
1237 }
1238}
1239
1240#[allow(dead_code)]
1251pub(crate) struct TypedSlot<T>(Option<T>);
1252
1253#[allow(dead_code)]
1254impl<T> TypedSlot<T> {
1255 pub(crate) fn empty() -> Self {
1256 Self(None)
1257 }
1258
1259 pub(crate) fn put(&mut self, value: T) {
1260 self.0 = Some(value);
1261 }
1262
1263 pub(crate) fn take(&mut self) -> Option<T> {
1264 self.0.take()
1265 }
1266
1267 pub(crate) fn is_some(&self) -> bool {
1268 self.0.is_some()
1269 }
1270}
1271
1272#[allow(dead_code)]
1278pub(crate) struct TypedPortRegistry {
1279 slots: HashMap<PortId, Box<dyn Any + Send>>,
1280}
1281
1282#[allow(dead_code)]
1283impl TypedPortRegistry {
1284 pub(crate) fn new() -> Self {
1285 Self {
1286 slots: HashMap::new(),
1287 }
1288 }
1289
1290 pub(crate) fn register<T: Any + Send>(&mut self, port_id: PortId) {
1293 let prev = self
1294 .slots
1295 .insert(port_id, Box::new(TypedSlot::<T>::empty()));
1296 assert!(prev.is_none(), "port {port_id:?} registered twice");
1297 }
1298
1299 pub(crate) fn get_mut<T: Any + Send>(&mut self, port_id: PortId) -> Option<&mut TypedSlot<T>> {
1304 self.slots.get_mut(&port_id)?.downcast_mut::<TypedSlot<T>>()
1305 }
1306}
1307
1308#[allow(dead_code)]
1316pub(crate) trait TypedKernel<In, Out>: Send + Sync {
1317 fn run(&self, input: In) -> Out;
1318}
1319
1320#[allow(dead_code)]
1328pub(crate) trait TypedStageFactory<In, Out>: Send + Sync {
1329 fn try_build(&self, spec: &StageSpec) -> Option<Box<dyn TypedKernel<In, Out>>>;
1330}
1331
1332struct TypedScalarPlan<In: 'static, Out: 'static> {
1337 steps: Vec<ScalarChunkStep<In>>,
1338 finish: Arc<dyn Fn(In) -> Out + Send + Sync>,
1339 stage_count: usize,
1340}
1341
1342impl<In, Out> TypedScalarPlan<In, Out>
1343where
1344 In: Send + 'static,
1345 Out: Send + 'static,
1346{
1347 fn run<I, F>(
1348 &self,
1349 input: I,
1350 config: FusedExecutionConfig,
1351 events: &mut usize,
1352 mut consume: F,
1353 ) -> StreamResult<u64>
1354 where
1355 I: IntoIterator<Item = In>,
1356 F: FnMut(Out),
1357 {
1358 let mut input = input.into_iter();
1359 if !self.steps.iter().any(ScalarChunkStep::is_add_or_subtract) {
1360 let mut outputs = 0_u64;
1361 for item in input {
1362 let mut value = Some(item);
1363 for step in &self.steps {
1364 let Some(current) = value else {
1365 break;
1366 };
1367 value = step.apply_checked(current)?;
1368 }
1369 if let Some(value) = value {
1370 consume((self.finish)(value));
1371 outputs += 1;
1372 }
1373 add_typed_helper_events(events, config, self.stage_count * 2)?;
1374 }
1375 return Ok(outputs);
1376 }
1377 let mut processor = ScalarChunkProcessor::new();
1378 let mut outputs = 0_u64;
1379 loop {
1380 let loaded = processor.load_from(&mut input);
1381 if loaded == 0 {
1382 break;
1383 }
1384 let result = processor.process(&self.steps);
1385 for value in processor.drain_output() {
1386 consume((self.finish)(value));
1387 outputs += 1;
1388 }
1389 if let Some(error) = result.failure {
1390 return Err(error);
1391 }
1392 let chunk_events = loaded
1393 .checked_mul(self.stage_count)
1394 .and_then(|count| count.checked_mul(2))
1395 .ok_or_else(|| StreamError::Failed("typed scalar event count overflow".into()))?;
1396 add_typed_helper_events(events, config, chunk_events)?;
1397 }
1398 Ok(outputs)
1399 }
1400}
1401
1402fn try_typed_scalar_plan<In, Out>(
1403 stages: &[super::builder::StageRecord],
1404 edges: &[super::builder::Edge],
1405 graph_inlet: PortId,
1406 graph_outlet: PortId,
1407) -> Option<TypedScalarPlan<In, Out>>
1408where
1409 In: Send + 'static,
1410 Out: Send + 'static,
1411{
1412 if !stages.iter().any(|stage| {
1413 matches!(
1414 &stage.spec.kind,
1415 StageKind::Map(map) if map.scalar_i64.is_some()
1416 )
1417 }) {
1418 return None;
1419 }
1420 let in_type = TypeId::of::<In>();
1421 let out_type = TypeId::of::<Out>();
1422 let mut current_inlet = graph_inlet;
1423 let mut seen = HashSet::new();
1424 let mut steps = Vec::new();
1425 let mut finish = None;
1426
1427 loop {
1428 let stage_index = stages.iter().position(|stage| {
1429 stage
1430 .spec
1431 .inlets
1432 .iter()
1433 .any(|inlet| inlet.id() == current_inlet)
1434 })?;
1435 if !seen.insert(stage_index) {
1436 return None;
1437 }
1438 let stage = &stages[stage_index];
1439 if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
1440 return None;
1441 }
1442 let inlet = &stage.spec.inlets[0];
1443 let outlet = &stage.spec.outlets[0];
1444 if inlet.type_id() != in_type || outlet.type_id() != in_type {
1445 return None;
1446 }
1447 match &stage.spec.kind {
1448 StageKind::Identity => {}
1449 StageKind::Map(map) => {
1450 let scalar = map
1451 .scalar_i64
1452 .as_ref()?
1453 .downcast_ref::<ScalarChunkStep<In>>()?;
1454 steps.push(scalar.clone());
1455 if outlet.id() == graph_outlet {
1456 finish = Some(
1457 map.scalar_finish
1458 .as_ref()?
1459 .downcast_ref::<Arc<dyn Fn(In) -> Out + Send + Sync>>()?
1460 .clone(),
1461 );
1462 }
1463 }
1464 _ => return None,
1465 }
1466 if outlet.id() == graph_outlet {
1467 if outlet.type_id() != out_type || steps.is_empty() {
1468 return None;
1469 }
1470 break;
1471 }
1472 current_inlet = edges
1473 .iter()
1474 .find_map(|edge| (edge.outlet == outlet.id()).then_some(edge.inlet))?;
1475 }
1476 if seen.len() != stages.len() {
1477 return None;
1478 }
1479 Some(TypedScalarPlan {
1480 steps,
1481 finish: finish?,
1482 stage_count: seen.len(),
1483 })
1484}
1485
1486enum TypedMiddleStep<T: 'static> {
1496 Pass,
1498 Map(Arc<dyn Fn(T) -> T + Send + Sync>),
1500 AsyncBoundary,
1502}
1503
1504enum TypedLastStep<In: 'static, Out: 'static> {
1517 Map(Arc<dyn Fn(In) -> Out + Send + Sync>),
1519 Identity(Arc<dyn Fn(In) -> Out + Send + Sync>),
1523}
1524
1525pub(crate) struct TypedFlowPlan<In: 'static, Out: 'static> {
1536 middle_steps: Vec<TypedMiddleStep<In>>,
1538 last_step: TypedLastStep<In, Out>,
1540 #[allow(dead_code)]
1543 stage_count: usize,
1544}
1545
1546impl<In: Send + 'static, Out: Send + 'static> TypedFlowPlan<In, Out> {
1547 fn run_item_uninstrumented(
1548 &self,
1549 item: In,
1550 config: FusedExecutionConfig,
1551 events: &mut usize,
1552 async_boundary_crossings: &mut usize,
1553 ) -> StreamResult<Out> {
1554 let mut val = item;
1555 for step in &self.middle_steps {
1556 bump_fused_event(events, config)?;
1557 val = match step {
1558 TypedMiddleStep::Pass => val,
1559 TypedMiddleStep::Map(f) => f(val),
1560 TypedMiddleStep::AsyncBoundary => {
1561 *async_boundary_crossings += 1;
1562 val
1563 }
1564 };
1565 bump_fused_event(events, config)?;
1566 }
1567 bump_fused_event(events, config)?;
1568 let out = match &self.last_step {
1569 TypedLastStep::Map(f) => f(val),
1570 TypedLastStep::Identity(f) => f(val),
1571 };
1572 bump_fused_event(events, config)?;
1573 Ok(out)
1574 }
1575
1576 fn run_item_count_uninstrumented(
1577 &self,
1578 item: In,
1579 config: FusedExecutionConfig,
1580 events: &mut usize,
1581 async_boundary_crossings: &mut usize,
1582 ) -> StreamResult<()> {
1583 let mut val = item;
1584 for step in &self.middle_steps {
1585 bump_fused_event(events, config)?;
1586 val = match step {
1587 TypedMiddleStep::Pass => val,
1588 TypedMiddleStep::Map(f) => f(val),
1589 TypedMiddleStep::AsyncBoundary => {
1590 *async_boundary_crossings += 1;
1591 val
1592 }
1593 };
1594 bump_fused_event(events, config)?;
1595 }
1596 bump_fused_event(events, config)?;
1597 match &self.last_step {
1598 TypedLastStep::Map(f) => drop(f(val)),
1599 TypedLastStep::Identity(_) => drop(val),
1600 }
1601 bump_fused_event(events, config)?;
1602 Ok(())
1603 }
1604}
1605
1606pub(crate) fn try_typed_flow_plan<In, Out>(
1620 stages: &[super::builder::StageRecord],
1621 edges: &[super::builder::Edge],
1622 graph_inlet: PortId,
1623 graph_outlet: PortId,
1624) -> Option<TypedFlowPlan<In, Out>>
1625where
1626 In: Clone + Send + 'static,
1627 Out: Send + 'static,
1628{
1629 let in_type_id = TypeId::of::<In>();
1630 let out_type_id = TypeId::of::<Out>();
1631
1632 let mut current_inlet = graph_inlet;
1633 let mut seen = HashSet::new();
1634 let mut stage_infos: Vec<(&StageKind, TypeId)> = Vec::new();
1636
1637 loop {
1638 let stage_index = stages.iter().position(|s| {
1639 s.spec
1640 .inlets
1641 .iter()
1642 .any(|inlet| inlet.id() == current_inlet)
1643 })?;
1644 if !seen.insert(stage_index) {
1645 return None;
1647 }
1648 let stage = &stages[stage_index];
1649 if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
1650 return None;
1652 }
1653 let inlet = &stage.spec.inlets[0];
1654 let outlet = &stage.spec.outlets[0];
1655
1656 if inlet.type_id() != in_type_id {
1658 return None;
1659 }
1660
1661 stage_infos.push((&stage.spec.kind, outlet.type_id()));
1662
1663 if outlet.id() == graph_outlet {
1664 break;
1665 }
1666 current_inlet = edges
1668 .iter()
1669 .find_map(|e| (e.outlet == outlet.id()).then_some(e.inlet))?;
1670 }
1671
1672 if seen.len() != stages.len() {
1673 return None;
1675 }
1676
1677 let (last_kind, last_outlet_type) = stage_infos.last()?;
1680 if *last_outlet_type != out_type_id {
1682 return None;
1683 }
1684
1685 let total = stage_infos.len();
1686 let mut middle_steps: Vec<TypedMiddleStep<In>> = Vec::with_capacity(total.saturating_sub(1));
1687
1688 for (kind, outlet_type) in &stage_infos[..total.saturating_sub(1)] {
1689 if *outlet_type != in_type_id {
1691 return None;
1692 }
1693 let step = match kind {
1694 StageKind::Identity => TypedMiddleStep::Pass,
1695 StageKind::Opaque => return None,
1699 StageKind::AsyncBoundary => TypedMiddleStep::AsyncBoundary,
1700 StageKind::Map(map) => {
1701 if map.scalar_i64.is_some() {
1702 return None;
1703 }
1704 let f = map
1706 .typed
1707 .downcast_ref::<Arc<dyn Fn(In) -> In + Send + Sync>>()?;
1708 TypedMiddleStep::Map(Arc::clone(f))
1709 }
1710 _ => return None,
1711 };
1712 middle_steps.push(step);
1713 }
1714
1715 let last_step: TypedLastStep<In, Out> = match last_kind {
1717 StageKind::Identity => {
1718 if in_type_id != out_type_id {
1720 return None;
1721 }
1722 TypedLastStep::Identity(Arc::new(|x: In| -> Out {
1725 let boxed: Box<dyn Any + Send> = Box::new(x);
1726 *boxed
1727 .downcast::<Out>()
1728 .expect("TypeId equality verified at plan time")
1729 }))
1730 }
1731 StageKind::Opaque => return None,
1733 StageKind::AsyncBoundary => {
1734 if in_type_id != out_type_id {
1737 return None;
1738 }
1739 TypedLastStep::Identity(Arc::new(|x: In| -> Out {
1740 let boxed: Box<dyn Any + Send> = Box::new(x);
1741 *boxed
1742 .downcast::<Out>()
1743 .expect("TypeId equality verified at plan time")
1744 }))
1745 }
1746 StageKind::Map(map) => {
1747 if map.scalar_i64.is_some() {
1748 return None;
1749 }
1750 let f = map
1752 .typed
1753 .downcast_ref::<Arc<dyn Fn(In) -> Out + Send + Sync>>()?;
1754 TypedLastStep::Map(Arc::clone(f))
1755 }
1756 _ => return None,
1757 };
1758
1759 Some(TypedFlowPlan {
1760 middle_steps,
1761 last_step,
1762 stage_count: total,
1763 })
1764}
1765
1766pub(crate) struct MergeSequenceCore<T> {
1781 next_sequence: u64,
1782 pending: Vec<(u64, T)>,
1784 output_buffer: VecDeque<T>,
1786 completed_count: usize,
1788 input_count: usize,
1790 completed: bool,
1792}
1793
1794impl<T> MergeSequenceCore<T> {
1795 pub(crate) fn new(input_count: usize) -> Self {
1796 Self {
1797 next_sequence: 0,
1798 pending: Vec::new(),
1799 output_buffer: VecDeque::new(),
1800 completed_count: 0,
1801 input_count,
1802 completed: false,
1803 }
1804 }
1805
1806 fn reset(&mut self) {
1808 self.next_sequence = 0;
1809 self.pending.clear();
1810 self.output_buffer.clear();
1811 self.completed_count = 0;
1812 self.completed = false;
1813 }
1814
1815 fn push_item(&mut self, seq: u64, val: T) -> StreamResult<()> {
1821 if seq == self.next_sequence {
1822 self.output_buffer.push_back(val);
1823 self.next_sequence += 1;
1824 while let Some(index) = self
1826 .pending
1827 .iter()
1828 .position(|(s, _)| *s == self.next_sequence)
1829 {
1830 let (_, item) = self.pending.remove(index);
1831 self.output_buffer.push_back(item);
1832 self.next_sequence += 1;
1833 }
1834 } else {
1835 if self.pending.iter().any(|(s, _)| *s == seq) {
1836 return Err(StreamError::Failed(format!(
1837 "duplicate sequence {seq} on merge sequence"
1838 )));
1839 }
1840 self.pending.push((seq, val));
1841 self.pending.sort_by_key(|(s, _)| *s);
1842 while let Some(index) = self
1844 .pending
1845 .iter()
1846 .position(|(s, _)| *s == self.next_sequence)
1847 {
1848 let (_, item) = self.pending.remove(index);
1849 self.output_buffer.push_back(item);
1850 self.next_sequence += 1;
1851 }
1852 }
1853 Ok(())
1854 }
1855
1856 fn on_inlet_complete(&mut self) -> StreamResult<bool> {
1864 self.completed_count += 1;
1865 if self.completed_count >= self.input_count && self.output_buffer.is_empty() {
1866 if !self.pending.is_empty() {
1867 return Err(StreamError::Failed(format!(
1868 "expected sequence {}, but all input ports have pushed or are complete",
1869 self.next_sequence,
1870 )));
1871 }
1872 self.completed = true;
1873 Ok(true)
1874 } else {
1875 Ok(false)
1876 }
1877 }
1878
1879 fn drain_into(&mut self, out: &mut Vec<T>) {
1881 out.extend(self.output_buffer.drain(..));
1882 }
1883}
1884
1885pub(crate) struct TypedMergeSequencePlan<In, T> {
1899 splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>>,
1902 extract_sequence: Arc<dyn Fn(&T) -> u64 + Send + Sync>,
1904 core: MergeSequenceCore<T>,
1906}
1907
1908impl<In: Clone + Send + 'static, T: Send + 'static> TypedMergeSequencePlan<In, T> {
1909 fn push_item(&mut self, item: In, out: &mut Vec<T>) -> StreamResult<()> {
1912 for split_fn in &self.splits {
1913 let val = split_fn(item.clone());
1914 let seq = (self.extract_sequence)(&val);
1915 self.core.push_item(seq, val)?;
1916 }
1917 self.core.drain_into(out);
1918 Ok(())
1919 }
1920
1921 fn finish(&mut self, out: &mut Vec<T>) -> StreamResult<()> {
1925 for _ in 0..self.splits.len() {
1926 self.core.on_inlet_complete()?;
1927 }
1928 self.core.drain_into(out);
1929 Ok(())
1930 }
1931
1932 fn reset(&mut self) {
1934 self.core.reset();
1935 }
1936}
1937
1938pub(crate) fn try_typed_merge_sequence_plan<In, Out>(
1950 stages: &[super::builder::StageRecord],
1951 edges: &[super::builder::Edge],
1952 graph_inlet: PortId,
1953 graph_outlet: PortId,
1954) -> Option<TypedMergeSequencePlan<In, Out>>
1955where
1956 In: Clone + Send + 'static,
1957 Out: Send + 'static,
1958{
1959 if stages.len() != 2 {
1961 return None;
1962 }
1963
1964 let in_type_id = TypeId::of::<In>();
1965 let out_type_id = TypeId::of::<Out>();
1966
1967 let unzip_idx = stages
1969 .iter()
1970 .position(|s| s.spec.inlets.len() == 1 && s.spec.inlets[0].id() == graph_inlet)?;
1971 let unzip_stage = &stages[unzip_idx];
1972
1973 let typed_split_any = match &unzip_stage.spec.kind {
1975 StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
1976 _ => return None,
1977 };
1978
1979 if unzip_stage.spec.inlets[0].type_id() != in_type_id {
1981 return None;
1982 }
1983
1984 let k = unzip_stage.spec.outlets.len();
1986 if k == 0 {
1987 return None;
1988 }
1989 for outlet in &unzip_stage.spec.outlets {
1990 if outlet.type_id() != out_type_id {
1991 return None;
1992 }
1993 }
1994
1995 let ms_idx = 1 - unzip_idx;
1997 let ms_stage = &stages[ms_idx];
1998
1999 let (ms_input_count, typed_extract_any) = match &ms_stage.spec.kind {
2001 StageKind::MergeSequence {
2002 input_count,
2003 typed_extract,
2004 ..
2005 } => (*input_count, Arc::clone(typed_extract)),
2006 _ => return None,
2007 };
2008
2009 if ms_stage.spec.inlets.len() != k || ms_stage.spec.outlets.len() != 1 {
2010 return None;
2011 }
2012 if ms_input_count != k {
2013 return None;
2014 }
2015 for inlet in &ms_stage.spec.inlets {
2017 if inlet.type_id() != out_type_id {
2018 return None;
2019 }
2020 }
2021 if ms_stage.spec.outlets[0].type_id() != out_type_id {
2022 return None;
2023 }
2024
2025 if ms_stage.spec.outlets[0].id() != graph_outlet {
2027 return None;
2028 }
2029
2030 let unzip_outlet_ids: Vec<PortId> =
2034 unzip_stage.spec.outlets.iter().map(AnyOutlet::id).collect();
2035 let ms_inlet_ids: Vec<PortId> = ms_stage.spec.inlets.iter().map(AnyInlet::id).collect();
2036
2037 let mut outlet_to_ms_inlet: Vec<Option<usize>> = vec![None; k];
2039 for edge in edges {
2040 if let Some(uo_idx) = unzip_outlet_ids.iter().position(|&id| id == edge.outlet) {
2041 let mi_idx = ms_inlet_ids.iter().position(|&id| id == edge.inlet)?;
2043 outlet_to_ms_inlet[uo_idx] = Some(mi_idx);
2044 }
2045 }
2046 if outlet_to_ms_inlet.iter().any(|x| x.is_none()) {
2047 return None; }
2049
2050 if k != 2 {
2057 return None;
2059 }
2060
2061 let typed_split =
2063 typed_split_any.downcast_ref::<Arc<dyn Fn(In) -> (Out, Out) + Send + Sync>>()?;
2064 let typed_split = Arc::clone(typed_split);
2065
2066 let typed_extract =
2068 typed_extract_any.downcast_ref::<Arc<dyn Fn(&Out) -> u64 + Send + Sync>>()?;
2069 let typed_extract = Arc::clone(typed_extract);
2070
2071 #[allow(clippy::type_complexity)]
2077 let mut splits: Vec<Option<Arc<dyn Fn(In) -> Out + Send + Sync>>> = vec![None; k];
2078
2079 let split0 = Arc::clone(&typed_split);
2080 let split1 = Arc::clone(&typed_split);
2081
2082 let ms_idx_for_out0 = outlet_to_ms_inlet[0].unwrap();
2083 let ms_idx_for_out1 = outlet_to_ms_inlet[1].unwrap();
2084
2085 splits[ms_idx_for_out0] = Some(Arc::new(move |input: In| split0(input).0));
2086 splits[ms_idx_for_out1] = Some(Arc::new(move |input: In| split1(input).1));
2087
2088 if splits.iter().any(|s| s.is_none()) {
2090 return None;
2091 }
2092 let splits: Vec<Arc<dyn Fn(In) -> Out + Send + Sync>> =
2093 splits.into_iter().map(|s| s.unwrap()).collect();
2094
2095 Some(TypedMergeSequencePlan {
2096 splits,
2097 extract_sequence: typed_extract,
2098 core: MergeSequenceCore::new(k),
2099 })
2100}
2101
2102pub(crate) fn run_typed_merge_sequence<In, T, I>(
2106 plan: &mut TypedMergeSequencePlan<In, T>,
2107 input: I,
2108) -> StreamResult<Vec<T>>
2109where
2110 In: Clone + Send + 'static,
2111 T: Send + 'static,
2112 I: IntoIterator<Item = In>,
2113{
2114 plan.reset();
2115 let input = input.into_iter();
2117 let hint = input.size_hint().0;
2118 let mut output: Vec<T> = Vec::with_capacity(hint * plan.splits.len());
2119 for item in input {
2120 plan.push_item(item, &mut output)?;
2121 }
2122 plan.finish(&mut output)?;
2123 Ok(output)
2124}
2125
2126pub(crate) struct MergeLatestCore<T> {
2139 latest: Vec<Option<T>>,
2141 seen_count: usize,
2143 completed_count: usize,
2145 input_count: usize,
2147 pending: VecDeque<Vec<T>>,
2149 completed: bool,
2151 eager_complete: bool,
2153}
2154
2155impl<T: Clone> MergeLatestCore<T> {
2156 pub(crate) fn new(input_count: usize, eager_complete: bool) -> Self {
2157 Self {
2158 latest: vec![None; input_count],
2159 seen_count: 0,
2160 completed_count: 0,
2161 input_count,
2162 pending: VecDeque::new(),
2163 completed: false,
2164 eager_complete,
2165 }
2166 }
2167
2168 fn reset(&mut self) {
2170 for slot in &mut self.latest {
2171 *slot = None;
2172 }
2173 self.seen_count = 0;
2174 self.completed_count = 0;
2175 self.pending.clear();
2176 self.completed = false;
2177 }
2178
2179 fn push_item(&mut self, inlet_index: usize, val: T) {
2182 if self.latest[inlet_index].is_none() {
2183 self.seen_count += 1;
2184 }
2185 self.latest[inlet_index] = Some(val);
2186 if self.seen_count >= self.input_count {
2187 let snapshot: Vec<T> = self
2189 .latest
2190 .iter()
2191 .map(|s| s.clone().expect("merge-latest typed: slot seen but None"))
2192 .collect();
2193 self.pending.push_back(snapshot);
2194 }
2195 }
2196
2197 fn on_inlet_complete(&mut self) -> bool {
2202 self.completed_count += 1;
2203 let all_done = self.completed_count >= self.input_count;
2204 let eager_done = self.eager_complete && self.pending.is_empty();
2205 if all_done || eager_done {
2206 self.completed = true;
2207 true
2208 } else {
2209 false
2210 }
2211 }
2212
2213 fn drain_into(&mut self, out: &mut Vec<Vec<T>>) {
2215 out.extend(self.pending.drain(..));
2216 }
2217}
2218
2219pub(crate) struct TypedMergeLatestPlan<In, T> {
2233 splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>>,
2236 core: MergeLatestCore<T>,
2238}
2239
2240impl<In: Clone + Send + 'static, T: Clone + Send + 'static> TypedMergeLatestPlan<In, T> {
2241 fn push_item(&mut self, item: In, out: &mut Vec<Vec<T>>) {
2243 for (idx, split_fn) in self.splits.iter().enumerate() {
2244 let val = split_fn(item.clone());
2245 self.core.push_item(idx, val);
2246 }
2247 self.core.drain_into(out);
2248 }
2249
2250 fn finish(&mut self) -> bool {
2253 for _ in 0..self.splits.len() {
2255 if self.core.on_inlet_complete() {
2256 return true;
2257 }
2258 }
2259 true
2260 }
2261
2262 fn reset(&mut self) {
2264 self.core.reset();
2265 }
2266}
2267
2268pub(crate) fn try_typed_merge_latest_plan<In, T>(
2284 stages: &[super::builder::StageRecord],
2285 edges: &[super::builder::Edge],
2286 graph_inlet: PortId,
2287 graph_outlet: PortId,
2288) -> Option<TypedMergeLatestPlan<In, T>>
2289where
2290 In: Clone + Send + 'static,
2291 T: Clone + Send + 'static,
2292{
2293 if stages.len() != 2 {
2295 return None;
2296 }
2297
2298 let in_type_id = TypeId::of::<In>();
2299 let elem_type_id = TypeId::of::<T>();
2300 let vec_type_id = TypeId::of::<Vec<T>>();
2301
2302 let unzip_idx = stages
2304 .iter()
2305 .position(|s| s.spec.inlets.len() == 1 && s.spec.inlets[0].id() == graph_inlet)?;
2306 let unzip_stage = &stages[unzip_idx];
2307
2308 let typed_split_any = match &unzip_stage.spec.kind {
2310 StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
2311 _ => return None,
2312 };
2313
2314 if unzip_stage.spec.inlets[0].type_id() != in_type_id {
2316 return None;
2317 }
2318
2319 let k = unzip_stage.spec.outlets.len();
2321 if k == 0 {
2322 return None;
2323 }
2324 for outlet in &unzip_stage.spec.outlets {
2325 if outlet.type_id() != elem_type_id {
2326 return None;
2327 }
2328 }
2329
2330 let ml_idx = 1 - unzip_idx;
2332 let ml_stage = &stages[ml_idx];
2333
2334 let (ml_input_count, typed_snapshot_any) = match &ml_stage.spec.kind {
2336 StageKind::MergeLatest {
2337 input_count,
2338 typed_snapshot,
2339 ..
2340 } => (*input_count, Arc::clone(typed_snapshot)),
2341 _ => return None,
2342 };
2343
2344 if ml_stage.spec.inlets.len() != k || ml_stage.spec.outlets.len() != 1 {
2345 return None;
2346 }
2347 if ml_input_count != k {
2348 return None;
2349 }
2350 for inlet in &ml_stage.spec.inlets {
2352 if inlet.type_id() != elem_type_id {
2353 return None;
2354 }
2355 }
2356 if ml_stage.spec.outlets[0].type_id() != vec_type_id {
2358 return None;
2359 }
2360
2361 if ml_stage.spec.outlets[0].id() != graph_outlet {
2363 return None;
2364 }
2365
2366 let unzip_outlet_ids: Vec<PortId> =
2368 unzip_stage.spec.outlets.iter().map(AnyOutlet::id).collect();
2369 let ml_inlet_ids: Vec<PortId> = ml_stage.spec.inlets.iter().map(AnyInlet::id).collect();
2370
2371 let mut outlet_to_ml_inlet: Vec<Option<usize>> = vec![None; k];
2372 for edge in edges {
2373 if let Some(uo_idx) = unzip_outlet_ids.iter().position(|&id| id == edge.outlet) {
2374 let mi_idx = ml_inlet_ids.iter().position(|&id| id == edge.inlet)?;
2376 outlet_to_ml_inlet[uo_idx] = Some(mi_idx);
2377 }
2378 }
2379 if outlet_to_ml_inlet.iter().any(|x| x.is_none()) {
2380 return None; }
2382
2383 if k != 2 {
2385 return None;
2386 }
2387
2388 type SplitFn<A, B> = Arc<dyn Fn(A) -> (B, B) + Send + Sync>;
2390 let typed_split = typed_split_any.downcast_ref::<SplitFn<In, T>>()?;
2391 let typed_split = Arc::clone(typed_split);
2392
2393 type SnapshotFn<U> = Arc<dyn Fn(&[Option<U>]) -> Vec<U> + Send + Sync>;
2395 typed_snapshot_any.downcast_ref::<SnapshotFn<T>>()?;
2396
2397 #[allow(clippy::type_complexity)]
2399 let mut splits: Vec<Option<Arc<dyn Fn(In) -> T + Send + Sync>>> = vec![None; k];
2400
2401 let split0 = Arc::clone(&typed_split);
2402 let split1 = Arc::clone(&typed_split);
2403
2404 let ml_idx_for_out0 = outlet_to_ml_inlet[0].unwrap();
2405 let ml_idx_for_out1 = outlet_to_ml_inlet[1].unwrap();
2406
2407 splits[ml_idx_for_out0] = Some(Arc::new(move |input: In| split0(input).0));
2408 splits[ml_idx_for_out1] = Some(Arc::new(move |input: In| split1(input).1));
2409
2410 if splits.iter().any(|s| s.is_none()) {
2411 return None;
2412 }
2413 let splits: Vec<Arc<dyn Fn(In) -> T + Send + Sync>> =
2414 splits.into_iter().map(|s| s.unwrap()).collect();
2415
2416 let eager_complete = match &ml_stage.spec.kind {
2418 StageKind::MergeLatest { eager_complete, .. } => *eager_complete,
2419 _ => return None,
2420 };
2421
2422 Some(TypedMergeLatestPlan {
2423 splits,
2424 core: MergeLatestCore::new(k, eager_complete),
2425 })
2426}
2427
2428type MergeLatestRunner<In, Out> =
2439 Box<dyn FnOnce(&mut dyn Iterator<Item = In>) -> StreamResult<Vec<Out>>>;
2440
2441pub(crate) fn try_build_typed_merge_latest_dispatch<In, Out>(
2461 stages: &[super::builder::StageRecord],
2462 edges: &[super::builder::Edge],
2463 inlet: PortId,
2464 outlet: PortId,
2465) -> Option<MergeLatestRunner<In, Out>>
2466where
2467 In: Clone + Send + 'static,
2468 Out: Send + 'static,
2469{
2470 let elem_type_id: TypeId = stages.iter().find_map(|s| {
2472 if let StageKind::MergeLatest { .. } = &s.spec.kind {
2473 s.spec.inlets.first().map(|i| i.type_id())
2474 } else {
2475 None
2476 }
2477 })?;
2478
2479 macro_rules! try_elem {
2489 ($($T:ty),*) => {
2490 $(
2491 if elem_type_id == TypeId::of::<$T>() {
2492 let mut plan = try_typed_merge_latest_plan::<In, $T>(stages, edges, inlet, outlet)?;
2493 let runner: MergeLatestRunner<In, Out> = Box::new(
2496 move |iter: &mut dyn Iterator<Item = In>| {
2497 plan.reset();
2498 let hint = iter.size_hint().0;
2499 let mut output: Vec<Vec<$T>> = Vec::with_capacity(hint);
2500 for item in iter {
2501 plan.push_item(item, &mut output);
2502 }
2503 plan.finish();
2504 let boxed: Box<dyn Any + Send> = Box::new(output);
2508 boxed
2509 .downcast::<Vec<Out>>()
2510 .map(|b| *b)
2511 .map_err(|_| StreamError::Failed(
2512 "merge-latest typed runner: Out type mismatch".into()
2513 ))
2514 }
2515 );
2516 return Some(runner);
2517 }
2518 )*
2519 };
2520 }
2521
2522 try_elem!(
2523 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, String
2524 );
2525
2526 None
2528}
2529
2530enum CyclicFeedbackStep<T> {
2564 Pass,
2566 Map(Arc<dyn Fn(T) -> T + Send + Sync>),
2568 TakeWhile(Arc<dyn Fn(&T) -> bool + Send + Sync>),
2571}
2572
2573type CyclicFeedbackRunner<In, Out> = Box<
2574 dyn FnOnce(
2575 &mut dyn Iterator<Item = In>,
2576 FusedExecutionConfig,
2577 ) -> StreamResult<FusedExecutionReport<Out>>,
2578>;
2579
2580fn run_cyclic_feedback_chain<T>(
2586 steps: &[CyclicFeedbackStep<T>],
2587 mut value: T,
2588 feedback_open: &mut bool,
2589) -> Option<T> {
2590 for step in steps {
2591 match step {
2592 CyclicFeedbackStep::Pass => {}
2593 CyclicFeedbackStep::Map(f) => value = f(value),
2594 CyclicFeedbackStep::TakeWhile(predicate) => {
2595 if !predicate(&value) {
2596 *feedback_open = false;
2597 return None;
2598 }
2599 }
2600 }
2601 }
2602 Some(value)
2603}
2604
2605pub(crate) fn try_build_typed_cyclic_feedback_dispatch<In, Out>(
2611 stages: &[super::builder::StageRecord],
2612 edges: &[super::builder::Edge],
2613 graph_inlet: PortId,
2614 graph_outlet: PortId,
2615) -> Option<CyclicFeedbackRunner<In, Out>>
2616where
2617 In: Clone + Send + 'static,
2618 Out: Send + 'static,
2619{
2620 let in_type_id = TypeId::of::<In>();
2621 if in_type_id != TypeId::of::<Out>() {
2624 return None;
2625 }
2626
2627 let inlet_for_outlet = |outlet: PortId| {
2629 edges
2630 .iter()
2631 .find_map(|e| (e.outlet == outlet).then_some(e.inlet))
2632 };
2633 let stage_owning_inlet = |inlet: PortId| {
2634 stages
2635 .iter()
2636 .enumerate()
2637 .find(|(_, s)| s.spec.inlets.iter().any(|i| i.id() == inlet))
2638 };
2639
2640 let (merge_index, merge) = stage_owning_inlet(graph_inlet)?;
2642 if !matches!(merge.spec.kind, StageKind::MergePreferred) {
2643 return None;
2644 }
2645 if merge.spec.inlets.len() != 2 || merge.spec.outlets.len() != 1 {
2647 return None;
2648 }
2649 let preferred_inlet = merge.spec.inlets[0].id();
2650 if preferred_inlet == graph_inlet || merge.spec.inlets[1].id() != graph_inlet {
2652 return None;
2653 }
2654 if merge.spec.inlets.iter().any(|i| i.type_id() != in_type_id)
2655 || merge.spec.outlets[0].type_id() != in_type_id
2656 {
2657 return None;
2658 }
2659
2660 let broadcast_inlet = inlet_for_outlet(merge.spec.outlets[0].id())?;
2662 let (broadcast_index, broadcast) = stage_owning_inlet(broadcast_inlet)?;
2663 if !matches!(broadcast.spec.kind, StageKind::Broadcast) {
2664 return None;
2665 }
2666 if broadcast.spec.inlets.len() != 1 || broadcast.spec.outlets.len() != 2 {
2667 return None;
2668 }
2669 if broadcast.spec.inlets[0].type_id() != in_type_id
2670 || broadcast
2671 .spec
2672 .outlets
2673 .iter()
2674 .any(|o| o.type_id() != in_type_id)
2675 {
2676 return None;
2677 }
2678
2679 if broadcast.spec.outlets[0].id() != graph_outlet
2691 || broadcast.spec.outlets[1].id() == graph_outlet
2692 {
2693 return None;
2694 }
2695 let feedback_outlet = broadcast.spec.outlets[1].id();
2696
2697 let mut visited: HashSet<usize> = HashSet::new();
2699 visited.insert(merge_index);
2700 visited.insert(broadcast_index);
2701 let mut steps: Vec<CyclicFeedbackStep<In>> = Vec::new();
2702 let mut current_outlet = feedback_outlet;
2703 loop {
2704 let inlet = inlet_for_outlet(current_outlet)?;
2705 if inlet == preferred_inlet {
2706 break; }
2708 let (stage_index, stage) = stage_owning_inlet(inlet)?;
2709 if stage.spec.inlets.len() != 1 || stage.spec.outlets.len() != 1 {
2710 return None;
2711 }
2712 if stage.spec.inlets[0].type_id() != in_type_id
2713 || stage.spec.outlets[0].type_id() != in_type_id
2714 {
2715 return None;
2716 }
2717 if !visited.insert(stage_index) {
2718 return None; }
2720 let step = match &stage.spec.kind {
2721 StageKind::Identity => CyclicFeedbackStep::Pass,
2722 StageKind::Map(map) => {
2723 if map.scalar_i64.is_some() {
2724 return None;
2725 }
2726 let f = map
2727 .typed
2728 .downcast_ref::<Arc<dyn Fn(In) -> In + Send + Sync>>()?;
2729 CyclicFeedbackStep::Map(Arc::clone(f))
2730 }
2731 StageKind::Opaque => match stage.spec.typed_cyclic.as_ref()? {
2732 TypedCyclicOp::BufferPassthrough => CyclicFeedbackStep::Pass,
2733 TypedCyclicOp::TakeWhile(predicate) => {
2734 let p = predicate.downcast_ref::<Arc<dyn Fn(&In) -> bool + Send + Sync>>()?;
2735 CyclicFeedbackStep::TakeWhile(Arc::clone(p))
2736 }
2737 },
2738 _ => return None,
2739 };
2740 steps.push(step);
2741 current_outlet = stage.spec.outlets[0].id();
2742 }
2743
2744 if visited.len() != stages.len() {
2746 return None;
2747 }
2748
2749 let runner: CyclicFeedbackRunner<In, Out> = Box::new(
2750 move |iter: &mut dyn Iterator<Item = In>, config: FusedExecutionConfig| {
2751 let limit = config.event_limit;
2752 let mut output: Vec<In> = Vec::with_capacity(iter.size_hint().0);
2753 let mut pending: VecDeque<In> = VecDeque::new();
2756 let mut feedback_open = true;
2757 let mut events: usize = 0;
2758
2759 for item in iter {
2760 pending.push_back(item);
2761 while let Some(value) = pending.pop_front() {
2762 events += 1;
2765 if events > limit {
2766 return Err(StreamError::EventLimitExceeded { limit });
2767 }
2768 output.push(value.clone());
2770 if feedback_open {
2772 events += steps.len();
2773 if events > limit {
2774 return Err(StreamError::EventLimitExceeded { limit });
2775 }
2776 if let Some(next) =
2777 run_cyclic_feedback_chain(&steps, value, &mut feedback_open)
2778 {
2779 pending.push_back(next);
2780 }
2781 }
2782 }
2783 }
2784
2785 let output = downcast_output_vec::<In, Out>(output, "cyclic feedback")?;
2786 Ok(FusedExecutionReport::new(
2787 output,
2788 events,
2789 0,
2790 FusedPlanReport::empty(FusedExecutorTier::TypedCyclicFeedback),
2791 ))
2792 },
2793 );
2794 Some(runner)
2795}
2796
2797type AcyclicJunctionRunner<In, Out> =
2802 Box<dyn FnOnce(&mut dyn Iterator<Item = In>, &mut FusedMetricsRun) -> StreamResult<Vec<Out>>>;
2803
2804fn downcast_output_vec<T, Out>(output: Vec<T>, context: &'static str) -> StreamResult<Vec<Out>>
2805where
2806 T: Send + 'static,
2807 Out: Send + 'static,
2808{
2809 let boxed: Box<dyn Any + Send> = Box::new(output);
2810 boxed
2811 .downcast::<Vec<Out>>()
2812 .map(|b| *b)
2813 .map_err(|_| StreamError::Failed(format!("{context} typed runner: output type mismatch")))
2814}
2815
2816fn stage_with_graph_inlet(
2817 stages: &[super::builder::StageRecord],
2818 graph_inlet: PortId,
2819) -> Option<(usize, &super::builder::StageRecord)> {
2820 stages.iter().enumerate().find(|(_, stage)| {
2821 stage
2822 .spec
2823 .inlets
2824 .iter()
2825 .any(|inlet| inlet.id() == graph_inlet)
2826 })
2827}
2828
2829fn other_stage(
2830 stages: &[super::builder::StageRecord],
2831 index: usize,
2832) -> Option<(usize, &super::builder::StageRecord)> {
2833 if stages.len() != 2 {
2834 return None;
2835 }
2836 let other = 1usize.checked_sub(index)?;
2837 stages.get(other).map(|stage| (other, stage))
2838}
2839
2840fn edge_target_index(
2841 edges: &[super::builder::Edge],
2842 outlet: PortId,
2843 inlets: &[AnyInlet],
2844) -> Option<usize> {
2845 let inlet_id = edges
2846 .iter()
2847 .find_map(|edge| (edge.outlet == outlet).then_some(edge.inlet))?;
2848 inlets.iter().position(|inlet| inlet.id() == inlet_id)
2849}
2850
2851fn outlets_cover_inlets(
2852 edges: &[super::builder::Edge],
2853 outlets: &[AnyOutlet],
2854 inlets: &[AnyInlet],
2855) -> Option<Vec<usize>> {
2856 if outlets.len() != inlets.len() {
2857 return None;
2858 }
2859 let mut seen = vec![false; inlets.len()];
2860 let mut mapping = Vec::with_capacity(outlets.len());
2861 for outlet in outlets {
2862 let inlet_index = edge_target_index(edges, outlet.id(), inlets)?;
2863 if seen[inlet_index] {
2864 return None;
2865 }
2866 seen[inlet_index] = true;
2867 mapping.push(inlet_index);
2868 }
2869 seen.iter().all(|item| *item).then_some(mapping)
2870}
2871
2872pub(crate) fn try_build_typed_acyclic_junction_dispatch<In, Out>(
2873 stages: &[super::builder::StageRecord],
2874 edges: &[super::builder::Edge],
2875 graph_inlet: PortId,
2876 graph_outlet: PortId,
2877 metrics_enabled: bool,
2878 metrics_timing: bool,
2879) -> Option<AcyclicJunctionRunner<In, Out>>
2880where
2881 In: Clone + Send + 'static,
2882 Out: Send + 'static,
2883{
2884 if let Some(runner) = try_typed_broadcast_zip_runner::<In, Out>(
2885 stages,
2886 edges,
2887 graph_inlet,
2888 graph_outlet,
2889 metrics_enabled,
2890 metrics_timing,
2891 ) {
2892 return Some(runner);
2893 }
2894 if let Some(runner) = try_typed_balance_merge_runner::<In, Out>(
2895 stages,
2896 edges,
2897 graph_inlet,
2898 graph_outlet,
2899 metrics_enabled,
2900 ) {
2901 return Some(runner);
2902 }
2903 if let Some(runner) = try_typed_partition_merge_runner::<In, Out>(
2904 stages,
2905 edges,
2906 graph_inlet,
2907 graph_outlet,
2908 metrics_enabled,
2909 ) {
2910 return Some(runner);
2911 }
2912 if let Some(runner) = try_build_typed_unzip_zip_dispatch::<In, Out>(
2913 stages,
2914 edges,
2915 graph_inlet,
2916 graph_outlet,
2917 metrics_enabled,
2918 ) {
2919 return Some(runner);
2920 }
2921 try_build_typed_merge_sorted_dispatch::<In, Out>(
2922 stages,
2923 edges,
2924 graph_inlet,
2925 graph_outlet,
2926 metrics_enabled,
2927 )
2928}
2929
2930fn try_typed_broadcast_zip_runner<In, Out>(
2931 stages: &[super::builder::StageRecord],
2932 edges: &[super::builder::Edge],
2933 graph_inlet: PortId,
2934 graph_outlet: PortId,
2935 metrics_enabled: bool,
2936 metrics_timing: bool,
2937) -> Option<AcyclicJunctionRunner<In, Out>>
2938where
2939 In: Clone + Send + 'static,
2940 Out: Send + 'static,
2941{
2942 if stages.len() != 2 || edges.len() != 2 {
2943 return None;
2944 }
2945 let in_type = TypeId::of::<In>();
2946 let pair_type = TypeId::of::<(In, In)>();
2947 if TypeId::of::<Out>() != pair_type {
2948 return None;
2949 }
2950
2951 let (broadcast_idx, broadcast_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
2952 if !matches!(broadcast_stage.spec.kind, StageKind::Broadcast) {
2953 return None;
2954 }
2955 let (zip_idx, zip_stage) = other_stage(stages, broadcast_idx)?;
2956 if !matches!(zip_stage.spec.kind, StageKind::Zip(_)) {
2957 return None;
2958 }
2959
2960 if broadcast_stage.spec.inlets.len() != 1
2961 || broadcast_stage.spec.outlets.len() != 2
2962 || zip_stage.spec.inlets.len() != 2
2963 || zip_stage.spec.outlets.len() != 1
2964 || zip_stage.spec.outlets[0].id() != graph_outlet
2965 || zip_stage.spec.outlets[0].type_id() != pair_type
2966 || broadcast_stage.spec.inlets[0].type_id() != in_type
2967 || broadcast_stage
2968 .spec
2969 .outlets
2970 .iter()
2971 .any(|outlet| outlet.type_id() != in_type)
2972 || zip_stage
2973 .spec
2974 .inlets
2975 .iter()
2976 .any(|inlet| inlet.type_id() != in_type)
2977 {
2978 return None;
2979 }
2980 outlets_cover_inlets(edges, &broadcast_stage.spec.outlets, &zip_stage.spec.inlets)?;
2981
2982 if !metrics_enabled {
2983 return Some(Box::new(|iter, _metrics| {
2984 let output = collect_broadcast_zip(iter);
2985 downcast_output_vec(output, "broadcast-zip")
2986 }));
2987 }
2988
2989 if !metrics_timing {
2990 return Some(Box::new(move |iter, metrics| {
2991 let output = collect_broadcast_zip(iter);
2992 let elements = output.len() as u64;
2993 metrics.complete_exact_counts(
2994 broadcast_idx,
2995 MetricUnits::scalar(elements),
2996 MetricUnits::scalar(elements.saturating_mul(2)),
2997 );
2998 metrics.complete_exact_counts(
2999 zip_idx,
3000 MetricUnits::scalar(elements.saturating_mul(2)),
3001 MetricUnits::scalar(elements),
3002 );
3003 downcast_output_vec(output, "broadcast-zip")
3004 }));
3005 }
3006
3007 Some(Box::new(move |iter, metrics| {
3008 let started = Instant::now();
3009 let output = collect_broadcast_zip(iter);
3010 let elapsed = started.elapsed();
3011 let elements = output.len() as u64;
3012 metrics.complete_exact_counts(
3013 broadcast_idx,
3014 MetricUnits::scalar(elements),
3015 MetricUnits::scalar(elements.saturating_mul(2)),
3016 );
3017 metrics.complete_exact_counts(
3018 zip_idx,
3019 MetricUnits::scalar(elements.saturating_mul(2)),
3020 MetricUnits::scalar(elements),
3021 );
3022 metrics.record_processing_time_share(&[broadcast_idx, zip_idx], elapsed);
3023 downcast_output_vec(output, "broadcast-zip")
3024 }))
3025}
3026
3027#[inline(never)]
3028fn collect_broadcast_zip<In: Clone>(iter: &mut dyn Iterator<Item = In>) -> Vec<(In, In)> {
3029 let mut output = Vec::with_capacity(iter.size_hint().0);
3030 for item in iter {
3031 output.push((item.clone(), item));
3032 }
3033 output
3034}
3035
3036fn try_typed_balance_merge_runner<In, Out>(
3037 stages: &[super::builder::StageRecord],
3038 edges: &[super::builder::Edge],
3039 graph_inlet: PortId,
3040 graph_outlet: PortId,
3041 metrics_enabled: bool,
3042) -> Option<AcyclicJunctionRunner<In, Out>>
3043where
3044 In: Clone + Send + 'static,
3045 Out: Send + 'static,
3046{
3047 if stages.len() != 2 || TypeId::of::<In>() != TypeId::of::<Out>() {
3048 return None;
3049 }
3050 let in_type = TypeId::of::<In>();
3051 let (balance_idx, balance_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
3052 if !matches!(balance_stage.spec.kind, StageKind::Balance) {
3053 return None;
3054 }
3055 let (merge_idx, merge_stage) = other_stage(stages, balance_idx)?;
3056 if !matches!(merge_stage.spec.kind, StageKind::Merge) {
3057 return None;
3058 }
3059
3060 if balance_stage.spec.inlets.len() != 1
3061 || balance_stage.spec.outlets.is_empty()
3062 || merge_stage.spec.outlets.len() != 1
3063 || merge_stage.spec.outlets[0].id() != graph_outlet
3064 || edges.len() != balance_stage.spec.outlets.len()
3065 || balance_stage.spec.inlets[0].type_id() != in_type
3066 || merge_stage.spec.outlets[0].type_id() != in_type
3067 || balance_stage
3068 .spec
3069 .outlets
3070 .iter()
3071 .any(|outlet| outlet.type_id() != in_type)
3072 || merge_stage
3073 .spec
3074 .inlets
3075 .iter()
3076 .any(|inlet| inlet.type_id() != in_type)
3077 {
3078 return None;
3079 }
3080 outlets_cover_inlets(edges, &balance_stage.spec.outlets, &merge_stage.spec.inlets)?;
3081
3082 if !metrics_enabled {
3083 return Some(Box::new(|iter, _metrics| {
3084 let mut output = Vec::with_capacity(iter.size_hint().0);
3085 output.extend(iter);
3086 downcast_output_vec(output, "balance-merge")
3087 }));
3088 }
3089
3090 Some(Box::new(move |iter, metrics| {
3091 let mut output = Vec::with_capacity(iter.size_hint().0);
3092 let enabled = metrics.enabled();
3093 if !enabled {
3094 output.extend(iter);
3095 return downcast_output_vec(output, "balance-merge");
3096 }
3097 let timing = enabled && metrics.timing_enabled();
3098 if !timing {
3099 output.extend(iter);
3100 let elements = output.len() as u64;
3101 for stage_index in [balance_idx, merge_idx] {
3102 metrics.complete_exact_counts(
3103 stage_index,
3104 MetricUnits::scalar(elements),
3105 MetricUnits::scalar(elements),
3106 );
3107 }
3108 return downcast_output_vec(output, "balance-merge");
3109 }
3110 let started = Instant::now();
3111 output.extend(iter);
3112 let elapsed = started.elapsed();
3113 let elements = output.len() as u64;
3114 for stage_index in [balance_idx, merge_idx] {
3115 metrics.complete_exact_counts(
3116 stage_index,
3117 MetricUnits::scalar(elements),
3118 MetricUnits::scalar(elements),
3119 );
3120 }
3121 metrics.record_processing_time_share(&[balance_idx, merge_idx], elapsed);
3122 downcast_output_vec(output, "balance-merge")
3123 }))
3124}
3125
3126fn try_typed_partition_merge_runner<In, Out>(
3127 stages: &[super::builder::StageRecord],
3128 edges: &[super::builder::Edge],
3129 graph_inlet: PortId,
3130 graph_outlet: PortId,
3131 metrics_enabled: bool,
3132) -> Option<AcyclicJunctionRunner<In, Out>>
3133where
3134 In: Clone + Send + 'static,
3135 Out: Send + 'static,
3136{
3137 if stages.len() != 2 || TypeId::of::<In>() != TypeId::of::<Out>() {
3138 return None;
3139 }
3140 let in_type = TypeId::of::<In>();
3141 let (partition_idx, partition_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
3142 let (output_count, typed_partitioner) = match &partition_stage.spec.kind {
3143 StageKind::Partition {
3144 output_count,
3145 typed_partitioner,
3146 ..
3147 } => (*output_count, Arc::clone(typed_partitioner)),
3148 _ => return None,
3149 };
3150 let (merge_idx, merge_stage) = other_stage(stages, partition_idx)?;
3151 if !matches!(merge_stage.spec.kind, StageKind::Merge) {
3152 return None;
3153 }
3154
3155 if partition_stage.spec.inlets.len() != 1
3156 || partition_stage.spec.outlets.len() != output_count
3157 || merge_stage.spec.outlets.len() != 1
3158 || merge_stage.spec.outlets[0].id() != graph_outlet
3159 || merge_stage.spec.inlets.len() != output_count
3160 || edges.len() != output_count
3161 || partition_stage.spec.inlets[0].type_id() != in_type
3162 || merge_stage.spec.outlets[0].type_id() != in_type
3163 || partition_stage
3164 .spec
3165 .outlets
3166 .iter()
3167 .any(|outlet| outlet.type_id() != in_type)
3168 || merge_stage
3169 .spec
3170 .inlets
3171 .iter()
3172 .any(|inlet| inlet.type_id() != in_type)
3173 {
3174 return None;
3175 }
3176 outlets_cover_inlets(
3177 edges,
3178 &partition_stage.spec.outlets,
3179 &merge_stage.spec.inlets,
3180 )?;
3181
3182 let partitioner =
3183 typed_partitioner.downcast_ref::<Arc<dyn Fn(&In) -> usize + Send + Sync>>()?;
3184 let partitioner = Arc::clone(partitioner);
3185
3186 if !metrics_enabled {
3187 return Some(Box::new(move |iter, _metrics| {
3188 let mut output = Vec::with_capacity(iter.size_hint().0);
3189 for item in iter {
3190 let idx = partitioner(&item);
3191 if idx >= output_count {
3192 return Err(StreamError::Failed(format!(
3193 "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
3194 )));
3195 }
3196 output.push(item);
3197 }
3198 downcast_output_vec(output, "partition-merge")
3199 }));
3200 }
3201
3202 Some(Box::new(move |iter, metrics| {
3203 let mut output = Vec::with_capacity(iter.size_hint().0);
3204 let enabled = metrics.enabled();
3205 let timing = enabled && metrics.timing_enabled();
3206 if !timing {
3207 for item in iter {
3208 let idx = partitioner(&item);
3209 if idx >= output_count {
3210 return Err(StreamError::Failed(format!(
3211 "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
3212 )));
3213 }
3214 output.push(item);
3215 }
3216 let elements = output.len() as u64;
3217 for stage_index in [partition_idx, merge_idx] {
3218 metrics.complete_exact_counts(
3219 stage_index,
3220 MetricUnits::scalar(elements),
3221 MetricUnits::scalar(elements),
3222 );
3223 }
3224 return downcast_output_vec(output, "partition-merge");
3225 }
3226 let started = Instant::now();
3227 for item in iter {
3228 let idx = partitioner(&item);
3229 if idx >= output_count {
3230 return Err(StreamError::Failed(format!(
3231 "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
3232 )));
3233 }
3234 output.push(item);
3235 }
3236 let elapsed = started.elapsed();
3237 let elements = output.len() as u64;
3238 for stage_index in [partition_idx, merge_idx] {
3239 metrics.complete_exact_counts(
3240 stage_index,
3241 MetricUnits::scalar(elements),
3242 MetricUnits::scalar(elements),
3243 );
3244 }
3245 metrics.record_processing_time_share(&[partition_idx, merge_idx], elapsed);
3246 downcast_output_vec(output, "partition-merge")
3247 }))
3248}
3249
3250fn try_build_typed_unzip_zip_dispatch<In, Out>(
3251 stages: &[super::builder::StageRecord],
3252 edges: &[super::builder::Edge],
3253 graph_inlet: PortId,
3254 graph_outlet: PortId,
3255 metrics_enabled: bool,
3256) -> Option<AcyclicJunctionRunner<In, Out>>
3257where
3258 In: Clone + Send + 'static,
3259 Out: Send + 'static,
3260{
3261 let elem_type_id = stages.iter().find_map(|stage| {
3262 if let StageKind::Zip(_) = stage.spec.kind {
3263 let [left, right] = stage.spec.inlets.as_slice() else {
3264 return None;
3265 };
3266 (left.type_id() == right.type_id()).then_some(left.type_id())
3267 } else {
3268 None
3269 }
3270 })?;
3271
3272 macro_rules! try_elem {
3273 ($($T:ty),*) => {
3274 $(
3275 if elem_type_id == TypeId::of::<$T>() {
3276 return try_typed_unzip_zip_runner_same::<In, $T, Out>(
3277 stages,
3278 edges,
3279 graph_inlet,
3280 graph_outlet,
3281 metrics_enabled,
3282 );
3283 }
3284 )*
3285 };
3286 }
3287
3288 try_elem!(
3289 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char,
3290 String
3291 );
3292 None
3293}
3294
3295fn try_typed_unzip_zip_runner_same<In, T, Out>(
3296 stages: &[super::builder::StageRecord],
3297 edges: &[super::builder::Edge],
3298 graph_inlet: PortId,
3299 graph_outlet: PortId,
3300 metrics_enabled: bool,
3301) -> Option<AcyclicJunctionRunner<In, Out>>
3302where
3303 In: Clone + Send + 'static,
3304 T: Send + 'static,
3305 Out: Send + 'static,
3306{
3307 if stages.len() != 2 || edges.len() != 2 || TypeId::of::<Out>() != TypeId::of::<(T, T)>() {
3308 return None;
3309 }
3310 let in_type = TypeId::of::<In>();
3311 let elem_type = TypeId::of::<T>();
3312 let pair_type = TypeId::of::<(T, T)>();
3313
3314 let (unzip_idx, unzip_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
3315 let typed_split = match &unzip_stage.spec.kind {
3316 StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
3317 _ => return None,
3318 };
3319 let (zip_idx, zip_stage) = other_stage(stages, unzip_idx)?;
3320 if !matches!(zip_stage.spec.kind, StageKind::Zip(_)) {
3321 return None;
3322 }
3323
3324 if unzip_stage.spec.inlets.len() != 1
3325 || unzip_stage.spec.outlets.len() != 2
3326 || zip_stage.spec.inlets.len() != 2
3327 || zip_stage.spec.outlets.len() != 1
3328 || zip_stage.spec.outlets[0].id() != graph_outlet
3329 || unzip_stage.spec.inlets[0].type_id() != in_type
3330 || zip_stage.spec.outlets[0].type_id() != pair_type
3331 || unzip_stage
3332 .spec
3333 .outlets
3334 .iter()
3335 .any(|outlet| outlet.type_id() != elem_type)
3336 || zip_stage
3337 .spec
3338 .inlets
3339 .iter()
3340 .any(|inlet| inlet.type_id() != elem_type)
3341 {
3342 return None;
3343 }
3344 let mapping = outlets_cover_inlets(edges, &unzip_stage.spec.outlets, &zip_stage.spec.inlets)?;
3345 let out0_to_left = mapping.first().copied()? == 0;
3346
3347 let split = typed_split.downcast_ref::<Arc<dyn Fn(In) -> (T, T) + Send + Sync>>()?;
3348 let split = Arc::clone(split);
3349
3350 if !metrics_enabled {
3351 return Some(Box::new(move |iter, _metrics| {
3352 let mut output = Vec::with_capacity(iter.size_hint().0);
3353 for item in iter {
3354 let (left, right) = split(item);
3355 if out0_to_left {
3356 output.push((left, right));
3357 } else {
3358 output.push((right, left));
3359 }
3360 }
3361 downcast_output_vec(output, "unzip-zip")
3362 }));
3363 }
3364
3365 Some(Box::new(move |iter, metrics| {
3366 let mut output = Vec::with_capacity(iter.size_hint().0);
3367 let enabled = metrics.enabled();
3368 let timing = enabled && metrics.timing_enabled();
3369 if !timing {
3370 for item in iter {
3371 let (left, right) = split(item);
3372 if out0_to_left {
3373 output.push((left, right));
3374 } else {
3375 output.push((right, left));
3376 }
3377 }
3378 let elements = output.len() as u64;
3379 metrics.complete_exact_counts(
3380 unzip_idx,
3381 MetricUnits::scalar(elements),
3382 MetricUnits::scalar(elements.saturating_mul(2)),
3383 );
3384 metrics.complete_exact_counts(
3385 zip_idx,
3386 MetricUnits::scalar(elements.saturating_mul(2)),
3387 MetricUnits::scalar(elements),
3388 );
3389 return downcast_output_vec(output, "unzip-zip");
3390 }
3391 let started = Instant::now();
3392 for item in iter {
3393 let (left, right) = split(item);
3394 if out0_to_left {
3395 output.push((left, right));
3396 } else {
3397 output.push((right, left));
3398 }
3399 }
3400 let elapsed = started.elapsed();
3401 let elements = output.len() as u64;
3402 metrics.complete_exact_counts(
3403 unzip_idx,
3404 MetricUnits::scalar(elements),
3405 MetricUnits::scalar(elements.saturating_mul(2)),
3406 );
3407 metrics.complete_exact_counts(
3408 zip_idx,
3409 MetricUnits::scalar(elements.saturating_mul(2)),
3410 MetricUnits::scalar(elements),
3411 );
3412 metrics.record_processing_time_share(&[unzip_idx, zip_idx], elapsed);
3413 downcast_output_vec(output, "unzip-zip")
3414 }))
3415}
3416
3417fn try_build_typed_merge_sorted_dispatch<In, Out>(
3418 stages: &[super::builder::StageRecord],
3419 edges: &[super::builder::Edge],
3420 graph_inlet: PortId,
3421 graph_outlet: PortId,
3422 metrics_enabled: bool,
3423) -> Option<AcyclicJunctionRunner<In, Out>>
3424where
3425 In: Clone + Send + 'static,
3426 Out: Send + 'static,
3427{
3428 let elem_type_id = stages.iter().find_map(|stage| {
3429 if let StageKind::MergeSorted(_) = stage.spec.kind {
3430 let [left, right] = stage.spec.inlets.as_slice() else {
3431 return None;
3432 };
3433 (left.type_id() == right.type_id()).then_some(left.type_id())
3434 } else {
3435 None
3436 }
3437 })?;
3438
3439 macro_rules! try_elem {
3440 ($($T:ty),*) => {
3441 $(
3442 if elem_type_id == TypeId::of::<$T>() {
3443 return try_typed_merge_sorted_runner::<In, $T, Out>(
3444 stages,
3445 edges,
3446 graph_inlet,
3447 graph_outlet,
3448 metrics_enabled,
3449 );
3450 }
3451 )*
3452 };
3453 }
3454
3455 try_elem!(
3456 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, char, String
3457 );
3458 None
3459}
3460
3461fn try_typed_merge_sorted_runner<In, T, Out>(
3462 stages: &[super::builder::StageRecord],
3463 edges: &[super::builder::Edge],
3464 graph_inlet: PortId,
3465 graph_outlet: PortId,
3466 metrics_enabled: bool,
3467) -> Option<AcyclicJunctionRunner<In, Out>>
3468where
3469 In: Clone + Send + 'static,
3470 T: Ord + Send + 'static,
3471 Out: Send + 'static,
3472{
3473 if stages.len() != 2 || edges.len() != 2 || TypeId::of::<Out>() != TypeId::of::<T>() {
3474 return None;
3475 }
3476 let in_type = TypeId::of::<In>();
3477 let elem_type = TypeId::of::<T>();
3478
3479 let (unzip_idx, unzip_stage) = stage_with_graph_inlet(stages, graph_inlet)?;
3480 let typed_split = match &unzip_stage.spec.kind {
3481 StageKind::Unzip { typed_split, .. } => Arc::clone(typed_split),
3482 _ => return None,
3483 };
3484 let (merge_idx, merge_stage) = other_stage(stages, unzip_idx)?;
3485 if !matches!(merge_stage.spec.kind, StageKind::MergeSorted(_)) {
3486 return None;
3487 }
3488
3489 if unzip_stage.spec.inlets.len() != 1
3490 || unzip_stage.spec.outlets.len() != 2
3491 || merge_stage.spec.inlets.len() != 2
3492 || merge_stage.spec.outlets.len() != 1
3493 || merge_stage.spec.outlets[0].id() != graph_outlet
3494 || unzip_stage.spec.inlets[0].type_id() != in_type
3495 || merge_stage.spec.outlets[0].type_id() != elem_type
3496 || unzip_stage
3497 .spec
3498 .outlets
3499 .iter()
3500 .any(|outlet| outlet.type_id() != elem_type)
3501 || merge_stage
3502 .spec
3503 .inlets
3504 .iter()
3505 .any(|inlet| inlet.type_id() != elem_type)
3506 {
3507 return None;
3508 }
3509 let mapping = outlets_cover_inlets(edges, &unzip_stage.spec.outlets, &merge_stage.spec.inlets)?;
3510 let out0_to_left = mapping.first().copied()? == 0;
3511
3512 let split = typed_split.downcast_ref::<Arc<dyn Fn(In) -> (T, T) + Send + Sync>>()?;
3513 let split = Arc::clone(split);
3514
3515 if !metrics_enabled {
3516 return Some(Box::new(move |iter, _metrics| {
3517 let mut left = VecDeque::new();
3518 let mut right = VecDeque::new();
3519 let mut output = Vec::with_capacity(iter.size_hint().0.saturating_mul(2));
3520 for item in iter {
3521 let (first, second) = split(item);
3522 if out0_to_left {
3523 left.push_back(first);
3524 right.push_back(second);
3525 } else {
3526 left.push_back(second);
3527 right.push_back(first);
3528 }
3529 drain_merge_sorted(&mut left, &mut right, false, false, &mut output);
3530 }
3531 drain_merge_sorted(&mut left, &mut right, true, true, &mut output);
3532 downcast_output_vec(output, "merge-sorted")
3533 }));
3534 }
3535
3536 Some(Box::new(move |iter, metrics| {
3537 let mut left = VecDeque::new();
3538 let mut right = VecDeque::new();
3539 let mut output = Vec::with_capacity(iter.size_hint().0.saturating_mul(2));
3540 for item in iter {
3541 let unzip = metrics.begin_node(unzip_idx, MetricUnits::scalar(1));
3542 let (first, second) = split(item);
3543 metrics.end_node(unzip, MetricUnits::scalar(2));
3544 if out0_to_left {
3545 left.push_back(first);
3546 right.push_back(second);
3547 } else {
3548 left.push_back(second);
3549 right.push_back(first);
3550 }
3551 let merge = metrics.begin_node(merge_idx, MetricUnits::scalar(2));
3552 let before = output.len();
3553 drain_merge_sorted(&mut left, &mut right, false, false, &mut output);
3554 metrics.end_node(merge, MetricUnits::scalar((output.len() - before) as u64));
3555 }
3556 let merge = metrics.begin_node(merge_idx, MetricUnits::scalar(0));
3557 let before = output.len();
3558 drain_merge_sorted(&mut left, &mut right, true, true, &mut output);
3559 metrics.end_node(merge, MetricUnits::scalar((output.len() - before) as u64));
3560 downcast_output_vec(output, "merge-sorted")
3561 }))
3562}
3563
3564fn drain_merge_sorted<T: Ord>(
3565 left: &mut VecDeque<T>,
3566 right: &mut VecDeque<T>,
3567 left_closed: bool,
3568 right_closed: bool,
3569 output: &mut Vec<T>,
3570) {
3571 loop {
3572 let next = match (left.front(), right.front()) {
3573 (Some(left_item), Some(right_item)) => {
3574 if left_item <= right_item {
3575 left.pop_front()
3576 } else {
3577 right.pop_front()
3578 }
3579 }
3580 (Some(_), None) if right_closed => left.pop_front(),
3581 (None, Some(_)) if left_closed => right.pop_front(),
3582 _ => None,
3583 };
3584 let Some(item) = next else {
3585 break;
3586 };
3587 output.push(item);
3588 }
3589}
3590
3591pub(super) enum BoundaryCountExecutor {
3594 #[cfg(test)]
3595 Threaded,
3596 Ractor,
3597}
3598
3599impl BoundaryCountExecutor {
3600 pub(super) fn run_count<I, T>(
3601 &self,
3602 input: I,
3603 segments: TypedLinearSegments<T>,
3604 config: AsyncBoundaryExecutionConfig,
3605 ) -> StreamResult<FusedTerminalReport<usize>>
3606 where
3607 I: IntoIterator<Item = T> + Send,
3608 I::IntoIter: Send + 'static,
3609 T: Send + 'static,
3610 {
3611 match self {
3612 #[cfg(test)]
3613 Self::Threaded => run_threaded_async_linear_count(input, segments, config),
3614 Self::Ractor => run_ractor_async_linear_count(input, segments, config),
3615 }
3616 }
3617}
3618
3619#[cfg(test)]
3620mod tests {
3621 use super::*;
3622 use crate::Attributes;
3623 use std::time::Duration;
3624
3625 fn execution_observation<T>(report: FusedExecutionReport<T>) -> (Vec<T>, usize, usize) {
3626 (
3627 report.output,
3628 report.events,
3629 report.async_boundary_crossings,
3630 )
3631 }
3632
3633 fn assert_execution_equivalent<T>(
3634 left: &FusedExecutionReport<T>,
3635 right: &FusedExecutionReport<T>,
3636 context: &str,
3637 ) where
3638 T: PartialEq + std::fmt::Debug,
3639 {
3640 assert_eq!(left.output, right.output, "{context}: output differs");
3641 assert_eq!(left.events, right.events, "{context}: event count differs");
3642 assert_eq!(
3643 left.async_boundary_crossings, right.async_boundary_crossings,
3644 "{context}: async-boundary crossings differ"
3645 );
3646 }
3647
3648 #[derive(Default)]
3649 struct BufferedFlowState {
3650 queued: VecDeque<i32>,
3651 upstream_closed: bool,
3652 pull_calls: usize,
3653 finish_calls: usize,
3654 }
3655
3656 struct BufferedFlowOnPull {
3657 state: Arc<Mutex<BufferedFlowState>>,
3658 }
3659
3660 impl GraphStage for BufferedFlowOnPull {
3661 type Shape = FlowShape<i32, i32>;
3662
3663 fn name(&self) -> &str {
3664 "BufferedFlowOnPull"
3665 }
3666
3667 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3668 let first_id = next_port_id_block(2);
3669 FlowShape::new(
3670 Inlet::with_id(first_id, "buffered-flow.in"),
3671 Outlet::with_id(first_id.offset(1), "buffered-flow.out"),
3672 )
3673 }
3674
3675 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3676 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3677 }
3678
3679 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3680 struct In {
3681 state: Arc<Mutex<BufferedFlowState>>,
3682 }
3683
3684 impl InHandler for In {
3685 fn on_push(
3686 &mut self,
3687 logic: &mut GraphStageLogic,
3688 inlet: AnyInlet,
3689 ) -> StreamResult<()> {
3690 let value: i32 = logic.grab_datum(inlet.id()).and_then(|value| {
3691 downcast_datum(value, "grab", || format!("inlet#{}", inlet.id().as_usize()))
3692 })?;
3693 self.state.lock().unwrap().queued.push_back(value);
3694 Ok(())
3695 }
3696
3697 fn on_upstream_finish(
3698 &mut self,
3699 _logic: &mut GraphStageLogic,
3700 _inlet: AnyInlet,
3701 ) -> StreamResult<()> {
3702 self.state.lock().unwrap().upstream_closed = true;
3703 Ok(())
3704 }
3705 }
3706
3707 struct Out {
3708 outlet: Outlet<i32>,
3709 state: Arc<Mutex<BufferedFlowState>>,
3710 }
3711
3712 impl OutHandler for Out {
3713 fn on_pull(
3714 &mut self,
3715 logic: &mut GraphStageLogic,
3716 _outlet: AnyOutlet,
3717 ) -> StreamResult<()> {
3718 let (next, upstream_closed) = {
3719 let mut state = self.state.lock().unwrap();
3720 state.pull_calls += 1;
3721 (state.queued.pop_front(), state.upstream_closed)
3722 };
3723 if let Some(value) = next {
3724 logic.emit(&self.outlet, value)
3725 } else if upstream_closed {
3726 logic.complete(&self.outlet)
3727 } else {
3728 Ok(())
3729 }
3730 }
3731
3732 fn on_downstream_finish(
3733 &mut self,
3734 logic: &mut GraphStageLogic,
3735 _outlet: AnyOutlet,
3736 ) -> StreamResult<()> {
3737 self.state.lock().unwrap().finish_calls += 1;
3738 logic.complete_stage()
3739 }
3740 }
3741
3742 let mut logic = GraphStageLogic::new(shape);
3743 logic
3744 .set_handler(
3745 &shape.inlet(),
3746 Box::new(In {
3747 state: Arc::clone(&self.state),
3748 }),
3749 )
3750 .unwrap();
3751 logic
3752 .set_out_handler(
3753 &shape.outlet(),
3754 Box::new(Out {
3755 outlet: shape.outlet(),
3756 state: Arc::clone(&self.state),
3757 }),
3758 )
3759 .unwrap();
3760 logic
3761 }
3762 }
3763
3764 struct EmitMultipleThenFailOnPush;
3765
3766 impl GraphStage for EmitMultipleThenFailOnPush {
3767 type Shape = FlowShape<i32, i32>;
3768
3769 fn name(&self) -> &str {
3770 "EmitMultipleThenFailOnPush"
3771 }
3772
3773 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3774 let first_id = next_port_id_block(2);
3775 FlowShape::new(
3776 Inlet::with_id(first_id, "emit-fail.in"),
3777 Outlet::with_id(first_id.offset(1), "emit-fail.out"),
3778 )
3779 }
3780
3781 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3782 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3783 }
3784
3785 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3786 struct Handler {
3787 outlet: Outlet<i32>,
3788 }
3789
3790 impl InHandler for Handler {
3791 fn on_push(
3792 &mut self,
3793 logic: &mut GraphStageLogic,
3794 _inlet: AnyInlet,
3795 ) -> StreamResult<()> {
3796 logic.emit_multiple(&self.outlet, [1, 2])?;
3797 Err(StreamError::Failed("emit_multiple boom".into()))
3798 }
3799 }
3800
3801 let mut logic = GraphStageLogic::new(shape);
3802 logic
3803 .set_handler(
3804 &shape.inlet(),
3805 Box::new(Handler {
3806 outlet: shape.outlet(),
3807 }),
3808 )
3809 .unwrap();
3810 logic
3811 }
3812 }
3813
3814 struct ReadNThenFailOnFinish;
3815
3816 struct EmitMultipleOnPush;
3817
3818 impl GraphStage for EmitMultipleOnPush {
3819 type Shape = FlowShape<i32, i32>;
3820
3821 fn name(&self) -> &str {
3822 "EmitMultipleOnPush"
3823 }
3824
3825 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3826 let first_id = next_port_id_block(2);
3827 FlowShape::new(
3828 Inlet::with_id(first_id, "emit-multiple.in"),
3829 Outlet::with_id(first_id.offset(1), "emit-multiple.out"),
3830 )
3831 }
3832
3833 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3834 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3835 }
3836
3837 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3838 struct Handler {
3839 outlet: Outlet<i32>,
3840 }
3841
3842 impl InHandler for Handler {
3843 fn on_push(
3844 &mut self,
3845 logic: &mut GraphStageLogic,
3846 _inlet: AnyInlet,
3847 ) -> StreamResult<()> {
3848 logic.emit_multiple(&self.outlet, [1, 2])
3849 }
3850 }
3851
3852 let mut logic = GraphStageLogic::new(shape);
3853 logic
3854 .set_handler(
3855 &shape.inlet(),
3856 Box::new(Handler {
3857 outlet: shape.outlet(),
3858 }),
3859 )
3860 .unwrap();
3861 logic
3862 }
3863 }
3864
3865 impl GraphStage for ReadNThenFailOnFinish {
3866 type Shape = FlowShape<i32, i32>;
3867
3868 fn name(&self) -> &str {
3869 "ReadNThenFailOnFinish"
3870 }
3871
3872 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
3873 let first_id = next_port_id_block(2);
3874 FlowShape::new(
3875 Inlet::with_id(first_id, "read-n.in"),
3876 Outlet::with_id(first_id.offset(1), "read-n.out"),
3877 )
3878 }
3879
3880 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
3881 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
3882 }
3883
3884 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
3885 struct Handler {
3886 inlet: Inlet<i32>,
3887 armed: bool,
3888 }
3889
3890 impl InHandler for Handler {
3891 fn on_push(
3892 &mut self,
3893 logic: &mut GraphStageLogic,
3894 _inlet: AnyInlet,
3895 ) -> StreamResult<()> {
3896 if !self.armed {
3897 self.armed = true;
3898 logic.read_n(&self.inlet, 2, |_values| {}, |_values| {})
3899 } else {
3900 Ok(())
3901 }
3902 }
3903
3904 fn on_upstream_finish(
3905 &mut self,
3906 _logic: &mut GraphStageLogic,
3907 _inlet: AnyInlet,
3908 ) -> StreamResult<()> {
3909 Err(StreamError::Failed("read_n finish boom".into()))
3910 }
3911 }
3912
3913 let mut logic = GraphStageLogic::new(shape);
3914 logic
3915 .set_handler(
3916 &shape.inlet(),
3917 Box::new(Handler {
3918 inlet: shape.inlet(),
3919 armed: false,
3920 }),
3921 )
3922 .unwrap();
3923 logic
3924 }
3925 }
3926
3927 fn single_opaque_stage_graph<G>(stage: G) -> GraphBlueprint<FlowShape<i32, i32>>
3928 where
3929 G: GraphStage<Shape = FlowShape<i32, i32>>,
3930 {
3931 GraphDsl::create(|builder| builder.add(stage)).unwrap()
3932 }
3933
3934 #[test]
3935 fn fusion_typed_only_errors_on_opaque_topology_with_node_name() {
3936 let graph = single_opaque_stage_graph(BufferedFlowOnPull {
3937 state: Arc::new(Mutex::new(BufferedFlowState::default())),
3938 })
3939 .add_attributes(Attributes::named("opaque-node"))
3940 .add_attributes(Attributes::fusion(Fusion::TypedOnly));
3941
3942 let result = graph.run_with_input_report([1], FusedExecutionConfig::default());
3943
3944 assert!(
3945 matches!(
3946 result,
3947 Err(StreamError::GraphValidation(ref message))
3948 if message.contains("typed executor does not support this graph shape")
3949 && message.contains("opaque-node")
3950 ),
3951 "expected readable typed-only diagnostic, got {result:?}"
3952 );
3953 }
3954
3955 #[test]
3956 fn auto_erased_fallback_is_reported_for_opaque_topology() {
3957 let graph = single_opaque_stage_graph(EmitMultipleOnPush);
3958
3959 let report = graph
3960 .run_with_input_report([1], FusedExecutionConfig::default())
3961 .unwrap();
3962
3963 assert_eq!(report.output, vec![1, 2]);
3964 assert_eq!(report.plan.selected_tier, FusedExecutorTier::Erased);
3965 assert_eq!(report.plan.tier_changes.len(), 1);
3966 let change = &report.plan.tier_changes[0];
3967 assert_eq!(
3968 change.reason,
3969 FusedTierChangeReason::AutoTypedUnsupportedFallback
3970 );
3971 assert_eq!(change.selected_tier, FusedExecutorTier::Erased);
3972 assert!(
3973 change
3974 .affected_nodes
3975 .iter()
3976 .any(|node| node.contains("EmitMultipleOnPush")),
3977 "expected fallback report to name opaque node, got {change:?}"
3978 );
3979 }
3980
3981 fn run_flow_with_timeout(
3982 graph: GraphBlueprint<FlowShape<i32, i32>>,
3983 input: Vec<i32>,
3984 ) -> StreamResult<Vec<i32>> {
3985 let (tx, rx) = mpsc::channel();
3986 thread::spawn(move || {
3987 tx.send(graph.run_with_input(input))
3988 .expect("test receiver is alive");
3989 });
3990 rx.recv_timeout(Duration::from_secs(2))
3991 .expect("graph run completed before timeout")
3992 }
3993
3994 #[derive(Default)]
3995 struct OneShotTimerChecks {
3996 inactive_before_schedule: bool,
3997 active_after_schedule: bool,
3998 inactive_on_timer: bool,
3999 }
4000
4001 struct OneShotTimerStage {
4002 checks: Arc<Mutex<OneShotTimerChecks>>,
4003 }
4004
4005 impl GraphStage for OneShotTimerStage {
4006 type Shape = FlowShape<i32, i32>;
4007
4008 fn name(&self) -> &str {
4009 "OneShotTimerStage"
4010 }
4011
4012 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
4013 let first_id = next_port_id_block(2);
4014 FlowShape::new(
4015 Inlet::with_id(first_id, "one-shot-timer.in"),
4016 Outlet::with_id(first_id.offset(1), "one-shot-timer.out"),
4017 )
4018 }
4019
4020 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
4021 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
4022 }
4023
4024 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
4025 struct Out {
4026 armed: bool,
4027 checks: Arc<Mutex<OneShotTimerChecks>>,
4028 }
4029
4030 impl OutHandler for Out {
4031 fn on_pull(
4032 &mut self,
4033 logic: &mut GraphStageLogic,
4034 _outlet: AnyOutlet,
4035 ) -> StreamResult<()> {
4036 if self.armed {
4037 return Ok(());
4038 }
4039 self.armed = true;
4040 {
4041 let mut checks = self.checks.lock().unwrap();
4042 checks.inactive_before_schedule = !logic.is_timer_active("once");
4043 }
4044 logic.schedule_once("once", Duration::from_millis(1))?;
4045 self.checks.lock().unwrap().active_after_schedule =
4046 logic.is_timer_active("once");
4047 Ok(())
4048 }
4049 }
4050
4051 struct Timer {
4052 outlet: Outlet<i32>,
4053 checks: Arc<Mutex<OneShotTimerChecks>>,
4054 }
4055
4056 impl TimerHandler for Timer {
4057 fn on_timer(&mut self, logic: &mut GraphStageLogic, key: &str) -> StreamResult<()> {
4058 assert_eq!(key, "once");
4059 self.checks.lock().unwrap().inactive_on_timer = !logic.is_timer_active("once");
4060 logic.push(&self.outlet, 42)?;
4061 logic.complete(&self.outlet)
4062 }
4063 }
4064
4065 let mut logic = GraphStageLogic::new(shape);
4066 logic
4067 .set_out_handler(
4068 &shape.outlet(),
4069 Box::new(Out {
4070 armed: false,
4071 checks: Arc::clone(&self.checks),
4072 }),
4073 )
4074 .unwrap();
4075 logic.set_timer_handler(Box::new(Timer {
4076 outlet: shape.outlet(),
4077 checks: Arc::clone(&self.checks),
4078 }));
4079 logic
4080 }
4081 }
4082
4083 #[derive(Default)]
4084 struct PeriodicTimerChecks {
4085 active_after_schedule: bool,
4086 active_on_first_tick: bool,
4087 inactive_after_cancel: bool,
4088 ticks: usize,
4089 }
4090
4091 struct PeriodicTimerStage {
4092 checks: Arc<Mutex<PeriodicTimerChecks>>,
4093 }
4094
4095 impl GraphStage for PeriodicTimerStage {
4096 type Shape = FlowShape<i32, i32>;
4097
4098 fn name(&self) -> &str {
4099 "PeriodicTimerStage"
4100 }
4101
4102 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
4103 let first_id = next_port_id_block(2);
4104 FlowShape::new(
4105 Inlet::with_id(first_id, "periodic-timer.in"),
4106 Outlet::with_id(first_id.offset(1), "periodic-timer.out"),
4107 )
4108 }
4109
4110 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
4111 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
4112 }
4113
4114 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
4115 struct Out {
4116 armed: bool,
4117 checks: Arc<Mutex<PeriodicTimerChecks>>,
4118 }
4119
4120 impl OutHandler for Out {
4121 fn on_pull(
4122 &mut self,
4123 logic: &mut GraphStageLogic,
4124 _outlet: AnyOutlet,
4125 ) -> StreamResult<()> {
4126 if self.armed {
4127 return Ok(());
4128 }
4129 self.armed = true;
4130 logic.schedule_periodically_with_initial_delay(
4131 "periodic",
4132 Duration::ZERO,
4133 Duration::from_millis(2),
4134 )?;
4135 self.checks.lock().unwrap().active_after_schedule =
4136 logic.is_timer_active("periodic");
4137 Ok(())
4138 }
4139 }
4140
4141 struct Timer {
4142 outlet: Outlet<i32>,
4143 checks: Arc<Mutex<PeriodicTimerChecks>>,
4144 }
4145
4146 impl TimerHandler for Timer {
4147 fn on_timer(&mut self, logic: &mut GraphStageLogic, key: &str) -> StreamResult<()> {
4148 assert_eq!(key, "periodic");
4149 let tick = {
4150 let mut checks = self.checks.lock().unwrap();
4151 checks.ticks += 1;
4152 if checks.ticks == 1 {
4153 checks.active_on_first_tick = logic.is_timer_active("periodic");
4154 }
4155 checks.ticks
4156 };
4157 logic.push(&self.outlet, tick as i32)?;
4158 if tick == 3 {
4159 assert!(logic.cancel_timer("periodic"));
4160 self.checks.lock().unwrap().inactive_after_cancel =
4161 !logic.is_timer_active("periodic");
4162 logic.complete(&self.outlet)?;
4163 }
4164 Ok(())
4165 }
4166 }
4167
4168 let mut logic = GraphStageLogic::new(shape);
4169 logic
4170 .set_out_handler(
4171 &shape.outlet(),
4172 Box::new(Out {
4173 armed: false,
4174 checks: Arc::clone(&self.checks),
4175 }),
4176 )
4177 .unwrap();
4178 logic.set_timer_handler(Box::new(Timer {
4179 outlet: shape.outlet(),
4180 checks: Arc::clone(&self.checks),
4181 }));
4182 logic
4183 }
4184 }
4185
4186 struct CompletingTimerStage {
4187 fired: Arc<AtomicUsize>,
4188 }
4189
4190 impl GraphStage for CompletingTimerStage {
4191 type Shape = FlowShape<i32, i32>;
4192
4193 fn name(&self) -> &str {
4194 "CompletingTimerStage"
4195 }
4196
4197 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
4198 let first_id = next_port_id_block(2);
4199 FlowShape::new(
4200 Inlet::with_id(first_id, "completing-timer.in"),
4201 Outlet::with_id(first_id.offset(1), "completing-timer.out"),
4202 )
4203 }
4204
4205 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
4206 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
4207 }
4208
4209 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
4210 struct Out {
4211 outlet: Outlet<i32>,
4212 }
4213
4214 impl OutHandler for Out {
4215 fn on_pull(
4216 &mut self,
4217 logic: &mut GraphStageLogic,
4218 _outlet: AnyOutlet,
4219 ) -> StreamResult<()> {
4220 logic.schedule_once("late", Duration::from_millis(1))?;
4221 logic.complete(&self.outlet)
4222 }
4223 }
4224
4225 struct Timer {
4226 fired: Arc<AtomicUsize>,
4227 }
4228
4229 impl TimerHandler for Timer {
4230 fn on_timer(
4231 &mut self,
4232 _logic: &mut GraphStageLogic,
4233 _key: &str,
4234 ) -> StreamResult<()> {
4235 self.fired.fetch_add(1, Ordering::SeqCst);
4236 Ok(())
4237 }
4238 }
4239
4240 let mut logic = GraphStageLogic::new(shape);
4241 logic
4242 .set_out_handler(
4243 &shape.outlet(),
4244 Box::new(Out {
4245 outlet: shape.outlet(),
4246 }),
4247 )
4248 .unwrap();
4249 logic.set_timer_handler(Box::new(Timer {
4250 fired: Arc::clone(&self.fired),
4251 }));
4252 logic
4253 }
4254 }
4255
4256 struct FailingTimerStage {
4257 fired: Arc<AtomicUsize>,
4258 }
4259
4260 impl GraphStage for FailingTimerStage {
4261 type Shape = FlowShape<i32, i32>;
4262
4263 fn name(&self) -> &str {
4264 "FailingTimerStage"
4265 }
4266
4267 fn allocate_shape(&self, _allocator: &mut PortAllocator) -> Self::Shape {
4268 let first_id = next_port_id_block(2);
4269 FlowShape::new(
4270 Inlet::with_id(first_id, "failing-timer.in"),
4271 Outlet::with_id(first_id.offset(1), "failing-timer.out"),
4272 )
4273 }
4274
4275 fn stage_spec(&self, shape: &Self::Shape) -> StageSpec {
4276 StageSpec::opaque(self.name(), shape.inlets(), shape.outlets())
4277 }
4278
4279 fn create_logic(&self, shape: &Self::Shape) -> GraphStageLogic {
4280 struct Out;
4281
4282 impl OutHandler for Out {
4283 fn on_pull(
4284 &mut self,
4285 logic: &mut GraphStageLogic,
4286 _outlet: AnyOutlet,
4287 ) -> StreamResult<()> {
4288 logic.schedule_once("late", Duration::from_millis(1))?;
4289 logic.fail_stage(StreamError::Failed("timer stage failed".into()))
4290 }
4291 }
4292
4293 struct Timer {
4294 fired: Arc<AtomicUsize>,
4295 }
4296
4297 impl TimerHandler for Timer {
4298 fn on_timer(
4299 &mut self,
4300 _logic: &mut GraphStageLogic,
4301 _key: &str,
4302 ) -> StreamResult<()> {
4303 self.fired.fetch_add(1, Ordering::SeqCst);
4304 Ok(())
4305 }
4306 }
4307
4308 let mut logic = GraphStageLogic::new(shape);
4309 logic
4310 .set_out_handler(&shape.outlet(), Box::new(Out))
4311 .unwrap();
4312 logic.set_timer_handler(Box::new(Timer {
4313 fired: Arc::clone(&self.fired),
4314 }));
4315 logic
4316 }
4317 }
4318
4319 #[test]
4320 fn graph_stage_logic_one_shot_timer_fires_on_executor_thread() {
4321 let checks = Arc::new(Mutex::new(OneShotTimerChecks::default()));
4322 let graph = single_opaque_stage_graph(OneShotTimerStage {
4323 checks: Arc::clone(&checks),
4324 });
4325
4326 let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
4327
4328 assert_eq!(output, vec![42]);
4329 let checks = checks.lock().unwrap();
4330 assert!(checks.inactive_before_schedule);
4331 assert!(checks.active_after_schedule);
4332 assert!(checks.inactive_on_timer);
4333 }
4334
4335 #[test]
4336 fn graph_stage_logic_periodic_timer_fires_until_cancelled() {
4337 let checks = Arc::new(Mutex::new(PeriodicTimerChecks::default()));
4338 let graph = single_opaque_stage_graph(PeriodicTimerStage {
4339 checks: Arc::clone(&checks),
4340 });
4341
4342 let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
4343
4344 assert_eq!(output, vec![1, 2, 3]);
4345 let checks = checks.lock().unwrap();
4346 assert_eq!(checks.ticks, 3);
4347 assert!(checks.active_after_schedule);
4348 assert!(checks.active_on_first_tick);
4349 assert!(checks.inactive_after_cancel);
4350 }
4351
4352 #[test]
4353 fn graph_stage_logic_cancels_timers_when_stage_completes() {
4354 let fired = Arc::new(AtomicUsize::new(0));
4355 let graph = single_opaque_stage_graph(CompletingTimerStage {
4356 fired: Arc::clone(&fired),
4357 });
4358
4359 let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
4360
4361 assert!(output.is_empty());
4362 assert_eq!(fired.load(Ordering::SeqCst), 0);
4363 }
4364
4365 #[test]
4366 fn graph_stage_logic_cancels_timers_when_stage_fails() {
4367 let fired = Arc::new(AtomicUsize::new(0));
4368 let graph = single_opaque_stage_graph(FailingTimerStage {
4369 fired: Arc::clone(&fired),
4370 });
4371
4372 let output = run_flow_with_timeout(graph, Vec::new()).unwrap();
4373
4374 assert!(output.is_empty());
4375 assert_eq!(fired.load(Ordering::SeqCst), 0);
4376 }
4377
4378 #[test]
4379 fn process_push_restores_handler_before_emit_multiple_error_propagates() {
4380 let graph = single_opaque_stage_graph(EmitMultipleThenFailOnPush);
4381 let inlet = graph.shape.inlet().id();
4382 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4383
4384 let result = executor.process_stage(0, inlet, datum(10));
4385
4386 assert!(matches!(
4387 result,
4388 Err(StreamError::Failed(message)) if message == "emit_multiple boom"
4389 ));
4390 assert!(
4391 executor.opaque_logics[0]
4392 .as_mut()
4393 .unwrap()
4394 .get_in_handler_mut(inlet)
4395 .is_some()
4396 );
4397 }
4398
4399 #[test]
4400 fn process_completion_restores_handler_before_read_n_finish_error_propagates() {
4401 let graph = single_opaque_stage_graph(ReadNThenFailOnFinish);
4402 let inlet = graph.shape.inlet().id();
4403 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4404
4405 executor.process_stage(0, inlet, datum(1)).unwrap();
4406 executor.process_stage(0, inlet, datum(2)).unwrap();
4407 let result = executor.process_completion(0, inlet);
4408
4409 assert!(matches!(
4410 result,
4411 Err(StreamError::Failed(message)) if message == "read_n finish boom"
4412 ));
4413 assert!(
4414 executor.opaque_logics[0]
4415 .as_mut()
4416 .unwrap()
4417 .get_in_handler_mut(inlet)
4418 .is_some()
4419 );
4420 }
4421
4422 #[test]
4423 fn opaque_request_drives_out_handler_for_buffered_output() {
4424 let state = Arc::new(Mutex::new(BufferedFlowState::default()));
4425 let graph = single_opaque_stage_graph(BufferedFlowOnPull {
4426 state: Arc::clone(&state),
4427 });
4428 let inlet = graph.shape.inlet().id();
4429 let outlet = graph.shape.outlet().id();
4430 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4431 let mut output = Vec::<i32>::new();
4432 let mut output_sink = VecOutputSink {
4433 output: &mut output,
4434 };
4435
4436 executor
4437 .deliver(inlet, datum(7_i32), outlet, &mut output_sink)
4438 .unwrap();
4439 assert!(output_sink.output.is_empty());
4440
4441 executor.request(outlet, outlet, &mut output_sink).unwrap();
4442
4443 assert_eq!(&*output_sink.output, &[7]);
4444 let state = state.lock().unwrap();
4445 assert_eq!(state.pull_calls, 2);
4446 assert_eq!(state.finish_calls, 0);
4447 }
4448
4449 #[test]
4450 fn opaque_downstream_finish_before_first_demand_invokes_out_handler() {
4451 let state = Arc::new(Mutex::new(BufferedFlowState::default()));
4452 let graph = single_opaque_stage_graph(BufferedFlowOnPull {
4453 state: Arc::clone(&state),
4454 });
4455 let outlet = graph.shape.outlet().id();
4456 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4457 let mut output = Vec::<i32>::new();
4458 let mut output_sink = VecOutputSink {
4459 output: &mut output,
4460 };
4461
4462 executor
4463 .downstream_finish(outlet, outlet, &mut output_sink)
4464 .unwrap();
4465 executor.request(outlet, outlet, &mut output_sink).unwrap();
4466
4467 assert!(output_sink.output.is_empty());
4468 let state = state.lock().unwrap();
4469 assert_eq!(state.pull_calls, 0);
4470 assert_eq!(state.finish_calls, 1);
4471 }
4472
4473 #[test]
4474 fn opaque_downstream_finish_drops_buffered_output_after_upstream_complete() {
4475 let state = Arc::new(Mutex::new(BufferedFlowState::default()));
4476 let graph = single_opaque_stage_graph(BufferedFlowOnPull {
4477 state: Arc::clone(&state),
4478 });
4479 let inlet = graph.shape.inlet().id();
4480 let outlet = graph.shape.outlet().id();
4481 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4482 let mut output = Vec::<i32>::new();
4483 let mut output_sink = VecOutputSink {
4484 output: &mut output,
4485 };
4486
4487 executor
4488 .deliver(inlet, datum(11_i32), outlet, &mut output_sink)
4489 .unwrap();
4490 executor.complete(inlet, outlet, &mut output_sink).unwrap();
4491 executor
4492 .downstream_finish(outlet, outlet, &mut output_sink)
4493 .unwrap();
4494 executor.request(outlet, outlet, &mut output_sink).unwrap();
4495
4496 assert!(output_sink.output.is_empty());
4497 let state = state.lock().unwrap();
4498 assert_eq!(state.finish_calls, 1);
4499 }
4500
4501 #[test]
4502 fn broadcast_cancels_upstream_only_after_all_outlets_cancel() {
4503 let graph = GraphDsl::try_create(|builder| {
4504 let broadcast = builder.add(Broadcast::<i32>::new(2));
4505 let merge = builder.add(Merge::<i32>::new(2));
4506 builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
4507 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
4508 Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
4509 })
4510 .unwrap();
4511
4512 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4513 let broadcast_index = *executor
4514 .stage_by_inlet
4515 .get(&graph.shape.inlet().id())
4516 .unwrap();
4517 let first = graph.stages[broadcast_index].spec.outlets[0].id();
4518 let second = graph.stages[broadcast_index].spec.outlets[1].id();
4519
4520 let first_transition = executor
4521 .process_downstream_finish(broadcast_index, first)
4522 .unwrap();
4523 assert!(first_transition.cancelled_inlets.is_empty());
4524
4525 let second_transition = executor
4526 .process_downstream_finish(broadcast_index, second)
4527 .unwrap();
4528 assert_eq!(
4529 second_transition.cancelled_inlets,
4530 vec![graph.stages[broadcast_index].spec.inlets[0].id()]
4531 );
4532 }
4533
4534 #[test]
4535 fn downstream_finish_propagates_through_merge_and_broadcast() {
4536 let graph = GraphDsl::try_create(|builder| {
4537 let broadcast = builder.add(Broadcast::<i32>::new(2));
4538 let merge = builder.add(Merge::<i32>::new(2));
4539 builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
4540 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
4541 Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
4542 })
4543 .unwrap();
4544
4545 let outlet = graph.shape.outlet().id();
4546 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4547 let mut output = Vec::<i32>::new();
4548 let mut output_sink = VecOutputSink {
4549 output: &mut output,
4550 };
4551
4552 executor
4553 .downstream_finish(outlet, outlet, &mut output_sink)
4554 .unwrap();
4555
4556 let broadcast_index = *executor
4557 .stage_by_inlet
4558 .get(&graph.shape.inlet().id())
4559 .unwrap();
4560 let StageState::Broadcast {
4561 live_outlets,
4562 cancelled_outlets,
4563 ..
4564 } = &executor.stage_states[broadcast_index]
4565 else {
4566 panic!("expected broadcast state");
4567 };
4568 assert_eq!(*live_outlets, 0);
4569 assert_eq!(cancelled_outlets, &vec![true, true]);
4570 }
4571
4572 #[test]
4573 fn cyclic_graph_clears_pending_events_when_output_cancels() {
4574 struct CancelAfterFirst {
4575 emitted: usize,
4576 }
4577
4578 impl FusedOutputSink<i32> for CancelAfterFirst {
4579 fn emit(&mut self, _value: i32) -> StreamResult<()> {
4580 self.emitted += 1;
4581 Err(StreamError::Cancelled)
4582 }
4583 }
4584
4585 let graph = GraphDsl::try_create(|builder| {
4586 let merge = builder.add(MergePreferred::<i32>::new(1));
4587 let broadcast = builder.add(Broadcast::<i32>::new(2));
4588 let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
4589 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
4590 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4591
4592 builder.connect(merge.outlet(), broadcast.inlet())?;
4593 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
4594 builder.connect(buffer.outlet(), positive.inlet())?;
4595 builder.connect(positive.outlet(), decrement.inlet())?;
4596 builder.connect(decrement.outlet(), merge.preferred())?;
4597
4598 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4599 })
4600 .unwrap();
4601
4602 let graph_outlet = graph.shape.outlet().id();
4603 let graph_inlet = graph.shape.inlet().id();
4604 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4605 let mut output = CancelAfterFirst { emitted: 0 };
4606
4607 executor
4608 .request(graph_outlet, graph_outlet, &mut output)
4609 .unwrap();
4610 let result = executor.deliver(graph_inlet, datum(1_i32), graph_outlet, &mut output);
4611
4612 assert_eq!(result, Err(StreamError::Cancelled));
4613 assert_eq!(output.emitted, 1);
4614 assert!(executor.event_stack.is_empty());
4615 }
4616
4617 const CYCLE_LIMIT: FusedExecutionConfig = FusedExecutionConfig {
4624 event_limit: 5_000_000,
4625 };
4626
4627 fn cyclic_feedback_i32(
4630 buffer_cap: usize,
4631 strategy: OverflowStrategy,
4632 ) -> GraphBlueprint<FlowShape<i32, i32>> {
4633 GraphDsl::try_create(|builder| {
4634 let merge = builder.add(MergePreferred::<i32>::new(1));
4635 let broadcast = builder.add(Broadcast::<i32>::new(2));
4636 let buffer = builder.add(Buffer::<i32>::new(buffer_cap, strategy));
4637 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
4638 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4639
4640 builder.connect(merge.outlet(), broadcast.inlet())?;
4641 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
4642 builder.connect(buffer.outlet(), positive.inlet())?;
4643 builder.connect(positive.outlet(), decrement.inlet())?;
4644 builder.connect(decrement.outlet(), merge.preferred())?;
4645
4646 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4647 })
4648 .unwrap()
4649 }
4650
4651 fn assert_cyclic_equiv_i32(graph: &GraphBlueprint<FlowShape<i32, i32>>, input: Vec<i32>) {
4654 let erased = graph
4655 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
4656 .map(|r| r.output);
4657 let typed = graph
4658 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::TypedOnly)
4659 .map(|r| r.output);
4660 let auto = graph
4661 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
4662 .map(|r| r.output);
4663 assert!(
4664 typed.is_ok(),
4665 "typed cyclic path was not selected for {input:?}: {typed:?}"
4666 );
4667 assert_eq!(erased, typed, "typed != erased for input {input:?}");
4668 assert_eq!(erased, auto, "auto != erased for input {input:?}");
4669 }
4670
4671 #[test]
4672 fn cyclic_typed_matches_erased_single_and_multi_input() {
4673 let graph = cyclic_feedback_i32(16, OverflowStrategy::Backpressure);
4674 for input in [
4675 vec![],
4676 vec![0],
4677 vec![1],
4678 vec![5],
4679 vec![100],
4680 vec![2, 5],
4681 vec![5, 2],
4682 vec![0, 3],
4683 vec![3, 0, 2],
4684 vec![1, 1, 1],
4685 vec![-1],
4686 vec![4, -3, 7],
4687 ] {
4688 assert_cyclic_equiv_i32(&graph, input);
4689 }
4690 }
4691
4692 #[test]
4693 fn cyclic_typed_matches_erased_across_buffer_configs() {
4694 for cap in [1usize, 2, 8, 64] {
4697 for strategy in [
4698 OverflowStrategy::Backpressure,
4699 OverflowStrategy::DropHead,
4700 OverflowStrategy::DropTail,
4701 OverflowStrategy::DropBuffer,
4702 OverflowStrategy::DropNew,
4703 OverflowStrategy::Fail,
4704 ] {
4705 let graph = cyclic_feedback_i32(cap, strategy);
4706 for input in [vec![6], vec![3, 0, 4], vec![1, 1]] {
4707 assert_cyclic_equiv_i32(&graph, input);
4708 }
4709 }
4710 }
4711 }
4712
4713 #[test]
4714 fn cyclic_typed_falls_back_for_feedback_first_broadcast_orientation() {
4715 let graph = GraphDsl::try_create(|builder| {
4721 let merge = builder.add(MergePreferred::<i32>::new(1));
4722 let broadcast = builder.add(Broadcast::<i32>::new(2));
4723 let buffer = builder.add(Buffer::<i32>::new(4, OverflowStrategy::Backpressure));
4724 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
4725 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4726
4727 builder.connect(merge.outlet(), broadcast.inlet())?;
4728 builder.connect(broadcast.outlet(0)?, buffer.inlet())?;
4730 builder.connect(buffer.outlet(), positive.inlet())?;
4731 builder.connect(positive.outlet(), decrement.inlet())?;
4732 builder.connect(decrement.outlet(), merge.preferred())?;
4733
4734 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(1)?))
4735 })
4736 .unwrap();
4737 for input in [vec![5], vec![2, 4], vec![0, 3]] {
4738 let typed = graph.run_with_input_report_mode(
4739 input.clone(),
4740 CYCLE_LIMIT,
4741 ExecutorMode::TypedOnly,
4742 );
4743 assert!(
4744 matches!(typed, Err(StreamError::GraphValidation(_))),
4745 "feedback-first orientation should not be typed-supported: {typed:?}"
4746 );
4747 let erased = graph
4748 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
4749 .map(|r| r.output);
4750 let auto = graph
4751 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
4752 .map(|r| r.output);
4753 assert_eq!(
4754 erased, auto,
4755 "auto must match erased on fallback for {input:?}"
4756 );
4757 }
4758 }
4759
4760 #[test]
4761 fn cyclic_typed_matches_erased_map_before_takewhile_and_identity() {
4762 let graph = GraphDsl::try_create(|builder| {
4764 let merge = builder.add(MergePreferred::<i32>::new(1));
4765 let broadcast = builder.add(Broadcast::<i32>::new(2));
4766 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4767 let nonneg = builder.add(TakeWhile::<i32>::new(|item| *item >= 0));
4768 let passthrough = builder.add(Identity::<i32>::new());
4769
4770 builder.connect(merge.outlet(), broadcast.inlet())?;
4771 builder.connect(broadcast.outlet(1)?, decrement.inlet())?;
4772 builder.connect(decrement.outlet(), nonneg.inlet())?;
4773 builder.connect(nonneg.outlet(), passthrough.inlet())?;
4774 builder.connect(passthrough.outlet(), merge.preferred())?;
4775
4776 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4777 })
4778 .unwrap();
4779 for input in [vec![5], vec![0], vec![3, 1, 4], vec![10, 0]] {
4780 assert_cyclic_equiv_i32(&graph, input);
4781 }
4782 }
4783
4784 #[test]
4785 fn cyclic_typed_matches_erased_u64_elements() {
4786 let graph = GraphDsl::try_create(|builder| {
4787 let merge = builder.add(MergePreferred::<u64>::new(1));
4788 let broadcast = builder.add(Broadcast::<u64>::new(2));
4789 let buffer = builder.add(Buffer::<u64>::new(16, OverflowStrategy::Backpressure));
4790 let positive = builder.add(TakeWhile::<u64>::new(|item| *item > 0));
4791 let decrement = builder.add(MapStage::new(|item: u64| item - 1));
4792
4793 builder.connect(merge.outlet(), broadcast.inlet())?;
4794 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
4795 builder.connect(buffer.outlet(), positive.inlet())?;
4796 builder.connect(positive.outlet(), decrement.inlet())?;
4797 builder.connect(decrement.outlet(), merge.preferred())?;
4798
4799 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4800 })
4801 .unwrap();
4802 for input in [vec![0u64], vec![7u64], vec![3u64, 5], vec![10_000u64]] {
4803 let erased = graph
4804 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::ErasedOnly)
4805 .map(|r| r.output);
4806 let typed = graph
4807 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::TypedOnly)
4808 .map(|r| r.output);
4809 let auto = graph
4810 .run_with_input_report_mode(input.clone(), CYCLE_LIMIT, ExecutorMode::Auto)
4811 .map(|r| r.output);
4812 assert!(typed.is_ok(), "typed not selected for u64 input {input:?}");
4813 assert_eq!(erased, typed, "u64 typed != erased for {input:?}");
4814 assert_eq!(erased, auto, "u64 auto != erased for {input:?}");
4815 }
4816 }
4817
4818 #[test]
4819 fn cyclic_typed_unproductive_cycle_surfaces_event_limit_like_erased() {
4820 let graph = GraphDsl::try_create(|builder| {
4823 let merge = builder.add(MergePreferred::<i32>::new(1));
4824 let broadcast = builder.add(Broadcast::<i32>::new(2));
4825 let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
4826 let increment = builder.add(MapStage::new(|item: i32| item + 1));
4827
4828 builder.connect(merge.outlet(), broadcast.inlet())?;
4829 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
4830 builder.connect(buffer.outlet(), increment.inlet())?;
4831 builder.connect(increment.outlet(), merge.preferred())?;
4832
4833 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4834 })
4835 .unwrap();
4836 let config = FusedExecutionConfig { event_limit: 512 };
4837 let erased = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::ErasedOnly);
4838 let typed = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::TypedOnly);
4839 let auto = graph.run_with_input_report_mode(vec![1], config, ExecutorMode::Auto);
4840 assert_eq!(
4841 erased.map(|r| r.output),
4842 Err(StreamError::EventLimitExceeded { limit: 512 })
4843 );
4844 assert_eq!(
4845 typed.map(|r| r.output),
4846 Err(StreamError::EventLimitExceeded { limit: 512 })
4847 );
4848 assert_eq!(
4849 auto.map(|r| r.output),
4850 Err(StreamError::EventLimitExceeded { limit: 512 })
4851 );
4852 }
4853
4854 #[test]
4855 fn cyclic_typed_falls_back_for_plain_merge() {
4856 let graph = GraphDsl::try_create(|builder| {
4859 let merge = builder.add(Merge::<i32>::new(2));
4860 let broadcast = builder.add(Broadcast::<i32>::new(2));
4861 let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
4862 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
4863 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4864
4865 builder.connect(merge.outlet(), broadcast.inlet())?;
4866 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
4867 builder.connect(buffer.outlet(), positive.inlet())?;
4868 builder.connect(positive.outlet(), decrement.inlet())?;
4869 builder.connect(decrement.outlet(), merge.inlet(1)?)?;
4870
4871 Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
4872 })
4873 .unwrap();
4874 let typed = graph.run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::TypedOnly);
4875 assert!(
4876 matches!(typed, Err(StreamError::GraphValidation(_))),
4877 "plain Merge cycle should not be typed-supported: {typed:?}"
4878 );
4879 let erased = graph
4880 .run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::ErasedOnly)
4881 .map(|r| r.output);
4882 let auto = graph
4883 .run_with_input_report_mode(vec![3], CYCLE_LIMIT, ExecutorMode::Auto)
4884 .map(|r| r.output);
4885 assert_eq!(erased, auto, "auto must match erased on fallback");
4886 }
4887
4888 #[test]
4889 fn cyclic_typed_falls_back_for_custom_opaque_in_feedback() {
4890 let graph = GraphDsl::try_create(|builder| {
4892 let merge = builder.add(MergePreferred::<i32>::new(1));
4893 let broadcast = builder.add(Broadcast::<i32>::new(2));
4894 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
4895 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
4896 let custom = builder.add(BufferedFlowOnPull {
4897 state: Arc::new(Mutex::new(BufferedFlowState::default())),
4898 });
4899
4900 builder.connect(merge.outlet(), broadcast.inlet())?;
4901 builder.connect(broadcast.outlet(1)?, positive.inlet())?;
4902 builder.connect(positive.outlet(), decrement.inlet())?;
4903 builder.connect(decrement.outlet(), custom.inlet())?;
4904 builder.connect(custom.outlet(), merge.preferred())?;
4905
4906 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
4907 })
4908 .unwrap();
4909 let typed = graph.run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::TypedOnly);
4910 assert!(
4911 matches!(typed, Err(StreamError::GraphValidation(_))),
4912 "custom opaque feedback stage should fall back: {typed:?}"
4913 );
4914 let erased = graph
4915 .run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::ErasedOnly)
4916 .map(|r| r.output);
4917 let auto = graph
4918 .run_with_input_report_mode(vec![4], CYCLE_LIMIT, ExecutorMode::Auto)
4919 .map(|r| r.output);
4920 assert_eq!(erased, auto, "auto must match erased on fallback");
4921 }
4922
4923 #[test]
4924 fn cyclic_typed_matches_erased_randomized() {
4925 let graph = cyclic_feedback_i32(16, OverflowStrategy::Backpressure);
4928 let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
4929 let mut next = || {
4930 state ^= state << 13;
4931 state ^= state >> 7;
4932 state ^= state << 17;
4933 state
4934 };
4935 for _ in 0..200 {
4936 let len = (next() % 6) as usize;
4937 let input: Vec<i32> = (0..len).map(|_| (next() % 20) as i32 - 5).collect();
4938 assert_cyclic_equiv_i32(&graph, input);
4939 }
4940 }
4941
4942 #[test]
4943 fn partition_holds_routed_element_until_target_outlet_pulls() {
4944 let graph = GraphDsl::create(|builder| {
4945 builder.add(Partition::<i32>::new(2, |value| usize::from(*value >= 10)))
4946 })
4947 .unwrap();
4948
4949 let stage_index = 0usize;
4950 let inlet = graph.shape.inlet().id();
4951 let out0 = graph.shape.outlet(0).unwrap().id();
4952 let out1 = graph.shape.outlet(1).unwrap().id();
4953 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4954
4955 executor.process_pull(stage_index, out0).unwrap();
4956 let transition = executor
4957 .process_stage(stage_index, inlet, datum(11_i32))
4958 .unwrap();
4959 assert!(matches!(transition.emissions, StageEmissions::None));
4960
4961 let pull_transition = executor.process_pull(stage_index, out1).unwrap();
4962 match pull_transition.emissions {
4963 StageEmissions::One(port, value) => {
4964 assert_eq!(port, out1);
4965 assert_eq!(
4966 downcast_datum::<i32, _>(value, "emit", || "Partition.out1").unwrap(),
4967 11
4968 );
4969 }
4970 _ => panic!("expected one pending partition emission"),
4971 }
4972 }
4973
4974 #[test]
4975 fn partition_cancels_upstream_only_after_all_outlets_cancel_when_not_eager() {
4976 let graph = GraphDsl::create(|builder| {
4977 builder.add(Partition::<i32>::new(2, |value| usize::from(*value >= 10)))
4978 })
4979 .unwrap();
4980
4981 let stage_index = 0usize;
4982 let out0 = graph.shape.outlet(0).unwrap().id();
4983 let out1 = graph.shape.outlet(1).unwrap().id();
4984 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
4985
4986 let first = executor
4987 .process_downstream_finish(stage_index, out0)
4988 .unwrap();
4989 assert!(first.cancelled_inlets.is_empty());
4990
4991 let second = executor
4992 .process_downstream_finish(stage_index, out1)
4993 .unwrap();
4994 assert_eq!(second.cancelled_inlets, vec![graph.shape.inlet().id()]);
4995 }
4996
4997 #[test]
4998 fn unzip_continues_emitting_to_live_outlet_after_peer_cancels() {
4999 let graph =
5000 GraphDsl::create(|builder| builder.add(Unzip::<i32, &'static str>::new())).unwrap();
5001
5002 let stage_index = 0usize;
5003 let inlet = graph.shape.inlet().id();
5004 let out0 = graph.shape.out0().id();
5005 let out1 = graph.shape.out1().id();
5006 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
5007
5008 executor.process_pull(stage_index, out0).unwrap();
5009 executor.process_pull(stage_index, out1).unwrap();
5010 let cancel = executor
5011 .process_downstream_finish(stage_index, out1)
5012 .unwrap();
5013 assert!(cancel.cancelled_inlets.is_empty());
5014
5015 let transition = executor
5016 .process_stage(stage_index, inlet, datum((7_i32, "seven")))
5017 .unwrap();
5018 match transition.emissions {
5019 StageEmissions::One(port, value) => {
5020 assert_eq!(port, out0);
5021 assert_eq!(
5022 downcast_datum::<i32, _>(value, "emit", || "Unzip.out0").unwrap(),
5023 7
5024 );
5025 }
5026 StageEmissions::Many(values) => {
5027 assert_eq!(values.len(), 1);
5028 assert_eq!(values[0].0, out0);
5029 }
5030 _ => panic!("expected emission to the remaining live unzip outlet"),
5031 }
5032 }
5033
5034 #[test]
5035 fn unzip_cancels_upstream_only_after_both_outlets_cancel() {
5036 let graph =
5037 GraphDsl::create(|builder| builder.add(Unzip::<i32, &'static str>::new())).unwrap();
5038
5039 let stage_index = 0usize;
5040 let out0 = graph.shape.out0().id();
5041 let out1 = graph.shape.out1().id();
5042 let mut executor = FusedExecutor::new(&graph, FusedExecutionConfig::default());
5043
5044 let first = executor
5045 .process_downstream_finish(stage_index, out0)
5046 .unwrap();
5047 assert!(first.cancelled_inlets.is_empty());
5048
5049 let second = executor
5050 .process_downstream_finish(stage_index, out1)
5051 .unwrap();
5052 assert_eq!(second.cancelled_inlets, vec![graph.shape.inlet().id()]);
5053 }
5054
5055 #[test]
5056 fn opaque_internal_outlet_repulls_after_first_emission() {
5057 let graph = GraphDsl::try_create(|builder| {
5058 let opaque = builder.add(EmitMultipleOnPush);
5059 let identity = builder.add(Identity::<i32>::new());
5060 builder.connect(opaque.outlet(), identity.inlet())?;
5061 Ok(FlowShape::new(opaque.inlet(), identity.outlet()))
5062 })
5063 .unwrap();
5064
5065 assert_eq!(graph.run_with_input([10]).unwrap(), vec![1, 2]);
5066 }
5067
5068 #[test]
5081 fn executor_mode_auto_erased_identical_typed_errors() {
5082 let graph = GraphDsl::try_create(|builder| {
5083 let broadcast = builder.add(Broadcast::<i32>::new(2));
5084 let merge = builder.add(Merge::<i32>::new(2));
5085 builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
5086 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
5087 Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
5088 })
5089 .unwrap();
5090
5091 let input = vec![1, 2, 3];
5092
5093 let auto_result = graph
5094 .run_with_input_mode(input.clone(), ExecutorMode::Auto)
5095 .unwrap();
5096 let erased_result = graph
5097 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5098 .unwrap();
5099
5100 assert_eq!(auto_result, erased_result);
5103 assert_eq!(auto_result.len(), input.len() * 2);
5105
5106 let typed_result = graph.run_with_input_mode(input.clone(), ExecutorMode::TypedOnly);
5108 assert!(
5109 matches!(
5110 typed_result,
5111 Err(StreamError::GraphValidation(ref msg))
5112 if msg.contains("typed executor does not support this graph shape")
5113 ),
5114 "expected TypedOnly to error for junction graph, got: {typed_result:?}"
5115 );
5116 }
5117
5118 fn identity_chain_bp(n: usize) -> GraphBlueprint<FlowShape<i64, i64>> {
5122 assert!(n >= 1);
5123 GraphDsl::try_create(|builder| {
5124 let first = builder.add(Identity::<i64>::new());
5125 let inlet = first.inlet();
5126 let mut outlet = first.outlet();
5127 for _ in 1..n {
5128 let next = builder.add(Identity::<i64>::new());
5129 builder.connect(outlet, next.inlet())?;
5130 outlet = next.outlet();
5131 }
5132 Ok(FlowShape::new(inlet, outlet))
5133 })
5134 .unwrap()
5135 }
5136
5137 fn map_chain_bp(n: usize) -> GraphBlueprint<FlowShape<i64, i64>> {
5139 assert!(n >= 1);
5140 GraphDsl::try_create(|builder| {
5141 let first = builder.add(MapStage::new(|x: i64| x.wrapping_mul(2)));
5142 let inlet = first.inlet();
5143 let mut outlet = first.outlet();
5144 for _ in 1..n {
5145 let next = builder.add(MapStage::new(|x: i64| x.wrapping_mul(2)));
5146 builder.connect(outlet, next.inlet())?;
5147 outlet = next.outlet();
5148 }
5149 Ok(FlowShape::new(inlet, outlet))
5150 })
5151 .unwrap()
5152 }
5153
5154 #[test]
5157 fn typed_erased_equivalence_identity_collect() {
5158 let graph = identity_chain_bp(5);
5159 let input: Vec<i64> = (0..20).collect();
5160
5161 let erased = graph
5162 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5163 .unwrap();
5164 let typed = graph
5165 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5166 .unwrap();
5167
5168 assert_eq!(
5169 typed, erased,
5170 "typed and erased paths disagree on identity×5"
5171 );
5172 }
5173
5174 #[test]
5177 fn typed_erased_equivalence_identity_count() {
5178 let graph = identity_chain_bp(5);
5179 let input: Vec<i64> = (0..20).collect();
5180 let config = FusedExecutionConfig::default();
5181
5182 let erased = graph
5183 .run_count_with_input_report_mode(input.clone(), config, ExecutorMode::ErasedOnly)
5184 .unwrap()
5185 .result;
5186 let typed = graph
5187 .run_count_with_input_report_mode(input.clone(), config, ExecutorMode::TypedOnly)
5188 .unwrap()
5189 .result;
5190
5191 assert_eq!(typed, erased, "typed and erased count differ on identity×5");
5192 }
5193
5194 #[test]
5197 fn typed_erased_equivalence_map_fold() {
5198 let graph = map_chain_bp(5);
5199 let input: Vec<i64> = (1..=10).collect();
5200 let config = FusedExecutionConfig::default();
5201
5202 let erased = graph
5203 .run_fold_with_input_report_mode(
5204 input.clone(),
5205 0i64,
5206 |acc, x| acc.wrapping_add(x),
5207 config,
5208 ExecutorMode::ErasedOnly,
5209 )
5210 .unwrap()
5211 .result;
5212 let typed = graph
5213 .run_fold_with_input_report_mode(
5214 input.clone(),
5215 0i64,
5216 |acc, x| acc.wrapping_add(x),
5217 config,
5218 ExecutorMode::TypedOnly,
5219 )
5220 .unwrap()
5221 .result;
5222
5223 assert_eq!(typed, erased, "typed and erased fold differ on map×5");
5224 }
5225
5226 #[test]
5229 fn typed_erased_equivalence_map_collect() {
5230 let graph = map_chain_bp(5);
5231 let input: Vec<i64> = (0..20).collect();
5232
5233 let erased = graph
5234 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5235 .unwrap();
5236 let typed = graph
5237 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5238 .unwrap();
5239
5240 assert_eq!(typed, erased, "typed and erased paths disagree on map×5");
5241 }
5242
5243 #[test]
5246 fn typed_only_errors_on_junction_graph() {
5247 let graph = GraphDsl::try_create(|builder| {
5248 let broadcast = builder.add(Broadcast::<i32>::new(2));
5249 let merge = builder.add(Merge::<i32>::new(2));
5250 builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
5251 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
5252 Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
5253 })
5254 .unwrap();
5255
5256 let result = graph.run_with_input_mode(vec![1], ExecutorMode::TypedOnly);
5257 assert!(
5258 matches!(
5259 result,
5260 Err(StreamError::GraphValidation(ref msg))
5261 if msg.contains("typed executor does not support this graph shape")
5262 ),
5263 "expected TypedOnly to error on junction, got: {result:?}"
5264 );
5265 }
5266
5267 #[test]
5269 fn auto_falls_back_silently_for_junction_graph() {
5270 let graph = GraphDsl::try_create(|builder| {
5271 let broadcast = builder.add(Broadcast::<i32>::new(2));
5272 let merge = builder.add(Merge::<i32>::new(2));
5273 builder.connect(broadcast.outlet(0)?, merge.inlet(0)?)?;
5274 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
5275 Ok(FlowShape::new(broadcast.inlet(), merge.outlet()))
5276 })
5277 .unwrap();
5278
5279 let result = graph.run_with_input_mode(vec![1, 2, 3], ExecutorMode::Auto);
5280 assert!(result.is_ok(), "Auto should succeed (fallback to erased)");
5281 assert_eq!(result.unwrap().len(), 6); }
5283
5284 fn merge_sequence_graph() -> GraphBlueprint<FlowShape<(u64, u64), u64>> {
5288 GraphDsl::try_create(|builder| {
5289 let unzip = builder.add(Unzip::<u64, u64>::new());
5290 let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
5291 builder.connect(unzip.out0(), merge.inlet(0)?)?;
5292 builder.connect(unzip.out1(), merge.inlet(1)?)?;
5293 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
5294 })
5295 .unwrap()
5296 }
5297
5298 #[test]
5301 fn typed_only_accepts_merge_sequence_topology() {
5302 let graph = merge_sequence_graph();
5303 let result =
5304 graph.run_with_input_mode(vec![(0u64, 1u64), (2u64, 3u64)], ExecutorMode::TypedOnly);
5305 assert!(
5306 result.is_ok(),
5307 "TypedOnly should accept Unzip→MergeSequence topology, got: {result:?}"
5308 );
5309 }
5310
5311 #[test]
5314 fn typed_erased_equivalence_merge_sequence_in_order() {
5315 let graph = merge_sequence_graph();
5316 let input: Vec<(u64, u64)> = (0..10).step_by(2).map(|i| (i, i + 1)).collect();
5317
5318 let erased = graph
5319 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5320 .unwrap();
5321 let typed = graph
5322 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5323 .unwrap();
5324
5325 assert_eq!(
5326 typed, erased,
5327 "typed and erased disagree on in-order merge_sequence"
5328 );
5329 let expected: Vec<u64> = (0..10).collect();
5331 assert_eq!(typed, expected);
5332 }
5333
5334 #[test]
5338 fn typed_erased_equivalence_merge_sequence_out_of_order() {
5339 let graph = merge_sequence_graph();
5340 let input: Vec<(u64, u64)> = vec![(1, 0), (3, 2), (5, 4)];
5342
5343 let erased = graph
5344 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5345 .unwrap();
5346 let typed = graph
5347 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5348 .unwrap();
5349
5350 assert_eq!(
5351 typed, erased,
5352 "typed and erased disagree on out-of-order merge_sequence"
5353 );
5354 assert_eq!(typed, vec![0u64, 1, 2, 3, 4, 5]);
5356 }
5357
5358 #[test]
5363 fn typed_erased_equivalence_merge_sequence_gap_failure() {
5364 let graph = merge_sequence_graph();
5367 let input = vec![(1u64, 2u64)];
5368
5369 let erased = graph.run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly);
5370 let typed = graph.run_with_input_mode(input.clone(), ExecutorMode::TypedOnly);
5371
5372 assert!(
5373 matches!(&erased, Err(StreamError::Failed(msg)) if msg.contains("expected sequence")),
5374 "ErasedOnly should fail on gap: {erased:?}"
5375 );
5376 assert!(
5377 matches!(&typed, Err(StreamError::Failed(msg)) if msg.contains("expected sequence")),
5378 "TypedOnly should fail on gap: {typed:?}"
5379 );
5380 }
5381
5382 #[test]
5385 fn typed_erased_equivalence_merge_sequence_completion() {
5386 let graph = merge_sequence_graph();
5387 let input = vec![(0u64, 1u64)];
5388
5389 let erased = graph
5390 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5391 .unwrap();
5392 let typed = graph
5393 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5394 .unwrap();
5395
5396 assert_eq!(typed, erased, "typed and erased disagree on completion");
5397 assert_eq!(typed, vec![0u64, 1u64]);
5398 }
5399
5400 #[test]
5403 fn auto_selects_typed_for_merge_sequence_topology() {
5404 let graph = merge_sequence_graph();
5405 let input: Vec<(u64, u64)> = (0..20).step_by(2).map(|i| (i, i + 1)).collect();
5406
5407 let auto_result = graph
5408 .run_with_input_mode(input.clone(), ExecutorMode::Auto)
5409 .unwrap();
5410 let erased_result = graph
5411 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5412 .unwrap();
5413
5414 assert_eq!(
5415 auto_result, erased_result,
5416 "Auto and ErasedOnly disagree on merge_sequence topology"
5417 );
5418 }
5419
5420 fn merge_latest_graph_exec() -> GraphBlueprint<FlowShape<(u64, u64), Vec<u64>>> {
5424 GraphDsl::try_create(|builder| {
5425 let unzip = builder.add(Unzip::<u64, u64>::new());
5426 let merge = builder.add(MergeLatest::<u64>::new(2, false));
5427 builder.connect(unzip.out0(), merge.inlet(0)?)?;
5428 builder.connect(unzip.out1(), merge.inlet(1)?)?;
5429 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
5430 })
5431 .unwrap()
5432 }
5433
5434 fn merge_latest_eager_graph_exec() -> GraphBlueprint<FlowShape<(i32, i32), Vec<i32>>> {
5436 GraphDsl::try_create(|builder| {
5437 let unzip = builder.add(Unzip::<i32, i32>::new());
5438 let merge = builder.add(MergeLatest::<i32>::new(2, true));
5439 builder.connect(unzip.out0(), merge.inlet(0)?)?;
5440 builder.connect(unzip.out1(), merge.inlet(1)?)?;
5441 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
5442 })
5443 .unwrap()
5444 }
5445
5446 #[test]
5448 fn typed_only_accepts_merge_latest_topology() {
5449 let graph = merge_latest_graph_exec();
5450 let result =
5451 graph.run_with_input_mode(vec![(0u64, 1u64), (2u64, 3u64)], ExecutorMode::TypedOnly);
5452 assert!(
5453 result.is_ok(),
5454 "TypedOnly should accept Unzip→MergeLatest topology, got: {result:?}"
5455 );
5456 }
5457
5458 #[test]
5460 fn typed_erased_equivalence_merge_latest_snapshot_ordering() {
5461 let graph = merge_latest_graph_exec();
5462 let input: Vec<(u64, u64)> = (0..10).map(|i| (i, i + 100)).collect();
5465
5466 let erased = graph
5467 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5468 .unwrap();
5469 let typed = graph
5470 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5471 .unwrap();
5472
5473 assert_eq!(
5474 typed, erased,
5475 "typed and erased disagree on snapshot ordering"
5476 );
5477 assert!(
5479 typed.iter().all(|s| s.len() == 2),
5480 "snapshots must have len 2"
5481 );
5482 }
5483
5484 #[test]
5486 fn typed_erased_equivalence_merge_latest_partial_fill() {
5487 let graph = merge_latest_graph_exec();
5490 let input = vec![(5u64, 42u64)];
5491
5492 let erased = graph
5493 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5494 .unwrap();
5495 let typed = graph
5496 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5497 .unwrap();
5498
5499 assert_eq!(typed, erased, "typed and erased disagree on partial-fill");
5500 assert_eq!(typed.len(), 1, "expected exactly one snapshot");
5502 }
5503
5504 #[test]
5510 fn typed_erased_equivalence_merge_latest_eager_complete() {
5511 let graph_eager = merge_latest_eager_graph_exec();
5512 let input = vec![(1i32, 10i32)];
5513
5514 let erased = graph_eager
5515 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5516 .unwrap();
5517 let typed = graph_eager
5518 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5519 .unwrap();
5520
5521 assert_eq!(
5522 typed, erased,
5523 "typed and erased disagree on eager-complete behavior"
5524 );
5525 assert!(
5526 !typed.is_empty(),
5527 "eager-complete graph should produce at least one snapshot"
5528 );
5529 }
5530
5531 #[test]
5533 fn typed_erased_equivalence_merge_latest_completion() {
5534 let graph = merge_latest_graph_exec();
5535 let input = vec![(0u64, 1u64)];
5536
5537 let erased = graph
5538 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5539 .unwrap();
5540 let typed = graph
5541 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5542 .unwrap();
5543
5544 assert_eq!(typed, erased, "typed and erased disagree on completion");
5545 }
5546
5547 #[test]
5550 fn auto_selects_typed_for_merge_latest_topology() {
5551 let graph = merge_latest_graph_exec();
5552 let input: Vec<(u64, u64)> = (0..20).map(|i| (i, i + 1_000)).collect();
5553
5554 let auto_result = graph
5555 .run_with_input_mode(input.clone(), ExecutorMode::Auto)
5556 .unwrap();
5557 let erased_result = graph
5558 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5559 .unwrap();
5560
5561 assert_eq!(
5562 auto_result, erased_result,
5563 "Auto and ErasedOnly disagree on merge_latest topology"
5564 );
5565 }
5566
5567 fn broadcast_zip_graph_exec() -> GraphBlueprint<FlowShape<i64, (i64, i64)>> {
5570 GraphDsl::try_create(|builder| {
5571 let broadcast = builder.add(Broadcast::<i64>::new(2));
5572 let zip = builder.add(Zip::<i64, i64>::new());
5573 builder.connect(broadcast.outlet(0)?, zip.in0())?;
5574 builder.connect(broadcast.outlet(1)?, zip.in1())?;
5575 Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
5576 })
5577 .unwrap()
5578 }
5579
5580 fn balance_merge_graph_exec() -> GraphBlueprint<FlowShape<i64, i64>> {
5581 GraphDsl::try_create(|builder| {
5582 let balance = builder.add(Balance::<i64>::new(2));
5583 let merge = builder.add(Merge::<i64>::new(2));
5584 builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
5585 builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
5586 Ok(FlowShape::new(balance.inlet(), merge.outlet()))
5587 })
5588 .unwrap()
5589 }
5590
5591 fn partition_merge_graph_exec() -> GraphBlueprint<FlowShape<i64, i64>> {
5592 GraphDsl::try_create(|builder| {
5593 let partition = builder.add(Partition::<i64>::new(2, |item| {
5594 item.unsigned_abs() as usize % 2
5595 }));
5596 let merge = builder.add(Merge::<i64>::new(2));
5597 builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
5598 builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
5599 Ok(FlowShape::new(partition.inlet(), merge.outlet()))
5600 })
5601 .unwrap()
5602 }
5603
5604 fn unzip_zip_graph_exec() -> GraphBlueprint<FlowShape<i64, (i64, i64)>> {
5605 GraphDsl::try_create(|builder| {
5606 let unzip = builder.add(UnzipWith::<i64, i64, i64>::new(|item| (item, item + 10)));
5607 let zip = builder.add(Zip::<i64, i64>::new());
5608 builder.connect(unzip.out0(), zip.in0())?;
5609 builder.connect(unzip.out1(), zip.in1())?;
5610 Ok(FlowShape::new(unzip.inlet(), zip.outlet()))
5611 })
5612 .unwrap()
5613 }
5614
5615 fn merge_sorted_graph_exec() -> GraphBlueprint<FlowShape<(u64, u64), u64>> {
5616 GraphDsl::try_create(|builder| {
5617 let unzip = builder.add(Unzip::<u64, u64>::new());
5618 let merge = builder.add(MergeSorted::<u64>::new());
5619 builder.connect(unzip.out0(), merge.inlet(0)?)?;
5620 builder.connect(unzip.out1(), merge.inlet(1)?)?;
5621 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
5622 })
5623 .unwrap()
5624 }
5625
5626 #[test]
5627 fn typed_erased_equivalence_broadcast_zip() {
5628 let graph = broadcast_zip_graph_exec();
5629 let input: Vec<i64> = (-3..=3).collect();
5630
5631 let erased = graph
5632 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5633 .unwrap();
5634 let typed = graph
5635 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5636 .unwrap();
5637 let auto = graph
5638 .run_with_input_mode(input, ExecutorMode::Auto)
5639 .unwrap();
5640
5641 assert_eq!(typed, erased, "typed and erased disagree on Broadcast->Zip");
5642 assert_eq!(
5643 auto, erased,
5644 "Auto and ErasedOnly disagree on Broadcast->Zip"
5645 );
5646 assert_eq!(typed.first().copied(), Some((-3, -3)));
5647 }
5648
5649 #[test]
5650 fn typed_erased_equivalence_balance_merge() {
5651 let graph = balance_merge_graph_exec();
5652 let input: Vec<i64> = (0..32).collect();
5653
5654 let erased = graph
5655 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5656 .unwrap();
5657 let typed = graph
5658 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5659 .unwrap();
5660 let auto = graph
5661 .run_with_input_mode(input, ExecutorMode::Auto)
5662 .unwrap();
5663
5664 assert_eq!(typed, erased, "typed and erased disagree on Balance->Merge");
5665 assert_eq!(
5666 auto, erased,
5667 "Auto and ErasedOnly disagree on Balance->Merge"
5668 );
5669 assert_eq!(typed.len(), 32);
5670 }
5671
5672 #[test]
5673 fn typed_erased_equivalence_partition_merge() {
5674 let graph = partition_merge_graph_exec();
5675 let input: Vec<i64> = (-12..12).collect();
5676
5677 let erased = graph
5678 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5679 .unwrap();
5680 let typed = graph
5681 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5682 .unwrap();
5683 let auto = graph
5684 .run_with_input_mode(input, ExecutorMode::Auto)
5685 .unwrap();
5686
5687 assert_eq!(
5688 typed, erased,
5689 "typed and erased disagree on Partition->Merge"
5690 );
5691 assert_eq!(
5692 auto, erased,
5693 "Auto and ErasedOnly disagree on Partition->Merge"
5694 );
5695 }
5696
5697 #[test]
5698 fn typed_erased_equivalence_partition_merge_error() {
5699 let graph = GraphDsl::try_create(|builder| {
5700 let partition = builder.add(Partition::<i64>::new(2, |_| 2));
5701 let merge = builder.add(Merge::<i64>::new(2));
5702 builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
5703 builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
5704 Ok(FlowShape::new(partition.inlet(), merge.outlet()))
5705 })
5706 .unwrap();
5707
5708 let erased = graph.run_with_input_mode(vec![7], ExecutorMode::ErasedOnly);
5709 let typed = graph.run_with_input_mode(vec![7], ExecutorMode::TypedOnly);
5710
5711 assert!(
5712 matches!(&erased, Err(StreamError::Failed(msg)) if msg.contains("out-of-bounds")),
5713 "ErasedOnly should fail on bad partitioner: {erased:?}"
5714 );
5715 assert!(
5716 matches!(&typed, Err(StreamError::Failed(msg)) if msg.contains("out-of-bounds")),
5717 "TypedOnly should fail on bad partitioner: {typed:?}"
5718 );
5719 }
5720
5721 #[test]
5722 fn typed_erased_equivalence_unzip_zip() {
5723 let graph = unzip_zip_graph_exec();
5724 let input: Vec<i64> = (0..16).collect();
5725
5726 let erased = graph
5727 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5728 .unwrap();
5729 let typed = graph
5730 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5731 .unwrap();
5732 let auto = graph
5733 .run_with_input_mode(input, ExecutorMode::Auto)
5734 .unwrap();
5735
5736 assert_eq!(typed, erased, "typed and erased disagree on UnzipWith->Zip");
5737 assert_eq!(
5738 auto, erased,
5739 "Auto and ErasedOnly disagree on UnzipWith->Zip"
5740 );
5741 assert_eq!(typed[0], (0, 10));
5742 }
5743
5744 #[test]
5745 fn typed_erased_equivalence_merge_sorted() {
5746 let graph = merge_sorted_graph_exec();
5747 let input: Vec<(u64, u64)> = (0..20).step_by(2).map(|item| (item, item + 1)).collect();
5748
5749 let erased = graph
5750 .run_with_input_mode(input.clone(), ExecutorMode::ErasedOnly)
5751 .unwrap();
5752 let typed = graph
5753 .run_with_input_mode(input.clone(), ExecutorMode::TypedOnly)
5754 .unwrap();
5755 let auto = graph
5756 .run_with_input_mode(input, ExecutorMode::Auto)
5757 .unwrap();
5758
5759 assert_eq!(
5760 typed, erased,
5761 "typed and erased disagree on Unzip->MergeSorted"
5762 );
5763 assert_eq!(
5764 auto, erased,
5765 "Auto and ErasedOnly disagree on Unzip->MergeSorted"
5766 );
5767 assert_eq!(typed, (0..20).collect::<Vec<_>>());
5768 }
5769
5770 #[test]
5771 fn typed_erased_equivalence_prioritized_merge_helper() {
5772 let graph =
5773 GraphDsl::create(|builder| builder.add(MergePrioritized::<i64>::new(vec![2, 1])))
5774 .unwrap();
5775 let inputs = vec![vec![1, 2, 3, 4], vec![100, 101]];
5776
5777 let erased = graph
5778 .run_fan_in_report_mode(
5779 inputs.clone(),
5780 FusedExecutionConfig::default(),
5781 ExecutorMode::ErasedOnly,
5782 )
5783 .unwrap();
5784 let typed = graph
5785 .run_fan_in_report_mode(
5786 inputs.clone(),
5787 FusedExecutionConfig::default(),
5788 ExecutorMode::TypedOnly,
5789 )
5790 .unwrap();
5791 let auto = graph
5792 .run_fan_in_report_mode(inputs, FusedExecutionConfig::default(), ExecutorMode::Auto)
5793 .unwrap();
5794
5795 assert_execution_equivalent(&typed, &erased, "MergePrioritized typed vs erased");
5796 assert_execution_equivalent(&auto, &erased, "MergePrioritized Auto vs erased");
5797 assert_eq!(typed.output, vec![1, 2, 100, 3, 4, 101]);
5798 }
5799
5800 #[test]
5801 fn typed_erased_equivalence_merge_preferred_helper() {
5802 let graph = GraphDsl::create(|builder| builder.add(MergePreferred::<i64>::new(2))).unwrap();
5803 let preferred = vec![1, 2, 3];
5804 let secondary = vec![vec![100, 101], vec![200, 201]];
5805
5806 let erased = graph
5807 .run_merge_preferred_report_mode(
5808 preferred.clone(),
5809 secondary.clone(),
5810 FusedExecutionConfig::default(),
5811 ExecutorMode::ErasedOnly,
5812 )
5813 .unwrap();
5814 let typed = graph
5815 .run_merge_preferred_report_mode(
5816 preferred.clone(),
5817 secondary.clone(),
5818 FusedExecutionConfig::default(),
5819 ExecutorMode::TypedOnly,
5820 )
5821 .unwrap();
5822 let auto = graph
5823 .run_merge_preferred_report_mode(
5824 preferred,
5825 secondary,
5826 FusedExecutionConfig::default(),
5827 ExecutorMode::Auto,
5828 )
5829 .unwrap();
5830
5831 assert_execution_equivalent(&typed, &erased, "MergePreferred typed vs erased");
5832 assert_execution_equivalent(&auto, &erased, "MergePreferred Auto vs erased");
5833 assert_eq!(typed.output, vec![1, 2, 3, 100, 200, 101, 201]);
5834 }
5835
5836 #[test]
5837 fn typed_erased_equivalence_concat_helper() {
5838 let graph = GraphDsl::create(|builder| builder.add(Concat::<i64>::new(3))).unwrap();
5839 let inputs = vec![vec![1, 2], vec![], vec![3, 4]];
5840
5841 let erased = graph
5842 .run_concat_report_mode(
5843 inputs.clone(),
5844 FusedExecutionConfig::default(),
5845 ExecutorMode::ErasedOnly,
5846 )
5847 .unwrap();
5848 let typed = graph
5849 .run_concat_report_mode(
5850 inputs.clone(),
5851 FusedExecutionConfig::default(),
5852 ExecutorMode::TypedOnly,
5853 )
5854 .unwrap();
5855 let auto = graph
5856 .run_concat_report_mode(inputs, FusedExecutionConfig::default(), ExecutorMode::Auto)
5857 .unwrap();
5858
5859 assert_execution_equivalent(&typed, &erased, "Concat typed vs erased");
5860 assert_execution_equivalent(&auto, &erased, "Concat Auto vs erased");
5861 assert_eq!(typed.output, vec![1, 2, 3, 4]);
5862 }
5863
5864 #[test]
5865 fn typed_erased_equivalence_interleave_helper() {
5866 let graph = GraphDsl::create(|builder| builder.add(Interleave::<i64>::new(3, 2))).unwrap();
5867 let inputs = vec![vec![1, 2, 3], vec![10, 11, 12], vec![20]];
5868
5869 let erased = graph
5870 .run_interleave_report_mode(
5871 inputs.clone(),
5872 2,
5873 false,
5874 FusedExecutionConfig::default(),
5875 ExecutorMode::ErasedOnly,
5876 )
5877 .unwrap();
5878 let typed = graph
5879 .run_interleave_report_mode(
5880 inputs.clone(),
5881 2,
5882 false,
5883 FusedExecutionConfig::default(),
5884 ExecutorMode::TypedOnly,
5885 )
5886 .unwrap();
5887 let auto = graph
5888 .run_interleave_report_mode(
5889 inputs,
5890 2,
5891 false,
5892 FusedExecutionConfig::default(),
5893 ExecutorMode::Auto,
5894 )
5895 .unwrap();
5896
5897 assert_execution_equivalent(&typed, &erased, "Interleave typed vs erased");
5898 assert_execution_equivalent(&auto, &erased, "Interleave Auto vs erased");
5899 assert_eq!(typed.output, vec![1, 2, 10, 11, 20, 3, 12]);
5900 }
5901
5902 #[test]
5903 fn typed_erased_equivalence_interleave_eager_close_helper() {
5904 let graph = GraphDsl::create(|builder| {
5905 builder.add(Interleave::<i64>::new_with_eager_close(2, 1, true))
5906 })
5907 .unwrap();
5908 let inputs = vec![vec![1], vec![10, 11]];
5909
5910 let erased = graph
5911 .run_interleave_report_mode(
5912 inputs.clone(),
5913 1,
5914 true,
5915 FusedExecutionConfig::default(),
5916 ExecutorMode::ErasedOnly,
5917 )
5918 .unwrap();
5919 let typed = graph
5920 .run_interleave_report_mode(
5921 inputs.clone(),
5922 1,
5923 true,
5924 FusedExecutionConfig::default(),
5925 ExecutorMode::TypedOnly,
5926 )
5927 .unwrap();
5928 let auto = graph
5929 .run_interleave_report_mode(
5930 inputs,
5931 1,
5932 true,
5933 FusedExecutionConfig::default(),
5934 ExecutorMode::Auto,
5935 )
5936 .unwrap();
5937
5938 assert_execution_equivalent(&typed, &erased, "Interleave eager close typed vs erased");
5939 assert_execution_equivalent(&auto, &erased, "Interleave eager close Auto vs erased");
5940 assert_eq!(typed.output, vec![1, 10]);
5941 }
5942
5943 #[test]
5944 fn typed_erased_equivalence_helper_event_limit_failures() {
5945 let config = FusedExecutionConfig { event_limit: 1 };
5946
5947 let prioritized =
5948 GraphDsl::create(|builder| builder.add(MergePrioritized::<i64>::new(vec![2, 1])))
5949 .unwrap();
5950 let prioritized_inputs = vec![vec![1], vec![10]];
5951 let erased = prioritized.run_fan_in_report_mode(
5952 prioritized_inputs.clone(),
5953 config,
5954 ExecutorMode::ErasedOnly,
5955 );
5956 let typed = prioritized.run_fan_in_report_mode(
5957 prioritized_inputs.clone(),
5958 config,
5959 ExecutorMode::TypedOnly,
5960 );
5961 let auto =
5962 prioritized.run_fan_in_report_mode(prioritized_inputs, config, ExecutorMode::Auto);
5963 assert_eq!(
5964 typed.map(execution_observation),
5965 erased.map(execution_observation)
5966 );
5967 assert_eq!(
5968 auto.map(execution_observation),
5969 Err(StreamError::EventLimitExceeded { limit: 1 })
5970 );
5971
5972 let preferred =
5973 GraphDsl::create(|builder| builder.add(MergePreferred::<i64>::new(1))).unwrap();
5974 let erased = preferred.run_merge_preferred_report_mode(
5975 vec![1],
5976 vec![vec![10]],
5977 config,
5978 ExecutorMode::ErasedOnly,
5979 );
5980 let typed = preferred.run_merge_preferred_report_mode(
5981 vec![1],
5982 vec![vec![10]],
5983 config,
5984 ExecutorMode::TypedOnly,
5985 );
5986 let auto = preferred.run_merge_preferred_report_mode(
5987 vec![1],
5988 vec![vec![10]],
5989 config,
5990 ExecutorMode::Auto,
5991 );
5992 assert_eq!(
5993 typed.map(execution_observation),
5994 erased.map(execution_observation)
5995 );
5996 assert_eq!(
5997 auto.map(execution_observation),
5998 Err(StreamError::EventLimitExceeded { limit: 1 })
5999 );
6000
6001 let concat = GraphDsl::create(|builder| builder.add(Concat::<i64>::new(2))).unwrap();
6002 let concat_inputs = vec![vec![1], vec![10]];
6003 let erased =
6004 concat.run_concat_report_mode(concat_inputs.clone(), config, ExecutorMode::ErasedOnly);
6005 let typed =
6006 concat.run_concat_report_mode(concat_inputs.clone(), config, ExecutorMode::TypedOnly);
6007 let auto = concat.run_concat_report_mode(concat_inputs, config, ExecutorMode::Auto);
6008 assert_eq!(
6009 typed.map(execution_observation),
6010 erased.map(execution_observation)
6011 );
6012 assert_eq!(
6013 auto.map(execution_observation),
6014 Err(StreamError::EventLimitExceeded { limit: 1 })
6015 );
6016
6017 let interleave =
6018 GraphDsl::create(|builder| builder.add(Interleave::<i64>::new(2, 1))).unwrap();
6019 let interleave_inputs = vec![vec![1], vec![10]];
6020 let erased = interleave.run_interleave_report_mode(
6021 interleave_inputs.clone(),
6022 1,
6023 false,
6024 config,
6025 ExecutorMode::ErasedOnly,
6026 );
6027 let typed = interleave.run_interleave_report_mode(
6028 interleave_inputs.clone(),
6029 1,
6030 false,
6031 config,
6032 ExecutorMode::TypedOnly,
6033 );
6034 let auto = interleave.run_interleave_report_mode(
6035 interleave_inputs,
6036 1,
6037 false,
6038 config,
6039 ExecutorMode::Auto,
6040 );
6041 assert_eq!(
6042 typed.map(execution_observation),
6043 erased.map(execution_observation)
6044 );
6045 assert_eq!(
6046 auto.map(execution_observation),
6047 Err(StreamError::EventLimitExceeded { limit: 1 })
6048 );
6049 }
6050
6051 #[test]
6061 fn merge_latest_blueprint_sequential_reuse_is_independent() {
6062 let graph = merge_latest_graph_exec();
6063 let input_a: Vec<(u64, u64)> = (0..5).map(|i| (i, i + 100)).collect();
6064 let input_b: Vec<(u64, u64)> = (10..15).map(|i| (i, i + 200)).collect();
6065
6066 let result_a_typed = graph
6068 .run_with_input_mode(input_a.clone(), ExecutorMode::TypedOnly)
6069 .unwrap();
6070 let result_b_typed = graph
6071 .run_with_input_mode(input_b.clone(), ExecutorMode::TypedOnly)
6072 .unwrap();
6073
6074 let result_a_erased = graph
6076 .run_with_input_mode(input_a, ExecutorMode::ErasedOnly)
6077 .unwrap();
6078 let result_b_erased = graph
6079 .run_with_input_mode(input_b, ExecutorMode::ErasedOnly)
6080 .unwrap();
6081
6082 assert_eq!(
6083 result_a_typed, result_a_erased,
6084 "sequential run A: typed and erased disagree"
6085 );
6086 assert_eq!(
6087 result_b_typed, result_b_erased,
6088 "sequential run B: typed and erased disagree"
6089 );
6090 assert_ne!(
6092 result_a_typed, result_b_typed,
6093 "runs A and B should differ (different inputs)"
6094 );
6095 }
6096
6097 #[test]
6103 fn merge_latest_blueprint_concurrent_reuse_is_independent() {
6104 use std::sync::Arc as StdArc;
6105
6106 let graph = StdArc::new(merge_latest_graph_exec());
6108
6109 let input_a: Vec<(u64, u64)> = (0..50).map(|i| (i, i + 1_000)).collect();
6110 let input_b: Vec<(u64, u64)> = (100..150).map(|i| (i, i + 2_000)).collect();
6111
6112 let graph_a = StdArc::clone(&graph);
6113 let graph_b = StdArc::clone(&graph);
6114 let ia = input_a.clone();
6115 let ib = input_b.clone();
6116
6117 let handle_a =
6118 std::thread::spawn(move || graph_a.run_with_input_mode(ia, ExecutorMode::TypedOnly));
6119 let handle_b =
6120 std::thread::spawn(move || graph_b.run_with_input_mode(ib, ExecutorMode::TypedOnly));
6121
6122 let result_a = handle_a.join().expect("thread A panicked").unwrap();
6123 let result_b = handle_b.join().expect("thread B panicked").unwrap();
6124
6125 let ref_a = graph
6127 .run_with_input_mode(input_a, ExecutorMode::ErasedOnly)
6128 .unwrap();
6129 let ref_b = graph
6130 .run_with_input_mode(input_b, ExecutorMode::ErasedOnly)
6131 .unwrap();
6132
6133 assert_eq!(
6134 result_a, ref_a,
6135 "concurrent run A: typed and erased disagree"
6136 );
6137 assert_eq!(
6138 result_b, ref_b,
6139 "concurrent run B: typed and erased disagree"
6140 );
6141 assert_ne!(result_a, result_b, "concurrent runs must be independent");
6143 }
6144}
6145
6146#[cfg(test)]
6147fn run_threaded_async_linear_count<I, T>(
6148 input: I,
6149 segments: TypedLinearSegments<T>,
6150 config: AsyncBoundaryExecutionConfig,
6151) -> StreamResult<FusedTerminalReport<usize>>
6152where
6153 I: IntoIterator<Item = T> + Send,
6154 I::IntoIter: Send,
6155 T: Send + 'static,
6156{
6157 let channels = segments.segments.len() + 1;
6158 let mut senders = Vec::with_capacity(channels);
6159 let mut receivers = Vec::with_capacity(channels);
6160 for _ in 0..channels {
6161 let (sender, receiver) = mpsc::sync_channel(config.buffer_size);
6162 senders.push(sender);
6163 receivers.push(Some(receiver));
6164 }
6165
6166 let first_sender = senders
6167 .first()
6168 .expect("at least one async segment channel")
6169 .clone();
6170 let mut final_receiver = Some(
6171 receivers
6172 .last_mut()
6173 .expect("at least one async segment channel")
6174 .take()
6175 .expect("final receiver is present"),
6176 );
6177 let events = AtomicUsize::new(0);
6178 let async_boundary_crossings = AtomicUsize::new(0);
6179
6180 let result = thread::scope(|scope| {
6181 let input = input.into_iter().map(Ok::<T, StreamError>);
6182 let source = scope.spawn(move || feed_threaded_async_linear_input(input, first_sender));
6183 let mut workers = Vec::with_capacity(segments.segments.len());
6184
6185 for (index, steps) in segments.segments.iter().enumerate() {
6186 let input = receivers[index].take().expect("worker receiver is present");
6187 let output = senders[index + 1].clone();
6188 let has_boundary_after = index + 1 < segments.segments.len();
6189 let events = &events;
6190 let async_boundary_crossings = &async_boundary_crossings;
6191 workers.push(scope.spawn(move || {
6192 run_threaded_async_linear_segment(
6193 steps,
6194 input,
6195 output,
6196 has_boundary_after,
6197 events,
6198 async_boundary_crossings,
6199 config,
6200 )
6201 }));
6202 }
6203 drop(senders);
6204
6205 let final_rx = final_receiver.take().expect("final receiver present");
6206 let mut count = 0;
6207 let mut terminal_error = None;
6208 loop {
6209 match final_rx.recv() {
6210 Ok(AsyncLinearMessage::Item(_)) => count += 1,
6211 Ok(AsyncLinearMessage::Done) => break,
6212 Ok(AsyncLinearMessage::Failed(error)) => {
6213 terminal_error = Some(error);
6214 break;
6215 }
6216 Err(_) => {
6217 terminal_error = Some(StreamError::AbruptTermination);
6218 break;
6219 }
6220 }
6221 }
6222 drop(final_rx);
6223
6224 let mut worker_error = join_threaded_async_linear_worker(source)?;
6225 for worker in workers {
6226 if worker_error.is_none() {
6227 worker_error = join_threaded_async_linear_worker(worker)?;
6228 } else {
6229 let _ = join_threaded_async_linear_worker(worker);
6230 }
6231 }
6232
6233 match (terminal_error, worker_error) {
6234 (Some(error), _) if error != StreamError::AbruptTermination => return Err(error),
6235 (_, Some(error)) => return Err(error),
6236 (Some(error), None) => return Err(error),
6237 (None, None) => {}
6238 }
6239
6240 Ok(count)
6241 });
6242
6243 Ok(FusedTerminalReport::new(
6244 result?,
6245 events.load(Ordering::Relaxed),
6246 async_boundary_crossings.load(Ordering::Relaxed),
6247 FusedPlanReport::empty(FusedExecutorTier::AsyncBoundary),
6248 ))
6249}
6250
6251fn run_ractor_async_linear_count<I, T>(
6252 input: I,
6253 segments: TypedLinearSegments<T>,
6254 config: AsyncBoundaryExecutionConfig,
6255) -> StreamResult<FusedTerminalReport<usize>>
6256where
6257 I: IntoIterator<Item = T> + Send,
6258 I::IntoIter: Send + 'static,
6259 T: Send + 'static,
6260{
6261 if config.buffer_size == 0 {
6262 return Err(StreamError::GraphValidation(
6263 "ractor async boundary execution requires buffer_size greater than zero".into(),
6264 ));
6265 }
6266
6267 let input = input.into_iter().map(Ok::<T, StreamError>);
6268 let runtime = ractor_boundary_runtime()?;
6269 if tokio::runtime::Handle::try_current().is_ok() {
6270 thread::scope(|scope| {
6271 let handle = scope.spawn(move || {
6272 runtime.block_on(run_ractor_async_linear_count_on_runtime(
6273 input, segments, config,
6274 ))
6275 });
6276 handle.join().map_err(|_| {
6277 StreamError::Failed("ractor async boundary runtime thread panicked".into())
6278 })?
6279 })
6280 } else {
6281 runtime.block_on(run_ractor_async_linear_count_on_runtime(
6282 input, segments, config,
6283 ))
6284 }
6285}
6286
6287async fn run_ractor_async_linear_count_on_runtime<I, T>(
6288 input: I,
6289 segments: TypedLinearSegments<T>,
6290 config: AsyncBoundaryExecutionConfig,
6291) -> StreamResult<FusedTerminalReport<usize>>
6292where
6293 I: Iterator<Item = StreamResult<T>> + Send + 'static,
6294 T: Send + 'static,
6295{
6296 let channels = segments.segments.len() + 1;
6297 let mut senders = Vec::with_capacity(channels);
6298 let mut receivers = Vec::with_capacity(channels);
6299 for _ in 0..channels {
6300 let (sender, receiver) = ractor::concurrency::mpsc_bounded(config.buffer_size);
6301 senders.push(sender);
6302 receivers.push(Some(receiver));
6303 }
6304
6305 let first_sender = senders
6306 .first()
6307 .expect("at least one async segment channel")
6308 .clone();
6309 let mut final_receiver = receivers
6310 .last_mut()
6311 .expect("at least one async segment channel")
6312 .take()
6313 .expect("final receiver is present");
6314 let events = Arc::new(AtomicUsize::new(0));
6315 let async_boundary_crossings = Arc::new(AtomicUsize::new(0));
6316
6317 let (source_ref, source_handle) = Actor::spawn(
6318 None,
6319 RactorBoundarySourceActor::<I, T>::new(),
6320 RactorBoundarySourceState {
6321 input: Some(input),
6322 output: first_sender,
6323 },
6324 )
6325 .await
6326 .map_err(ractor_spawn_error)?;
6327
6328 let mut actors = Vec::with_capacity(segments.segments.len() + 1);
6329 actors.push((source_ref, source_handle));
6330
6331 for (index, steps) in segments.segments.into_iter().enumerate() {
6332 let input = receivers[index].take().expect("worker receiver is present");
6333 let output = senders[index + 1].clone();
6334 let has_boundary_after = index + 1 < channels - 1;
6335 let (worker_ref, worker_handle) = match Actor::spawn(
6336 None,
6337 RactorLinearSegmentActor::<T>::new(),
6338 RactorLinearSegmentState {
6339 steps,
6340 input,
6341 output,
6342 has_boundary_after,
6343 events: Arc::clone(&events),
6344 async_boundary_crossings: Arc::clone(&async_boundary_crossings),
6345 config,
6346 },
6347 )
6348 .await
6349 {
6350 Ok(actor) => actor,
6351 Err(error) => {
6352 let error = ractor_spawn_error(error);
6353 stop_ractor_async_linear_actors(&actors);
6354 let _ = join_ractor_async_linear_actors(actors).await;
6355 return Err(error);
6356 }
6357 };
6358 actors.push((worker_ref, worker_handle));
6359 }
6360 drop(senders);
6361
6362 let mut start_error = None;
6363 for (actor, _) in &actors {
6364 if actor.send_message(RactorBoundaryCommand::Run).is_err() {
6365 start_error = Some(StreamError::AbruptTermination);
6366 break;
6367 }
6368 }
6369 if let Some(error) = start_error {
6370 stop_ractor_async_linear_actors(&actors);
6371 let _ = join_ractor_async_linear_actors(actors).await;
6372 return Err(error);
6373 }
6374
6375 let mut count = 0;
6376 let mut terminal_error = None;
6377 loop {
6378 match final_receiver.recv().await {
6379 Some(AsyncLinearMessage::Item(_)) => count += 1,
6380 Some(AsyncLinearMessage::Done) => break,
6381 Some(AsyncLinearMessage::Failed(error)) => {
6382 terminal_error = Some(error);
6383 break;
6384 }
6385 None => {
6386 terminal_error = Some(StreamError::AbruptTermination);
6387 break;
6388 }
6389 }
6390 }
6391 drop(final_receiver);
6392
6393 stop_ractor_async_linear_actors(&actors);
6394 let actor_error = join_ractor_async_linear_actors(actors).await;
6395
6396 match (terminal_error, actor_error) {
6397 (Some(error), _) if error != StreamError::AbruptTermination => return Err(error),
6398 (_, Some(error)) => return Err(error),
6399 (Some(error), None) => return Err(error),
6400 (None, None) => {}
6401 }
6402
6403 Ok(FusedTerminalReport::new(
6404 count,
6405 events.load(Ordering::Relaxed),
6406 async_boundary_crossings.load(Ordering::Relaxed),
6407 FusedPlanReport::empty(FusedExecutorTier::AsyncBoundary),
6408 ))
6409}
6410
6411struct RactorLinearSegmentActor<T> {
6412 _marker: PhantomData<fn() -> T>,
6413}
6414
6415impl<T> RactorLinearSegmentActor<T> {
6416 fn new() -> Self {
6417 Self {
6418 _marker: PhantomData,
6419 }
6420 }
6421}
6422
6423struct RactorLinearSegmentState<T> {
6424 steps: Vec<TypedLinearStep<T>>,
6425 input: ractor::concurrency::MpscReceiver<AsyncLinearMessage<T>>,
6426 output: ractor::concurrency::MpscSender<AsyncLinearMessage<T>>,
6427 has_boundary_after: bool,
6428 events: Arc<AtomicUsize>,
6429 async_boundary_crossings: Arc<AtomicUsize>,
6430 config: AsyncBoundaryExecutionConfig,
6431}
6432
6433impl<T> Actor for RactorLinearSegmentActor<T>
6434where
6435 T: Send + 'static,
6436{
6437 type Msg = RactorBoundaryCommand;
6438 type State = RactorLinearSegmentState<T>;
6439 type Arguments = RactorLinearSegmentState<T>;
6440
6441 async fn pre_start(
6442 &self,
6443 _myself: ActorRef<Self::Msg>,
6444 args: Self::Arguments,
6445 ) -> Result<Self::State, ActorProcessingErr> {
6446 Ok(args)
6447 }
6448
6449 async fn handle(
6450 &self,
6451 myself: ActorRef<Self::Msg>,
6452 message: Self::Msg,
6453 state: &mut Self::State,
6454 ) -> Result<(), ActorProcessingErr> {
6455 match message {
6456 RactorBoundaryCommand::Run => {
6457 run_ractor_async_linear_segment(state)
6458 .await
6459 .map_err(actor_processing_error)?;
6460 myself.stop(None);
6461 }
6462 }
6463 Ok(())
6464 }
6465}
6466
6467async fn run_ractor_async_linear_segment<T>(
6468 state: &mut RactorLinearSegmentState<T>,
6469) -> StreamResult<()>
6470where
6471 T: Send + 'static,
6472{
6473 loop {
6474 match state.input.recv().await {
6475 Some(AsyncLinearMessage::Item(item)) => {
6476 let result =
6477 run_async_linear_item(item, &state.steps, &state.events, state.config.fused)
6478 .and_then(|item| {
6479 if state.has_boundary_after {
6480 bump_fused_event_atomic(&state.events, state.config.fused)?;
6481 state
6482 .async_boundary_crossings
6483 .fetch_add(1, Ordering::Relaxed);
6484 bump_fused_event_atomic(&state.events, state.config.fused)?;
6485 }
6486 Ok(item)
6487 });
6488
6489 match result {
6490 Ok(item) => state
6491 .output
6492 .send(AsyncLinearMessage::Item(item))
6493 .await
6494 .map_err(|_| StreamError::AbruptTermination)?,
6495 Err(error) => {
6496 let _ = state
6497 .output
6498 .send(AsyncLinearMessage::Failed(error.clone()))
6499 .await;
6500 return Err(error);
6501 }
6502 }
6503 }
6504 Some(AsyncLinearMessage::Done) => {
6505 state
6506 .output
6507 .send(AsyncLinearMessage::Done)
6508 .await
6509 .map_err(|_| StreamError::AbruptTermination)?;
6510 return Ok(());
6511 }
6512 Some(AsyncLinearMessage::Failed(error)) => {
6513 let _ = state
6514 .output
6515 .send(AsyncLinearMessage::Failed(error.clone()))
6516 .await;
6517 return Err(error);
6518 }
6519 None => return Err(StreamError::AbruptTermination),
6520 }
6521 }
6522}
6523
6524async fn join_ractor_async_linear_actor(
6525 handle: ractor::concurrency::JoinHandle<()>,
6526) -> StreamResult<()> {
6527 handle.await.map_err(|error| {
6528 StreamError::Failed(format!("ractor async boundary actor task failed: {error}"))
6529 })
6530}
6531
6532fn stop_ractor_async_linear_actors(
6533 actors: &[(
6534 ActorRef<RactorBoundaryCommand>,
6535 ractor::concurrency::JoinHandle<()>,
6536 )],
6537) {
6538 for (actor, _) in actors {
6539 actor.stop(None);
6540 }
6541}
6542
6543#[cfg_attr(not(test), allow(dead_code))]
6544async fn join_ractor_async_linear_actors(
6545 actors: Vec<(
6546 ActorRef<RactorBoundaryCommand>,
6547 ractor::concurrency::JoinHandle<()>,
6548 )>,
6549) -> Option<StreamError> {
6550 let mut actor_error = None;
6551 for (_, handle) in actors {
6552 let result = join_ractor_async_linear_actor(handle).await;
6553 if actor_error.is_some() {
6554 continue;
6555 }
6556 if let Err(error) = result {
6557 actor_error = Some(error);
6558 }
6559 }
6560 actor_error
6561}
6562
6563fn ractor_spawn_error(error: ractor::SpawnErr) -> StreamError {
6564 StreamError::Failed(format!(
6565 "ractor async boundary actor failed to spawn: {error}"
6566 ))
6567}
6568
6569fn actor_processing_error(error: StreamError) -> ActorProcessingErr {
6570 Box::new(error)
6571}
6572
6573#[cfg(test)]
6574fn feed_threaded_async_linear_input<I, T>(
6575 input: I,
6576 output: mpsc::SyncSender<AsyncLinearMessage<T>>,
6577) -> StreamResult<()>
6578where
6579 I: IntoIterator<Item = StreamResult<T>>,
6580{
6581 for item in input {
6582 match item {
6583 Ok(item) => output
6584 .send(AsyncLinearMessage::Item(item))
6585 .map_err(|_| StreamError::AbruptTermination)?,
6586 Err(error) => {
6587 let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
6588 return Err(error);
6589 }
6590 }
6591 }
6592 output
6593 .send(AsyncLinearMessage::Done)
6594 .map_err(|_| StreamError::AbruptTermination)
6595}
6596
6597#[cfg(test)]
6598fn run_threaded_async_linear_segment<T>(
6599 steps: &[TypedLinearStep<T>],
6600 input: mpsc::Receiver<AsyncLinearMessage<T>>,
6601 output: mpsc::SyncSender<AsyncLinearMessage<T>>,
6602 has_boundary_after: bool,
6603 events: &AtomicUsize,
6604 async_boundary_crossings: &AtomicUsize,
6605 config: AsyncBoundaryExecutionConfig,
6606) -> StreamResult<()>
6607where
6608 T: Send + 'static,
6609{
6610 loop {
6611 match input.recv().map_err(|_| StreamError::AbruptTermination)? {
6612 AsyncLinearMessage::Item(item) => {
6613 let result =
6614 run_async_linear_item(item, steps, events, config.fused).and_then(|item| {
6615 if has_boundary_after {
6616 bump_fused_event_atomic(events, config.fused)?;
6617 async_boundary_crossings.fetch_add(1, Ordering::Relaxed);
6618 bump_fused_event_atomic(events, config.fused)?;
6619 }
6620 output
6621 .send(AsyncLinearMessage::Item(item))
6622 .map_err(|_| StreamError::AbruptTermination)
6623 });
6624 if let Err(error) = result {
6625 let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
6626 return Err(error);
6627 }
6628 }
6629 AsyncLinearMessage::Done => {
6630 output
6631 .send(AsyncLinearMessage::Done)
6632 .map_err(|_| StreamError::AbruptTermination)?;
6633 return Ok(());
6634 }
6635 AsyncLinearMessage::Failed(error) => {
6636 let _ = output.send(AsyncLinearMessage::Failed(error.clone()));
6637 return Err(error);
6638 }
6639 }
6640 }
6641}
6642
6643fn run_async_linear_item<T>(
6644 mut item: T,
6645 steps: &[TypedLinearStep<T>],
6646 events: &AtomicUsize,
6647 config: FusedExecutionConfig,
6648) -> StreamResult<T>
6649where
6650 T: Send + 'static,
6651{
6652 for step in steps {
6653 bump_fused_event_atomic(events, config)?;
6654 match step {
6655 TypedLinearStep::Pass => {}
6656 TypedLinearStep::Map(mapper) => {
6657 item = mapper(item);
6658 }
6659 TypedLinearStep::AsyncBoundary => {
6660 return Err(StreamError::GraphValidation(
6661 "async boundary execution expects pre-split linear segments".into(),
6662 ));
6663 }
6664 }
6665 bump_fused_event_atomic(events, config)?;
6666 }
6667 Ok(item)
6668}
6669
6670#[cfg(test)]
6671fn join_threaded_async_linear_worker(
6672 handle: thread::ScopedJoinHandle<'_, StreamResult<()>>,
6673) -> StreamResult<Option<StreamError>> {
6674 match handle.join() {
6675 Ok(Ok(())) => Ok(None),
6676 Ok(Err(error)) => Ok(Some(error)),
6677 Err(_) => Err(StreamError::Failed("async boundary worker panicked".into())),
6678 }
6679}
6680
6681fn bump_fused_event_atomic(events: &AtomicUsize, config: FusedExecutionConfig) -> StreamResult<()> {
6682 let events = events.fetch_add(1, Ordering::Relaxed) + 1;
6683 if events > config.event_limit {
6684 return Err(StreamError::EventLimitExceeded {
6685 limit: config.event_limit,
6686 });
6687 }
6688 Ok(())
6689}
6690
6691fn add_typed_helper_events(
6692 events: &mut usize,
6693 config: FusedExecutionConfig,
6694 count: usize,
6695) -> StreamResult<()> {
6696 let next = events
6697 .checked_add(count)
6698 .ok_or_else(|| StreamError::Failed("typed helper event count overflow".into()))?;
6699 if next > config.event_limit {
6700 return Err(StreamError::EventLimitExceeded {
6701 limit: config.event_limit,
6702 });
6703 }
6704 *events = next;
6705 Ok(())
6706}
6707
6708fn typed_fan_in_success_events(output_len: usize, input_count: usize) -> StreamResult<usize> {
6709 output_len
6710 .checked_mul(3)
6711 .and_then(|events| events.checked_add(input_count))
6712 .and_then(|events| events.checked_add(1))
6713 .ok_or_else(|| StreamError::Failed("typed helper event count overflow".into()))
6714}
6715
6716fn direct_single_fan_in_stage<'a, T>(
6717 stages: &'a [super::builder::StageRecord],
6718 edges: &[super::builder::Edge],
6719 shape_inlets: &[AnyInlet],
6720 shape_outlet: &AnyOutlet,
6721) -> Option<&'a super::builder::StageRecord>
6722where
6723 T: 'static,
6724{
6725 if stages.len() != 1 || !edges.is_empty() {
6726 return None;
6727 }
6728 let stage = stages.first()?;
6729 let element_type = TypeId::of::<T>();
6730 if stage.spec.inlets.len() != shape_inlets.len()
6731 || stage.spec.outlets.len() != 1
6732 || stage.spec.outlets[0].id() != shape_outlet.id()
6733 || stage.spec.outlets[0].type_id() != element_type
6734 || shape_outlet.type_id() != element_type
6735 || stage
6736 .spec
6737 .inlets
6738 .iter()
6739 .map(AnyInlet::id)
6740 .ne(shape_inlets.iter().map(AnyInlet::id))
6741 || stage
6742 .spec
6743 .inlets
6744 .iter()
6745 .any(|inlet| inlet.type_id() != element_type)
6746 || shape_inlets
6747 .iter()
6748 .any(|inlet| inlet.type_id() != element_type)
6749 {
6750 return None;
6751 }
6752 Some(stage)
6753}
6754
6755fn run_typed_scheduled_fan_in<T>(
6756 inputs: Vec<Vec<T>>,
6757 schedule: &[usize],
6758 config: FusedExecutionConfig,
6759) -> StreamResult<FusedExecutionReport<T>>
6760where
6761 T: Send + 'static,
6762{
6763 let output_capacity = inputs.iter().map(Vec::len).sum();
6764 let events = typed_fan_in_success_events(output_capacity, inputs.len())?;
6765 let mut checked_events = 0;
6766 add_typed_helper_events(&mut checked_events, config, events)?;
6767
6768 let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6769 let mut schedule_index = 0;
6770 let mut output = Vec::with_capacity(output_capacity);
6771 while output.len() < output_capacity {
6772 let input_index = next_scheduled_input(&queues, schedule, &mut schedule_index)
6773 .ok_or_else(|| StreamError::GraphValidation("no runnable fan-in input".into()))?;
6774 let item = queues[input_index]
6775 .next()
6776 .expect("scheduled input had an item");
6777 output.push(item);
6778 }
6779
6780 Ok(FusedExecutionReport::new(
6781 output,
6782 events,
6783 0,
6784 FusedPlanReport::empty(FusedExecutorTier::TypedFanIn),
6785 ))
6786}
6787
6788fn run_typed_concat<T>(
6789 inputs: Vec<Vec<T>>,
6790 config: FusedExecutionConfig,
6791) -> StreamResult<FusedExecutionReport<T>>
6792where
6793 T: Send + 'static,
6794{
6795 let output_capacity = inputs.iter().map(Vec::len).sum();
6796 let events = typed_fan_in_success_events(output_capacity, inputs.len())?;
6797 let mut checked_events = 0;
6798 add_typed_helper_events(&mut checked_events, config, events)?;
6799
6800 let mut output = Vec::with_capacity(output_capacity);
6801 for input in inputs {
6802 output.extend(input);
6803 }
6804
6805 Ok(FusedExecutionReport::new(
6806 output,
6807 events,
6808 0,
6809 FusedPlanReport::empty(FusedExecutorTier::TypedConcat),
6810 ))
6811}
6812
6813fn run_typed_interleave<T>(
6814 inputs: Vec<Vec<T>>,
6815 segment_size: usize,
6816 eager_close: bool,
6817 config: FusedExecutionConfig,
6818) -> StreamResult<FusedExecutionReport<T>>
6819where
6820 T: Send + 'static,
6821{
6822 let output_capacity = inputs.iter().map(Vec::len).sum();
6823 let mut events = 0;
6824 if !eager_close {
6825 let total_events = typed_fan_in_success_events(output_capacity, inputs.len())?;
6826 add_typed_helper_events(&mut events, config, total_events)?;
6827 }
6828
6829 let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6830 let mut completed = vec![false; queues.len()];
6831 let mut output = Vec::with_capacity(output_capacity);
6832
6833 for (index, queue) in queues.iter().enumerate() {
6834 if queue.len() == 0 {
6835 completed[index] = true;
6836 if eager_close {
6837 add_typed_helper_events(&mut events, config, 2)?;
6838 return Ok(FusedExecutionReport::new(
6839 output,
6840 events,
6841 0,
6842 FusedPlanReport::empty(FusedExecutorTier::TypedInterleave),
6843 ));
6844 }
6845 }
6846 }
6847
6848 let mut current = 0usize;
6849 while completed.iter().any(|done| !done) {
6850 if completed[current] {
6851 current = next_open_index(&completed, current)
6852 .ok_or_else(|| StreamError::GraphValidation("no open interleave input".into()))?;
6853 continue;
6854 }
6855
6856 let mut emitted = 0usize;
6857 while emitted < segment_size {
6858 match queues[current].next() {
6859 Some(item) => {
6860 if eager_close {
6861 add_typed_helper_events(&mut events, config, 3)?;
6862 }
6863 output.push(item);
6864 emitted += 1;
6865 }
6866 None => {
6867 completed[current] = true;
6868 if eager_close {
6869 add_typed_helper_events(&mut events, config, 2)?;
6870 return Ok(FusedExecutionReport::new(
6871 output,
6872 events,
6873 0,
6874 FusedPlanReport::empty(FusedExecutorTier::TypedInterleave),
6875 ));
6876 }
6877 break;
6878 }
6879 }
6880 }
6881
6882 if completed.iter().all(|done| *done) {
6883 break;
6884 }
6885 current = next_open_index(&completed, current)
6886 .ok_or_else(|| StreamError::GraphValidation("no open interleave input".into()))?;
6887 }
6888
6889 Ok(FusedExecutionReport::new(
6890 output,
6891 events,
6892 0,
6893 FusedPlanReport::empty(FusedExecutorTier::TypedInterleave),
6894 ))
6895}
6896
6897impl<T> GraphBlueprint<FanInShape<T, T>>
6898where
6899 T: Clone + Send + 'static,
6900{
6901 pub fn run_fan_in(&self, inputs: Vec<Vec<T>>) -> StreamResult<Vec<T>> {
6902 Ok(self
6903 .run_fan_in_report(inputs, FusedExecutionConfig::default())?
6904 .output)
6905 }
6906
6907 pub fn run_fan_in_report(
6908 &self,
6909 inputs: Vec<Vec<T>>,
6910 config: FusedExecutionConfig,
6911 ) -> StreamResult<FusedExecutionReport<T>> {
6912 self.run_fan_in_report_mode(inputs, config, ExecutorMode::Auto)
6913 }
6914
6915 pub(crate) fn run_fan_in_report_mode(
6916 &self,
6917 inputs: Vec<Vec<T>>,
6918 config: FusedExecutionConfig,
6919 mode: ExecutorMode,
6920 ) -> StreamResult<FusedExecutionReport<T>> {
6921 if inputs.len() != self.shape.inlet_count() {
6922 return Err(StreamError::GraphValidation(format!(
6923 "expected {} input streams, got {}",
6924 self.shape.inlet_count(),
6925 inputs.len()
6926 )));
6927 }
6928
6929 let selection = self.resolve_executor_selection(mode)?;
6930 let mode = selection.mode;
6931
6932 if mode != ExecutorMode::ErasedOnly {
6933 if let Some(schedule) = self.typed_prioritized_fan_in_schedule() {
6934 let mut report = run_typed_scheduled_fan_in(inputs, &schedule, config)?;
6935 report.plan = selection.plan_report(FusedExecutorTier::TypedFanIn);
6936 return Ok(report);
6937 }
6938 if mode == ExecutorMode::TypedOnly {
6939 return Err(self.typed_unsupported_error(&selection));
6940 }
6941 }
6942
6943 let mut report = self.run_fan_in_report_erased(inputs, config)?;
6944 report.plan = selection.erased_plan_report();
6945 Ok(report)
6946 }
6947
6948 fn typed_prioritized_fan_in_schedule(&self) -> Option<Vec<usize>> {
6949 let shape_inlets = self.shape.inlets();
6950 let shape_outlet = self.shape.outlet().erase();
6951 let stage = direct_single_fan_in_stage::<T>(
6952 &self.stages,
6953 &self.edges,
6954 &shape_inlets,
6955 &shape_outlet,
6956 )?;
6957 let StageKind::MergePrioritized { weights } = &stage.spec.kind else {
6958 return None;
6959 };
6960 if weights.len() != self.shape.inlet_count() || weights.contains(&0) {
6961 return None;
6962 }
6963 Some(
6964 weights
6965 .iter()
6966 .enumerate()
6967 .flat_map(|(index, weight)| std::iter::repeat_n(index, *weight))
6968 .collect(),
6969 )
6970 }
6971
6972 fn run_fan_in_report_erased(
6973 &self,
6974 inputs: Vec<Vec<T>>,
6975 config: FusedExecutionConfig,
6976 ) -> StreamResult<FusedExecutionReport<T>> {
6977 let output_capacity = inputs.iter().map(Vec::len).sum();
6978 let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
6979 let schedule = self.fan_in_schedule();
6980 let mut schedule_index = 0;
6981 let outlet = self.shape.outlet().id();
6982 let mut executor = FusedExecutor::new(self, config);
6983 let mut output = Vec::with_capacity(output_capacity);
6984 let mut completed = vec![false; queues.len()];
6985
6986 {
6987 let mut output_sink = VecOutputSink {
6988 output: &mut output,
6989 };
6990 for (index, queue) in queues.iter().enumerate() {
6991 if queue.len() == 0 {
6992 executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
6993 completed[index] = true;
6994 }
6995 }
6996 while queues.iter().any(|queue| queue.len() > 0) {
6997 let input_index = next_scheduled_input(&queues, &schedule, &mut schedule_index)
6998 .ok_or_else(|| {
6999 StreamError::GraphValidation("no runnable fan-in input".into())
7000 })?;
7001 let item = queues[input_index]
7002 .next()
7003 .expect("scheduled input had an item");
7004 executor.deliver(
7005 self.shape.inlet(input_index)?.id(),
7006 datum(item),
7007 outlet,
7008 &mut output_sink,
7009 )?;
7010 if queues[input_index].len() == 0 && !completed[input_index] {
7011 executor.complete(
7012 self.shape.inlet(input_index)?.id(),
7013 outlet,
7014 &mut output_sink,
7015 )?;
7016 completed[input_index] = true;
7017 }
7018 }
7019 }
7020
7021 Ok(FusedExecutionReport::new(
7022 output,
7023 executor.events,
7024 executor.async_boundary_crossings,
7025 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7026 ))
7027 }
7028
7029 fn fan_in_schedule(&self) -> Vec<usize> {
7030 self.stages
7031 .iter()
7032 .find_map(|stage| match &stage.spec.kind {
7033 StageKind::MergePrioritized { weights }
7034 if weights.len() == self.shape.inlet_count()
7035 && stage.spec.outlets.len() == 1
7036 && stage.spec.outlets[0].id() == self.shape.outlet().id()
7037 && stage.spec.inlets.iter().map(AnyInlet::id).eq(self
7038 .shape
7039 .inlets()
7040 .iter()
7041 .map(|inlet| inlet.id())) =>
7042 {
7043 Some(
7044 weights
7045 .iter()
7046 .enumerate()
7047 .flat_map(|(index, weight)| std::iter::repeat_n(index, *weight))
7048 .collect(),
7049 )
7050 }
7051 _ => None,
7052 })
7053 .unwrap_or_else(|| (0..self.shape.inlet_count()).collect())
7054 }
7055
7056 pub fn run_concat(&self, inputs: Vec<Vec<T>>) -> StreamResult<Vec<T>> {
7057 Ok(self
7058 .run_concat_report(inputs, FusedExecutionConfig::default())?
7059 .output)
7060 }
7061
7062 pub fn run_concat_report(
7063 &self,
7064 inputs: Vec<Vec<T>>,
7065 config: FusedExecutionConfig,
7066 ) -> StreamResult<FusedExecutionReport<T>> {
7067 self.run_concat_report_mode(inputs, config, ExecutorMode::Auto)
7068 }
7069
7070 pub(crate) fn run_concat_report_mode(
7071 &self,
7072 inputs: Vec<Vec<T>>,
7073 config: FusedExecutionConfig,
7074 mode: ExecutorMode,
7075 ) -> StreamResult<FusedExecutionReport<T>> {
7076 if inputs.len() != self.shape.inlet_count() {
7077 return Err(StreamError::GraphValidation(format!(
7078 "expected {} input streams, got {}",
7079 self.shape.inlet_count(),
7080 inputs.len()
7081 )));
7082 }
7083
7084 let selection = self.resolve_executor_selection(mode)?;
7085 let mode = selection.mode;
7086
7087 if mode != ExecutorMode::ErasedOnly {
7088 if self.typed_concat_supported() {
7089 let mut report = run_typed_concat(inputs, config)?;
7090 report.plan = selection.plan_report(FusedExecutorTier::TypedConcat);
7091 return Ok(report);
7092 }
7093 if mode == ExecutorMode::TypedOnly {
7094 return Err(self.typed_unsupported_error(&selection));
7095 }
7096 }
7097
7098 let mut report = self.run_concat_report_erased(inputs, config)?;
7099 report.plan = selection.erased_plan_report();
7100 Ok(report)
7101 }
7102
7103 fn typed_concat_supported(&self) -> bool {
7104 let shape_inlets = self.shape.inlets();
7105 let shape_outlet = self.shape.outlet().erase();
7106 direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
7107 .is_some_and(|stage| matches!(&stage.spec.kind, StageKind::Concat))
7108 }
7109
7110 fn run_concat_report_erased(
7111 &self,
7112 inputs: Vec<Vec<T>>,
7113 config: FusedExecutionConfig,
7114 ) -> StreamResult<FusedExecutionReport<T>> {
7115 let output_capacity = inputs.iter().map(Vec::len).sum();
7116 let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
7117 let outlet = self.shape.outlet().id();
7118 let mut executor = FusedExecutor::new(self, config);
7119 let mut output = Vec::with_capacity(output_capacity);
7120
7121 {
7122 let mut output_sink = VecOutputSink {
7123 output: &mut output,
7124 };
7125 for (index, queue) in queues.iter_mut().enumerate() {
7126 for item in queue.by_ref() {
7127 executor.deliver(
7128 self.shape.inlet(index)?.id(),
7129 datum(item),
7130 outlet,
7131 &mut output_sink,
7132 )?;
7133 }
7134 executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
7135 }
7136 }
7137
7138 Ok(FusedExecutionReport::new(
7139 output,
7140 executor.events,
7141 executor.async_boundary_crossings,
7142 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7143 ))
7144 }
7145
7146 pub fn run_or_else(&self, primary: Vec<T>, secondary: Vec<T>) -> StreamResult<Vec<T>> {
7147 Ok(self
7148 .run_or_else_report(primary, secondary, FusedExecutionConfig::default())?
7149 .output)
7150 }
7151
7152 pub fn run_or_else_report(
7153 &self,
7154 primary: Vec<T>,
7155 secondary: Vec<T>,
7156 config: FusedExecutionConfig,
7157 ) -> StreamResult<FusedExecutionReport<T>> {
7158 if self.shape.inlet_count() != 2 {
7159 return Err(StreamError::GraphValidation(format!(
7160 "or-else helper expected 2 inlets, got {}",
7161 self.shape.inlet_count()
7162 )));
7163 }
7164
7165 let selection = self.resolve_executor_selection(ExecutorMode::Auto)?;
7166 if selection.mode == ExecutorMode::TypedOnly {
7167 return Err(self.typed_unsupported_error(&selection));
7168 }
7169
7170 let primary = primary.into_iter();
7171 let secondary = secondary.into_iter();
7172 let primary_inlet = self.shape.inlet(0)?.id();
7173 let secondary_inlet = self.shape.inlet(1)?.id();
7174 let outlet = self.shape.outlet().id();
7175 let mut executor = FusedExecutor::new(self, config);
7176 let mut output = Vec::new();
7177 let mut primary_emitted = false;
7178
7179 {
7180 let mut output_sink = VecOutputSink {
7181 output: &mut output,
7182 };
7183 for item in primary {
7184 primary_emitted = true;
7185 executor.deliver(primary_inlet, datum(item), outlet, &mut output_sink)?;
7186 }
7187 executor.complete(primary_inlet, outlet, &mut output_sink)?;
7188
7189 if !primary_emitted {
7190 for item in secondary {
7191 executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
7192 }
7193 }
7194 executor.complete(secondary_inlet, outlet, &mut output_sink)?;
7195 }
7196
7197 Ok(FusedExecutionReport::new(
7198 output,
7199 executor.events,
7200 executor.async_boundary_crossings,
7201 selection.erased_plan_report(),
7202 ))
7203 }
7204
7205 pub fn run_or_else_secondary_first(
7206 &self,
7207 primary: Vec<T>,
7208 secondary: Vec<T>,
7209 ) -> StreamResult<Vec<T>> {
7210 Ok(self
7211 .run_or_else_secondary_first_report(
7212 primary,
7213 secondary,
7214 FusedExecutionConfig::default(),
7215 )?
7216 .output)
7217 }
7218
7219 pub fn run_or_else_secondary_first_report(
7220 &self,
7221 primary: Vec<T>,
7222 secondary: Vec<T>,
7223 config: FusedExecutionConfig,
7224 ) -> StreamResult<FusedExecutionReport<T>> {
7225 if self.shape.inlet_count() != 2 {
7226 return Err(StreamError::GraphValidation(format!(
7227 "or-else helper expected 2 inlets, got {}",
7228 self.shape.inlet_count()
7229 )));
7230 }
7231
7232 let selection = self.resolve_executor_selection(ExecutorMode::Auto)?;
7233 if selection.mode == ExecutorMode::TypedOnly {
7234 return Err(self.typed_unsupported_error(&selection));
7235 }
7236
7237 let primary_inlet = self.shape.inlet(0)?.id();
7238 let secondary_inlet = self.shape.inlet(1)?.id();
7239 let outlet = self.shape.outlet().id();
7240 let mut executor = FusedExecutor::new(self, config);
7241 let mut output = Vec::new();
7242
7243 {
7244 let mut output_sink = VecOutputSink {
7245 output: &mut output,
7246 };
7247 for item in secondary {
7248 executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
7249 }
7250 for item in primary {
7251 executor.deliver(primary_inlet, datum(item), outlet, &mut output_sink)?;
7252 }
7253 executor.complete(primary_inlet, outlet, &mut output_sink)?;
7254 executor.complete(secondary_inlet, outlet, &mut output_sink)?;
7255 }
7256
7257 Ok(FusedExecutionReport::new(
7258 output,
7259 executor.events,
7260 executor.async_boundary_crossings,
7261 selection.erased_plan_report(),
7262 ))
7263 }
7264
7265 pub fn run_or_else_secondary_closed_first(&self, secondary: Vec<T>) -> StreamResult<Vec<T>> {
7266 if self.shape.inlet_count() != 2 {
7267 return Err(StreamError::GraphValidation(format!(
7268 "or-else helper expected 2 inlets, got {}",
7269 self.shape.inlet_count()
7270 )));
7271 }
7272
7273 let selection = self.resolve_executor_selection(ExecutorMode::Auto)?;
7274 if selection.mode == ExecutorMode::TypedOnly {
7275 return Err(self.typed_unsupported_error(&selection));
7276 }
7277
7278 let primary_inlet = self.shape.inlet(0)?.id();
7279 let secondary_inlet = self.shape.inlet(1)?.id();
7280 let outlet = self.shape.outlet().id();
7281 let mut executor = FusedExecutor::new(self, FusedExecutionConfig::default());
7282 let mut output = Vec::new();
7283
7284 {
7285 let mut output_sink = VecOutputSink {
7286 output: &mut output,
7287 };
7288 for item in secondary {
7289 executor.deliver(secondary_inlet, datum(item), outlet, &mut output_sink)?;
7290 }
7291 executor.complete(secondary_inlet, outlet, &mut output_sink)?;
7292 executor.complete(primary_inlet, outlet, &mut output_sink)?;
7293 }
7294
7295 Ok(output)
7296 }
7297
7298 pub fn run_interleave(
7299 &self,
7300 inputs: Vec<Vec<T>>,
7301 segment_size: usize,
7302 eager_close: bool,
7303 ) -> StreamResult<Vec<T>> {
7304 Ok(self
7305 .run_interleave_report(
7306 inputs,
7307 segment_size,
7308 eager_close,
7309 FusedExecutionConfig::default(),
7310 )?
7311 .output)
7312 }
7313
7314 pub fn run_interleave_report(
7315 &self,
7316 inputs: Vec<Vec<T>>,
7317 segment_size: usize,
7318 eager_close: bool,
7319 config: FusedExecutionConfig,
7320 ) -> StreamResult<FusedExecutionReport<T>> {
7321 self.run_interleave_report_mode(
7322 inputs,
7323 segment_size,
7324 eager_close,
7325 config,
7326 ExecutorMode::Auto,
7327 )
7328 }
7329
7330 pub(crate) fn run_interleave_report_mode(
7331 &self,
7332 inputs: Vec<Vec<T>>,
7333 segment_size: usize,
7334 eager_close: bool,
7335 config: FusedExecutionConfig,
7336 mode: ExecutorMode,
7337 ) -> StreamResult<FusedExecutionReport<T>> {
7338 if inputs.len() != self.shape.inlet_count() {
7339 return Err(StreamError::GraphValidation(format!(
7340 "expected {} input streams, got {}",
7341 self.shape.inlet_count(),
7342 inputs.len()
7343 )));
7344 }
7345 if segment_size == 0 {
7346 return Err(StreamError::GraphValidation(
7347 "interleave segment size must be greater than zero".into(),
7348 ));
7349 }
7350
7351 let selection = self.resolve_executor_selection(mode)?;
7352 let mode = selection.mode;
7353
7354 if mode != ExecutorMode::ErasedOnly {
7355 if self.typed_interleave_supported(segment_size, eager_close) {
7356 let mut report = run_typed_interleave(inputs, segment_size, eager_close, config)?;
7357 report.plan = selection.plan_report(FusedExecutorTier::TypedInterleave);
7358 return Ok(report);
7359 }
7360 if mode == ExecutorMode::TypedOnly {
7361 return Err(self.typed_unsupported_error(&selection));
7362 }
7363 }
7364
7365 let mut report =
7366 self.run_interleave_report_erased(inputs, segment_size, eager_close, config)?;
7367 report.plan = selection.erased_plan_report();
7368 Ok(report)
7369 }
7370
7371 fn typed_interleave_supported(&self, segment_size: usize, eager_close: bool) -> bool {
7372 let shape_inlets = self.shape.inlets();
7373 let shape_outlet = self.shape.outlet().erase();
7374 direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
7375 .is_some_and(|stage| {
7376 matches!(
7377 &stage.spec.kind,
7378 StageKind::Interleave {
7379 segment_size: stage_segment_size,
7380 eager_close: stage_eager_close,
7381 } if *stage_segment_size == segment_size && *stage_eager_close == eager_close
7382 )
7383 })
7384 }
7385
7386 fn run_interleave_report_erased(
7387 &self,
7388 inputs: Vec<Vec<T>>,
7389 segment_size: usize,
7390 eager_close: bool,
7391 config: FusedExecutionConfig,
7392 ) -> StreamResult<FusedExecutionReport<T>> {
7393 let output_capacity = inputs.iter().map(Vec::len).sum();
7394 let mut queues: Vec<_> = inputs.into_iter().map(Vec::into_iter).collect();
7395 let mut completed = vec![false; queues.len()];
7396 let outlet = self.shape.outlet().id();
7397 let mut executor = FusedExecutor::new(self, config);
7398 let mut output = Vec::with_capacity(output_capacity);
7399
7400 {
7401 let mut output_sink = VecOutputSink {
7402 output: &mut output,
7403 };
7404 for (index, queue) in queues.iter().enumerate() {
7405 if queue.len() == 0 {
7406 executor.complete(self.shape.inlet(index)?.id(), outlet, &mut output_sink)?;
7407 completed[index] = true;
7408 if eager_close {
7409 return Ok(FusedExecutionReport::new(
7410 output,
7411 executor.events,
7412 executor.async_boundary_crossings,
7413 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7414 ));
7415 }
7416 }
7417 }
7418
7419 let mut current = 0usize;
7420 while completed.iter().any(|done| !done) {
7421 if completed[current] {
7422 current = next_open_index(&completed, current).ok_or_else(|| {
7423 StreamError::GraphValidation("no open interleave input".into())
7424 })?;
7425 continue;
7426 }
7427
7428 let mut emitted = 0usize;
7429 while emitted < segment_size {
7430 match queues[current].next() {
7431 Some(item) => {
7432 executor.deliver(
7433 self.shape.inlet(current)?.id(),
7434 datum(item),
7435 outlet,
7436 &mut output_sink,
7437 )?;
7438 emitted += 1;
7439 }
7440 None => {
7441 executor.complete(
7442 self.shape.inlet(current)?.id(),
7443 outlet,
7444 &mut output_sink,
7445 )?;
7446 completed[current] = true;
7447 if eager_close {
7448 return Ok(FusedExecutionReport::new(
7449 output,
7450 executor.events,
7451 executor.async_boundary_crossings,
7452 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7453 ));
7454 }
7455 break;
7456 }
7457 }
7458 }
7459
7460 if completed.iter().all(|done| *done) {
7461 break;
7462 }
7463 current = next_open_index(&completed, current).ok_or_else(|| {
7464 StreamError::GraphValidation("no open interleave input".into())
7465 })?;
7466 }
7467 }
7468
7469 Ok(FusedExecutionReport::new(
7470 output,
7471 executor.events,
7472 executor.async_boundary_crossings,
7473 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7474 ))
7475 }
7476}
7477
7478impl<T> GraphBlueprint<MergePreferredShape<T>>
7479where
7480 T: Clone + Send + 'static,
7481{
7482 pub fn run_merge_preferred(
7483 &self,
7484 preferred: Vec<T>,
7485 secondary_inputs: Vec<Vec<T>>,
7486 ) -> StreamResult<Vec<T>> {
7487 Ok(self
7488 .run_merge_preferred_report(
7489 preferred,
7490 secondary_inputs,
7491 FusedExecutionConfig::default(),
7492 )?
7493 .output)
7494 }
7495
7496 pub fn run_merge_preferred_report(
7497 &self,
7498 preferred: Vec<T>,
7499 secondary_inputs: Vec<Vec<T>>,
7500 config: FusedExecutionConfig,
7501 ) -> StreamResult<FusedExecutionReport<T>> {
7502 self.run_merge_preferred_report_mode(
7503 preferred,
7504 secondary_inputs,
7505 config,
7506 ExecutorMode::Auto,
7507 )
7508 }
7509
7510 pub(crate) fn run_merge_preferred_report_mode(
7511 &self,
7512 preferred: Vec<T>,
7513 secondary_inputs: Vec<Vec<T>>,
7514 config: FusedExecutionConfig,
7515 mode: ExecutorMode,
7516 ) -> StreamResult<FusedExecutionReport<T>> {
7517 if secondary_inputs.len() != self.shape.secondary_count() {
7518 return Err(StreamError::GraphValidation(format!(
7519 "expected {} secondary input streams, got {}",
7520 self.shape.secondary_count(),
7521 secondary_inputs.len()
7522 )));
7523 }
7524
7525 let selection = self.resolve_executor_selection(mode)?;
7526 let mode = selection.mode;
7527
7528 if mode != ExecutorMode::ErasedOnly {
7529 if self.typed_merge_preferred_supported() {
7530 let mut report = run_typed_merge_preferred(preferred, secondary_inputs, config)?;
7531 report.plan = selection.plan_report(FusedExecutorTier::TypedMergePreferred);
7532 return Ok(report);
7533 }
7534 if mode == ExecutorMode::TypedOnly {
7535 return Err(self.typed_unsupported_error(&selection));
7536 }
7537 }
7538
7539 let mut report =
7540 self.run_merge_preferred_report_erased(preferred, secondary_inputs, config)?;
7541 report.plan = selection.erased_plan_report();
7542 Ok(report)
7543 }
7544
7545 fn typed_merge_preferred_supported(&self) -> bool {
7546 let shape_inlets = self.shape.inlets();
7547 let shape_outlet = self.shape.outlet().erase();
7548 direct_single_fan_in_stage::<T>(&self.stages, &self.edges, &shape_inlets, &shape_outlet)
7549 .is_some_and(|stage| matches!(&stage.spec.kind, StageKind::MergePreferred))
7550 }
7551
7552 fn run_merge_preferred_report_erased(
7553 &self,
7554 preferred: Vec<T>,
7555 secondary_inputs: Vec<Vec<T>>,
7556 config: FusedExecutionConfig,
7557 ) -> StreamResult<FusedExecutionReport<T>> {
7558 let output_capacity =
7559 preferred.len() + secondary_inputs.iter().map(Vec::len).sum::<usize>();
7560 let mut preferred = preferred.into_iter();
7561 let mut secondary: Vec<_> = secondary_inputs.into_iter().map(Vec::into_iter).collect();
7562 let secondary_schedule = (0..secondary.len()).collect::<Vec<_>>();
7563 let mut secondary_index = 0;
7564 let outlet = self.shape.outlet().id();
7565 let preferred_inlet = self.shape.preferred().id();
7566 let mut executor = FusedExecutor::new(self, config);
7567 let mut output = Vec::with_capacity(output_capacity);
7568 let mut preferred_completed = false;
7569 let mut secondary_completed = vec![false; secondary.len()];
7570
7571 {
7572 let mut output_sink = VecOutputSink {
7573 output: &mut output,
7574 };
7575 if preferred.len() == 0 {
7576 executor.complete(preferred_inlet, outlet, &mut output_sink)?;
7577 preferred_completed = true;
7578 }
7579 for (index, queue) in secondary.iter().enumerate() {
7580 if queue.len() == 0 {
7581 executor.complete(
7582 self.shape.secondary(index)?.id(),
7583 outlet,
7584 &mut output_sink,
7585 )?;
7586 secondary_completed[index] = true;
7587 }
7588 }
7589 while preferred.len() > 0 || secondary.iter().any(|queue| queue.len() > 0) {
7590 if let Some(item) = preferred.next() {
7591 executor.deliver(preferred_inlet, datum(item), outlet, &mut output_sink)?;
7592 if preferred.len() == 0 && !preferred_completed {
7593 executor.complete(preferred_inlet, outlet, &mut output_sink)?;
7594 preferred_completed = true;
7595 }
7596 continue;
7597 }
7598
7599 let input_index =
7600 next_scheduled_input(&secondary, &secondary_schedule, &mut secondary_index)
7601 .ok_or_else(|| {
7602 StreamError::GraphValidation("no runnable secondary input".into())
7603 })?;
7604 let item = secondary[input_index]
7605 .next()
7606 .expect("scheduled secondary input had an item");
7607 executor.deliver(
7608 self.shape.secondary(input_index)?.id(),
7609 datum(item),
7610 outlet,
7611 &mut output_sink,
7612 )?;
7613 if secondary[input_index].len() == 0 && !secondary_completed[input_index] {
7614 executor.complete(
7615 self.shape.secondary(input_index)?.id(),
7616 outlet,
7617 &mut output_sink,
7618 )?;
7619 secondary_completed[input_index] = true;
7620 }
7621 }
7622 }
7623
7624 Ok(FusedExecutionReport::new(
7625 output,
7626 executor.events,
7627 executor.async_boundary_crossings,
7628 self.plan_report(FusedExecutorTier::Erased, Vec::new()),
7629 ))
7630 }
7631}
7632
7633fn run_typed_merge_preferred<T>(
7634 preferred: Vec<T>,
7635 secondary_inputs: Vec<Vec<T>>,
7636 config: FusedExecutionConfig,
7637) -> StreamResult<FusedExecutionReport<T>>
7638where
7639 T: Send + 'static,
7640{
7641 let output_capacity = preferred.len() + secondary_inputs.iter().map(Vec::len).sum::<usize>();
7642 let input_count = secondary_inputs.len() + 1;
7643 let events = typed_fan_in_success_events(output_capacity, input_count)?;
7644 let mut checked_events = 0;
7645 add_typed_helper_events(&mut checked_events, config, events)?;
7646
7647 let mut output = Vec::with_capacity(output_capacity);
7648 output.extend(preferred);
7649
7650 let mut secondary: Vec<_> = secondary_inputs.into_iter().map(Vec::into_iter).collect();
7651 let secondary_schedule = (0..secondary.len()).collect::<Vec<_>>();
7652 let mut secondary_index = 0;
7653 while output.len() < output_capacity {
7654 let input_index =
7655 next_scheduled_input(&secondary, &secondary_schedule, &mut secondary_index)
7656 .ok_or_else(|| {
7657 StreamError::GraphValidation("no runnable secondary input".into())
7658 })?;
7659 let item = secondary[input_index]
7660 .next()
7661 .expect("scheduled secondary input had an item");
7662 output.push(item);
7663 }
7664
7665 Ok(FusedExecutionReport::new(
7666 output,
7667 events,
7668 0,
7669 FusedPlanReport::empty(FusedExecutorTier::TypedMergePreferred),
7670 ))
7671}
7672
7673fn next_scheduled_input<I>(
7674 queues: &[I],
7675 schedule: &[usize],
7676 schedule_index: &mut usize,
7677) -> Option<usize>
7678where
7679 I: ExactSizeIterator,
7680{
7681 if schedule.is_empty() {
7682 return queues.iter().position(|queue| queue.len() > 0);
7683 }
7684
7685 for _ in 0..schedule.len() {
7686 let index = schedule[*schedule_index % schedule.len()];
7687 *schedule_index += 1;
7688 if queues.get(index).is_some_and(|queue| queue.len() > 0) {
7689 return Some(index);
7690 }
7691 }
7692
7693 queues.iter().position(|queue| queue.len() > 0)
7694}
7695
7696fn next_open_index(completed: &[bool], current: usize) -> Option<usize> {
7697 if completed.is_empty() {
7698 return None;
7699 }
7700 let len = completed.len();
7701 for offset in 1..=len {
7702 let index = (current + offset) % len;
7703 if !completed[index] {
7704 return Some(index);
7705 }
7706 }
7707 None
7708}
7709
7710pub(crate) trait FusedOutputSink<Out> {
7719 fn emit(&mut self, value: Out) -> StreamResult<()>;
7720}
7721
7722pub(crate) struct VecOutputSink<'a, Out> {
7723 pub(crate) output: &'a mut Vec<Out>,
7724}
7725
7726impl<Out> FusedOutputSink<Out> for VecOutputSink<'_, Out> {
7727 fn emit(&mut self, value: Out) -> StreamResult<()> {
7728 self.output.push(value);
7729 Ok(())
7730 }
7731}
7732
7733pub(crate) struct CountOutputSink {
7734 pub(crate) count: usize,
7735}
7736
7737impl<Out> FusedOutputSink<Out> for CountOutputSink {
7738 fn emit(&mut self, _value: Out) -> StreamResult<()> {
7739 self.count += 1;
7740 Ok(())
7741 }
7742}
7743
7744pub(crate) struct FoldOutputSink<Acc, F> {
7745 pub(crate) accumulator: Option<Acc>,
7746 pub(crate) fold: F,
7747}
7748
7749impl<Out, Acc, F> FusedOutputSink<Out> for FoldOutputSink<Acc, F>
7750where
7751 F: FnMut(Acc, Out) -> Acc,
7752{
7753 fn emit(&mut self, value: Out) -> StreamResult<()> {
7754 let accumulator = self
7755 .accumulator
7756 .take()
7757 .expect("fold accumulator is present while executor is running");
7758 self.accumulator = Some((self.fold)(accumulator, value));
7759 Ok(())
7760 }
7761}
7762
7763impl<Acc, F> FoldOutputSink<Acc, F> {
7764 pub(crate) fn finish(mut self) -> Acc {
7765 self.accumulator
7766 .take()
7767 .expect("fold accumulator is present after executor finishes")
7768 }
7769}
7770
7771#[derive(Debug)]
7783pub(crate) struct FusedExecutor<'a, S: Shape> {
7784 graph: &'a GraphBlueprint<S>,
7785 pub(crate) edge_by_outlet: HashMap<PortId, PortId>,
7787 pub(crate) edge_by_inlet: HashMap<PortId, PortId>,
7789 pub(crate) stage_by_inlet: HashMap<PortId, usize>,
7791 pub(crate) stage_by_outlet: HashMap<PortId, usize>,
7793 stage_states: Vec<StageState>,
7794 pub(crate) opaque_logics: Vec<Option<GraphStageLogic>>,
7795 timer_mailbox: Arc<StageTimerMailbox>,
7796 timers_enabled: bool,
7797 config: FusedExecutionConfig,
7798 pub(crate) events: usize,
7799 pub(crate) async_boundary_crossings: usize,
7800 cancelled_outlets: HashSet<PortId>,
7801 event_stack: Vec<FusedEvent>,
7802 draining: bool,
7803 has_cycle: bool,
7804 metrics: FusedMetricsRun,
7805 metrics_enabled: bool,
7806}
7807
7808#[derive(Debug)]
7809enum StageState {
7810 Stateless,
7811 Broadcast {
7812 cancelled_outlets: Vec<bool>,
7813 live_outlets: usize,
7814 },
7815 Balance {
7816 next: usize,
7817 cancelled_outlets: Vec<bool>,
7818 live_outlets: usize,
7819 },
7820 Merge {
7821 open_inputs: usize,
7822 eager_complete: bool,
7823 completed: bool,
7824 },
7825 OrElse {
7826 primary_emitted: bool,
7827 buffer: VecDeque<DatumValue>,
7828 primary_closed: bool,
7829 secondary_closed: bool,
7830 completed: bool,
7831 },
7832 Zip {
7833 left_inlet: PortId,
7834 right_inlet: PortId,
7835 left: VecDeque<DatumValue>,
7836 right: VecDeque<DatumValue>,
7837 left_pending_complete: bool,
7838 right_pending_complete: bool,
7839 completed: bool,
7840 },
7841 Unzip {
7842 fast_path: Option<UnzipFanInFastPath>,
7843 zip_fast_path: Option<UnzipZipFastPath>,
7844 demand: [bool; 2],
7845 cancelled: [bool; 2],
7846 upstream_closed: bool,
7847 },
7848 MergeSorted {
7849 left: VecDeque<DatumValue>,
7850 right: VecDeque<DatumValue>,
7851 left_closed: bool,
7852 right_closed: bool,
7853 pending: VecDeque<DatumValue>,
7854 completed: bool,
7855 },
7856 MergeSequence {
7857 next_sequence: u64,
7858 pending: Vec<(u64, DatumValue)>,
7859 completed_count: usize,
7860 output_buffer: VecDeque<DatumValue>,
7861 completed: bool,
7862 },
7863 MergeLatest {
7864 latest: Vec<Option<DatumValue>>,
7865 seen_count: usize,
7866 completed_count: usize,
7867 pending: VecDeque<DatumValue>,
7868 completed: bool,
7869 },
7870 Partition {
7871 pending: Option<(usize, DatumValue)>,
7872 upstream_closed: bool,
7873 demand: Vec<bool>,
7874 cancelled: Vec<bool>,
7875 output_count: usize,
7876 completed: bool,
7877 },
7878}
7879
7880#[derive(Clone, Copy, Debug)]
7881struct UnzipFanInFastPath {
7882 fan_in_stage_index: usize,
7883 target_inlet_indices: [usize; 2],
7887}
7888
7889#[derive(Clone, Copy, Debug)]
7890struct UnzipZipFastPath {
7891 zip_stage_index: usize,
7892}
7893
7894enum StageEmissions {
7895 None,
7896 One(PortId, DatumValue),
7897 Two((PortId, DatumValue), (PortId, DatumValue)),
7898 Many(Vec<(PortId, DatumValue)>),
7899}
7900
7901impl StageEmissions {
7902 fn len(&self) -> usize {
7903 match self {
7904 Self::None => 0,
7905 Self::One(..) => 1,
7906 Self::Two(..) => 2,
7907 Self::Many(emissions) => emissions.len(),
7908 }
7909 }
7910}
7911
7912struct StageTransition {
7913 emissions: StageEmissions,
7914 completed_outlets: Vec<PortId>,
7915 cancelled_inlets: Vec<PortId>,
7916}
7917
7918impl StageTransition {
7919 fn none() -> Self {
7920 Self {
7921 emissions: StageEmissions::None,
7922 completed_outlets: Vec::new(),
7923 cancelled_inlets: Vec::new(),
7924 }
7925 }
7926
7927 fn emit(emissions: StageEmissions) -> Self {
7928 Self {
7929 emissions,
7930 completed_outlets: Vec::new(),
7931 cancelled_inlets: Vec::new(),
7932 }
7933 }
7934
7935 fn with_completion(mut self, completed_outlets: Vec<PortId>) -> Self {
7936 self.completed_outlets = completed_outlets;
7937 self
7938 }
7939
7940 fn with_cancellations(mut self, cancelled_inlets: Vec<PortId>) -> Self {
7941 self.cancelled_inlets = cancelled_inlets;
7942 self
7943 }
7944}
7945
7946#[derive(Debug)]
7947enum FusedEvent {
7948 Deliver { inlet: PortId, value: DatumValue },
7949 CompleteInlet { inlet: PortId },
7950 Request { outlet: PortId },
7951 DownstreamFinish { outlet: PortId },
7952 Emit { outlet: PortId, value: DatumValue },
7953 CompleteOutlet { outlet: PortId },
7954 CancelInlet { inlet: PortId },
7955}
7956
7957fn graph_has_cycle<S: Shape>(
7958 graph: &GraphBlueprint<S>,
7959 stage_by_inlet: &HashMap<PortId, usize>,
7960 stage_by_outlet: &HashMap<PortId, usize>,
7961) -> bool {
7962 let mut adjacency = vec![Vec::new(); graph.stages.len()];
7963 for edge in &graph.edges {
7964 let Some(from) = stage_by_outlet.get(&edge.outlet).copied() else {
7965 continue;
7966 };
7967 let Some(to) = stage_by_inlet.get(&edge.inlet).copied() else {
7968 continue;
7969 };
7970 adjacency[from].push(to);
7971 }
7972
7973 #[derive(Clone, Copy, PartialEq, Eq)]
7974 enum Visit {
7975 New,
7976 Active,
7977 Done,
7978 }
7979
7980 fn visit(node: usize, adjacency: &[Vec<usize>], marks: &mut [Visit]) -> bool {
7981 marks[node] = Visit::Active;
7982 for &next in &adjacency[node] {
7983 if marks[next] == Visit::Active {
7984 return true;
7985 }
7986 if marks[next] == Visit::New && visit(next, adjacency, marks) {
7987 return true;
7988 }
7989 }
7990 marks[node] = Visit::Done;
7991 false
7992 }
7993
7994 let mut marks = vec![Visit::New; graph.stages.len()];
7995 for node in 0..graph.stages.len() {
7996 if marks[node] == Visit::New && visit(node, &adjacency, &mut marks) {
7997 return true;
7998 }
7999 }
8000 false
8001}
8002
8003impl<'a, S: Shape> FusedExecutor<'a, S> {
8004 fn is_outlet_cancelled(&self, outlet: PortId) -> bool {
8005 !self.cancelled_outlets.is_empty() && self.cancelled_outlets.contains(&outlet)
8006 }
8007
8008 fn new(graph: &'a GraphBlueprint<S>, config: FusedExecutionConfig) -> Self {
8009 let edge_by_outlet = graph
8010 .edges
8011 .iter()
8012 .map(|edge| (edge.outlet, edge.inlet))
8013 .collect();
8014 let edge_by_inlet = graph
8015 .edges
8016 .iter()
8017 .map(|edge| (edge.inlet, edge.outlet))
8018 .collect();
8019 let mut stage_by_inlet = HashMap::new();
8020 let mut stage_by_outlet = HashMap::new();
8021 for (index, stage) in graph.stages.iter().enumerate() {
8022 for inlet in &stage.spec.inlets {
8023 stage_by_inlet.insert(inlet.id(), index);
8024 }
8025 for outlet in &stage.spec.outlets {
8026 stage_by_outlet.insert(outlet.id(), index);
8027 }
8028 }
8029
8030 let timer_mailbox = Arc::new(StageTimerMailbox::default());
8031 let timer_materializer = Arc::new(OnceLock::new());
8032 let mut opaque_logics: Vec<_> = graph
8033 .stages
8034 .iter()
8035 .map(|stage| stage.logic_factory.as_ref().map(|factory| factory()))
8036 .collect();
8037 for (stage_index, logic) in opaque_logics.iter_mut().enumerate() {
8038 if let Some(logic) = logic {
8039 logic.attach_timer_runtime(StageTimerRuntime::new(
8040 stage_index,
8041 Arc::clone(&timer_mailbox),
8042 Arc::clone(&timer_materializer),
8043 ));
8044 }
8045 }
8046 let timers_enabled = opaque_logics.iter().any(|logic| {
8047 logic
8048 .as_ref()
8049 .is_some_and(GraphStageLogic::has_timer_handler)
8050 });
8051
8052 let stage_states = graph
8053 .stages
8054 .iter()
8055 .map(|stage| match stage.spec.kind {
8056 StageKind::Broadcast => StageState::Broadcast {
8057 cancelled_outlets: vec![false; stage.spec.outlets.len()],
8058 live_outlets: stage.spec.outlets.len(),
8059 },
8060 StageKind::Balance => StageState::Balance {
8061 next: 0,
8062 cancelled_outlets: vec![false; stage.spec.outlets.len()],
8063 live_outlets: stage.spec.outlets.len(),
8064 },
8065 StageKind::Merge => StageState::Merge {
8066 open_inputs: stage.spec.inlets.len(),
8067 eager_complete: false,
8068 completed: false,
8069 },
8070 StageKind::MergePreferred => StageState::Merge {
8071 open_inputs: stage.spec.inlets.len(),
8072 eager_complete: false,
8073 completed: false,
8074 },
8075 StageKind::MergePrioritized { .. } => StageState::Merge {
8076 open_inputs: stage.spec.inlets.len(),
8077 eager_complete: false,
8078 completed: false,
8079 },
8080 StageKind::Concat => StageState::Merge {
8081 open_inputs: stage.spec.inlets.len(),
8082 eager_complete: false,
8083 completed: false,
8084 },
8085 StageKind::Interleave { eager_close, .. } => StageState::Merge {
8086 open_inputs: stage.spec.inlets.len(),
8087 eager_complete: eager_close,
8088 completed: false,
8089 },
8090 StageKind::OrElse { primary_inlet: _ } => StageState::OrElse {
8091 primary_emitted: false,
8092 buffer: VecDeque::new(),
8093 primary_closed: false,
8094 secondary_closed: false,
8095 completed: false,
8096 },
8097 StageKind::Zip(_) => {
8098 if let [left, right] = stage.spec.inlets.as_slice() {
8099 StageState::Zip {
8100 left_inlet: left.id(),
8101 right_inlet: right.id(),
8102 left: VecDeque::new(),
8103 right: VecDeque::new(),
8104 left_pending_complete: false,
8105 right_pending_complete: false,
8106 completed: false,
8107 }
8108 } else {
8109 StageState::Stateless
8110 }
8111 }
8112 StageKind::Unzip { .. } => StageState::Unzip {
8113 fast_path: unzip_fan_in_fast_path(
8114 stage,
8115 graph,
8116 &edge_by_outlet,
8117 &stage_by_inlet,
8118 ),
8119 zip_fast_path: unzip_zip_fast_path(
8120 stage,
8121 graph,
8122 &edge_by_outlet,
8123 &stage_by_inlet,
8124 ),
8125 demand: [false; 2],
8126 cancelled: [false; 2],
8127 upstream_closed: false,
8128 },
8129 StageKind::MergeSorted(_) => StageState::MergeSorted {
8130 left: VecDeque::new(),
8131 right: VecDeque::new(),
8132 left_closed: false,
8133 right_closed: false,
8134 pending: VecDeque::new(),
8135 completed: false,
8136 },
8137 StageKind::MergeSequence { .. } => StageState::MergeSequence {
8138 next_sequence: 0,
8139 pending: Vec::new(),
8140 completed_count: 0,
8141 output_buffer: VecDeque::new(),
8142 completed: false,
8143 },
8144 StageKind::MergeLatest { input_count, .. } => {
8145 let mut latest = Vec::with_capacity(input_count);
8146 for _ in 0..input_count {
8147 latest.push(None);
8148 }
8149 StageState::MergeLatest {
8150 latest,
8151 seen_count: 0,
8152 completed_count: 0,
8153 pending: VecDeque::new(),
8154 completed: false,
8155 }
8156 }
8157 StageKind::Partition { output_count, .. } => StageState::Partition {
8158 pending: None,
8159 upstream_closed: false,
8160 demand: vec![false; output_count],
8161 cancelled: vec![false; output_count],
8162 output_count,
8163 completed: false,
8164 },
8165 _ => StageState::Stateless,
8166 })
8167 .collect();
8168
8169 let has_cycle =
8170 !graph.edges.is_empty() && graph_has_cycle(graph, &stage_by_inlet, &stage_by_outlet);
8171
8172 let metrics = FusedMetricsRun::new(&graph.stage_attribute_reports());
8173 let metrics_enabled = metrics.enabled();
8174 let mut executor = Self {
8175 graph,
8176 edge_by_outlet,
8177 edge_by_inlet,
8178 stage_by_inlet,
8179 stage_by_outlet,
8180 stage_states,
8181 opaque_logics,
8182 timer_mailbox,
8183 timers_enabled,
8184 config,
8185 events: 0,
8186 async_boundary_crossings: 0,
8187 cancelled_outlets: HashSet::new(),
8188 event_stack: Vec::new(),
8189 draining: false,
8190 has_cycle,
8191 metrics,
8192 metrics_enabled,
8193 };
8194 executor.prime_connected_demands();
8195 executor
8196 }
8197
8198 fn deliver<Out>(
8199 &mut self,
8200 inlet: PortId,
8201 value: DatumValue,
8202 graph_outlet: PortId,
8203 output: &mut impl FusedOutputSink<Out>,
8204 ) -> StreamResult<()>
8205 where
8206 Out: Send + 'static,
8207 {
8208 if !self.has_cycle {
8209 return self.process_deliver_direct(inlet, value, graph_outlet, output);
8210 }
8211 self.schedule_event(FusedEvent::Deliver { inlet, value });
8212 self.drain_events(graph_outlet, output)
8213 }
8214
8215 fn complete<Out>(
8216 &mut self,
8217 inlet: PortId,
8218 graph_outlet: PortId,
8219 output: &mut impl FusedOutputSink<Out>,
8220 ) -> StreamResult<()>
8221 where
8222 Out: Send + 'static,
8223 {
8224 if !self.has_cycle {
8225 return self.process_complete_inlet_direct(inlet, graph_outlet, output);
8226 }
8227 self.schedule_event(FusedEvent::CompleteInlet { inlet });
8228 self.drain_events(graph_outlet, output)
8229 }
8230
8231 fn request<Out>(
8232 &mut self,
8233 outlet: PortId,
8234 graph_outlet: PortId,
8235 output: &mut impl FusedOutputSink<Out>,
8236 ) -> StreamResult<()>
8237 where
8238 Out: Send + 'static,
8239 {
8240 if !self.has_cycle {
8241 return self.process_request_direct(outlet, graph_outlet, output);
8242 }
8243 self.schedule_event(FusedEvent::Request { outlet });
8244 self.drain_events(graph_outlet, output)
8245 }
8246
8247 #[cfg_attr(not(test), allow(dead_code))]
8248 fn downstream_finish<Out>(
8249 &mut self,
8250 outlet: PortId,
8251 graph_outlet: PortId,
8252 output: &mut impl FusedOutputSink<Out>,
8253 ) -> StreamResult<()>
8254 where
8255 Out: Send + 'static,
8256 {
8257 if !self.has_cycle {
8258 return self.process_downstream_finish_direct(outlet, graph_outlet, output);
8259 }
8260 self.schedule_event(FusedEvent::DownstreamFinish { outlet });
8261 self.drain_events(graph_outlet, output)
8262 }
8263
8264 fn schedule_event(&mut self, event: FusedEvent) {
8265 self.event_stack.push(event);
8266 }
8267
8268 fn schedule_transition(&mut self, transition: StageTransition) {
8269 for inlet in transition.cancelled_inlets.into_iter().rev() {
8270 self.schedule_event(FusedEvent::CancelInlet { inlet });
8271 }
8272 for outlet in transition.completed_outlets.into_iter().rev() {
8273 if !self.is_outlet_cancelled(outlet) {
8274 self.schedule_event(FusedEvent::CompleteOutlet { outlet });
8275 }
8276 }
8277 self.schedule_emissions_in_order(transition.emissions);
8278 }
8279
8280 fn schedule_emissions_in_order(&mut self, emissions: StageEmissions) {
8281 match emissions {
8282 StageEmissions::None => {}
8283 StageEmissions::One(outlet, value) => {
8284 self.schedule_event(FusedEvent::Emit { outlet, value });
8285 }
8286 StageEmissions::Two((first_outlet, first_value), (second_outlet, second_value)) => {
8287 self.schedule_event(FusedEvent::Emit {
8288 outlet: second_outlet,
8289 value: second_value,
8290 });
8291 self.schedule_event(FusedEvent::Emit {
8292 outlet: first_outlet,
8293 value: first_value,
8294 });
8295 }
8296 StageEmissions::Many(emissions) => {
8297 for (outlet, value) in emissions.into_iter().rev() {
8298 self.schedule_event(FusedEvent::Emit { outlet, value });
8299 }
8300 }
8301 }
8302 }
8303
8304 fn drain_events<Out>(
8305 &mut self,
8306 graph_outlet: PortId,
8307 output: &mut impl FusedOutputSink<Out>,
8308 ) -> StreamResult<()>
8309 where
8310 Out: Send + 'static,
8311 {
8312 if self.draining {
8313 return Ok(());
8314 }
8315
8316 self.draining = true;
8317 let result = (|| {
8318 while let Some(event) = self.event_stack.pop() {
8319 self.process_event(event, graph_outlet, output)?;
8320 }
8321 Ok(())
8322 })();
8323 self.draining = false;
8324 if result.is_err() {
8325 self.event_stack.clear();
8326 }
8327 result
8328 }
8329
8330 fn drain_timer_events_nonblocking<Out>(
8331 &mut self,
8332 graph_outlet: PortId,
8333 output: &mut impl FusedOutputSink<Out>,
8334 ) -> StreamResult<()>
8335 where
8336 Out: Send + 'static,
8337 {
8338 if !self.timers_enabled {
8339 return Ok(());
8340 }
8341 if !self.timer_mailbox.has_pending() {
8342 return Ok(());
8343 }
8344 while let Some(stage_index) = self.timer_mailbox.pop() {
8345 self.process_timer_event(stage_index, graph_outlet, output)?;
8346 }
8347 Ok(())
8348 }
8349
8350 fn drain_timer_events_until_idle<Out>(
8351 &mut self,
8352 graph_outlet: PortId,
8353 output: &mut impl FusedOutputSink<Out>,
8354 ) -> StreamResult<()>
8355 where
8356 Out: Send + 'static,
8357 {
8358 if !self.timers_enabled {
8359 return Ok(());
8360 }
8361 self.drain_timer_events_nonblocking(graph_outlet, output)?;
8362 while self.has_timer_work() || self.timer_mailbox.has_pending() {
8363 let stage_index = self.timer_mailbox.wait();
8364 self.process_timer_event(stage_index, graph_outlet, output)?;
8365 self.drain_timer_events_nonblocking(graph_outlet, output)?;
8366 }
8367 Ok(())
8368 }
8369
8370 fn has_timer_work(&self) -> bool {
8371 self.opaque_logics
8372 .iter()
8373 .any(|logic| logic.as_ref().is_some_and(GraphStageLogic::has_timer_work))
8374 }
8375
8376 fn process_timer_event<Out>(
8377 &mut self,
8378 stage_index: usize,
8379 graph_outlet: PortId,
8380 output: &mut impl FusedOutputSink<Out>,
8381 ) -> StreamResult<()>
8382 where
8383 Out: Send + 'static,
8384 {
8385 let Some(transition) = self.process_stage_timer(stage_index)? else {
8386 return Ok(());
8387 };
8388 self.bump_event()?;
8389 if self.has_cycle {
8390 self.process_transition(transition);
8391 self.drain_events(graph_outlet, output)
8392 } else {
8393 self.process_transition_direct(transition, graph_outlet, output)
8394 }
8395 }
8396
8397 fn process_stage_timer(&mut self, stage_index: usize) -> StreamResult<Option<StageTransition>> {
8398 let Some(stage) = self.graph.stages.get(stage_index) else {
8399 return Ok(None);
8400 };
8401 if !matches!(stage.spec.kind, StageKind::Opaque) {
8402 return Ok(None);
8403 }
8404 let Some(logic) = self
8405 .opaque_logics
8406 .get_mut(stage_index)
8407 .and_then(|logic| logic.as_mut())
8408 else {
8409 return Ok(None);
8410 };
8411
8412 logic.drain_async_callbacks();
8413 let Some(key) = logic.pop_timer_event() else {
8414 return Ok(None);
8415 };
8416
8417 let mut handler = logic.take_timer_handler();
8418 let result = if let Some(ref mut handler) = handler {
8419 handler.on_timer(logic, &key)
8420 } else {
8421 Ok(())
8422 };
8423 if let Some(handler) = handler {
8424 logic.restore_timer_handler(handler);
8425 }
8426 if result.is_err() {
8427 logic.cancel_all_timers();
8428 }
8429 result?;
8430
8431 self.collect_opaque_emissions(stage, stage_index).map(Some)
8432 }
8433
8434 fn process_event<Out>(
8435 &mut self,
8436 event: FusedEvent,
8437 graph_outlet: PortId,
8438 output: &mut impl FusedOutputSink<Out>,
8439 ) -> StreamResult<()>
8440 where
8441 Out: Send + 'static,
8442 {
8443 match event {
8444 FusedEvent::Deliver { inlet, value } => {
8445 self.bump_event()?;
8446 let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
8447 StreamError::GraphValidation(format!(
8448 "inlet {} has no owning stage",
8449 inlet.as_usize()
8450 ))
8451 })?;
8452 let transition = self.process_stage(stage_index, inlet, value)?;
8453 self.process_transition(transition);
8454 Ok(())
8455 }
8456 FusedEvent::CompleteInlet { inlet } => {
8457 self.bump_event()?;
8458 let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
8459 StreamError::GraphValidation(format!(
8460 "inlet {} has no owning stage",
8461 inlet.as_usize()
8462 ))
8463 })?;
8464 let transition = self.process_completion(stage_index, inlet)?;
8465 self.process_transition(transition);
8466 Ok(())
8467 }
8468 FusedEvent::Request { outlet } => {
8469 if self.is_outlet_cancelled(outlet) {
8470 return Ok(());
8471 }
8472 self.bump_event()?;
8473 let Some(stage_index) = self.stage_by_outlet.get(&outlet).copied() else {
8474 return Ok(());
8475 };
8476 let transition = self.process_pull(stage_index, outlet)?;
8477 self.process_transition(transition);
8478 Ok(())
8479 }
8480 FusedEvent::DownstreamFinish { outlet } => {
8481 if !self.cancelled_outlets.insert(outlet) {
8482 return Ok(());
8483 }
8484 self.bump_event()?;
8485 let stage_index = *self.stage_by_outlet.get(&outlet).ok_or_else(|| {
8486 StreamError::GraphValidation(format!(
8487 "outlet {} has no owning stage",
8488 outlet.as_usize()
8489 ))
8490 })?;
8491 let transition = self.process_downstream_finish(stage_index, outlet)?;
8492 self.process_transition(transition);
8493 Ok(())
8494 }
8495 FusedEvent::Emit { outlet, value } => {
8496 self.process_emit_cyclic(outlet, value, graph_outlet, output)
8497 }
8498 FusedEvent::CompleteOutlet { outlet } => {
8499 self.process_complete_outlet_cyclic(outlet, graph_outlet, output)
8500 }
8501 FusedEvent::CancelInlet { inlet } => {
8502 if let Some(outlet) = self.edge_by_inlet.get(&inlet).copied() {
8503 self.schedule_event(FusedEvent::DownstreamFinish { outlet });
8504 }
8505 Ok(())
8506 }
8507 }
8508 }
8509
8510 fn process_transition(&mut self, transition: StageTransition) {
8511 self.schedule_transition(transition);
8512 }
8513
8514 fn process_transition_direct<Out>(
8515 &mut self,
8516 transition: StageTransition,
8517 graph_outlet: PortId,
8518 output: &mut impl FusedOutputSink<Out>,
8519 ) -> StreamResult<()>
8520 where
8521 Out: Send + 'static,
8522 {
8523 self.process_emissions_direct(transition.emissions, graph_outlet, output)?;
8524 for outlet in transition.completed_outlets {
8525 if !self.is_outlet_cancelled(outlet) {
8526 self.process_complete_outlet_direct(outlet, graph_outlet, output)?;
8527 }
8528 }
8529 for inlet in transition.cancelled_inlets {
8530 self.process_cancel_inlet_direct(inlet, graph_outlet, output)?;
8531 }
8532 Ok(())
8533 }
8534
8535 fn process_emissions_direct<Out>(
8536 &mut self,
8537 emissions: StageEmissions,
8538 graph_outlet: PortId,
8539 output: &mut impl FusedOutputSink<Out>,
8540 ) -> StreamResult<()>
8541 where
8542 Out: Send + 'static,
8543 {
8544 match emissions {
8545 StageEmissions::None => Ok(()),
8546 StageEmissions::One(outlet, value) => {
8547 self.process_emit_direct(outlet, value, graph_outlet, output)
8548 }
8549 StageEmissions::Two((first_outlet, first_value), (second_outlet, second_value)) => {
8550 self.process_emit_direct(first_outlet, first_value, graph_outlet, output)?;
8551 self.process_emit_direct(second_outlet, second_value, graph_outlet, output)
8552 }
8553 StageEmissions::Many(emissions) => {
8554 for (outlet, value) in emissions {
8555 self.process_emit_direct(outlet, value, graph_outlet, output)?;
8556 }
8557 Ok(())
8558 }
8559 }
8560 }
8561
8562 fn process_deliver_direct<Out>(
8563 &mut self,
8564 inlet: PortId,
8565 value: DatumValue,
8566 graph_outlet: PortId,
8567 output: &mut impl FusedOutputSink<Out>,
8568 ) -> StreamResult<()>
8569 where
8570 Out: Send + 'static,
8571 {
8572 self.bump_event()?;
8573 let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
8574 StreamError::GraphValidation(format!("inlet {} has no owning stage", inlet.as_usize()))
8575 })?;
8576 let transition = self.process_stage(stage_index, inlet, value)?;
8577 self.process_transition_direct(transition, graph_outlet, output)
8578 }
8579
8580 fn process_complete_inlet_direct<Out>(
8581 &mut self,
8582 inlet: PortId,
8583 graph_outlet: PortId,
8584 output: &mut impl FusedOutputSink<Out>,
8585 ) -> StreamResult<()>
8586 where
8587 Out: Send + 'static,
8588 {
8589 self.bump_event()?;
8590 let stage_index = *self.stage_by_inlet.get(&inlet).ok_or_else(|| {
8591 StreamError::GraphValidation(format!("inlet {} has no owning stage", inlet.as_usize()))
8592 })?;
8593 let transition = self.process_completion(stage_index, inlet)?;
8594 self.process_transition_direct(transition, graph_outlet, output)
8595 }
8596
8597 fn process_request_direct<Out>(
8598 &mut self,
8599 outlet: PortId,
8600 graph_outlet: PortId,
8601 output: &mut impl FusedOutputSink<Out>,
8602 ) -> StreamResult<()>
8603 where
8604 Out: Send + 'static,
8605 {
8606 if self.is_outlet_cancelled(outlet) {
8607 return Ok(());
8608 }
8609 self.bump_event()?;
8610 let Some(stage_index) = self.stage_by_outlet.get(&outlet).copied() else {
8611 return Ok(());
8612 };
8613 let transition = self.process_pull(stage_index, outlet)?;
8614 self.process_transition_direct(transition, graph_outlet, output)
8615 }
8616
8617 fn process_downstream_finish_direct<Out>(
8618 &mut self,
8619 outlet: PortId,
8620 graph_outlet: PortId,
8621 output: &mut impl FusedOutputSink<Out>,
8622 ) -> StreamResult<()>
8623 where
8624 Out: Send + 'static,
8625 {
8626 if !self.cancelled_outlets.insert(outlet) {
8627 return Ok(());
8628 }
8629 self.bump_event()?;
8630 let stage_index = *self.stage_by_outlet.get(&outlet).ok_or_else(|| {
8631 StreamError::GraphValidation(format!(
8632 "outlet {} has no owning stage",
8633 outlet.as_usize()
8634 ))
8635 })?;
8636 let transition = self.process_downstream_finish(stage_index, outlet)?;
8637 self.process_transition_direct(transition, graph_outlet, output)
8638 }
8639
8640 fn process_cancel_inlet_direct<Out>(
8641 &mut self,
8642 inlet: PortId,
8643 graph_outlet: PortId,
8644 output: &mut impl FusedOutputSink<Out>,
8645 ) -> StreamResult<()>
8646 where
8647 Out: Send + 'static,
8648 {
8649 if let Some(outlet) = self.edge_by_inlet.get(&inlet).copied() {
8650 self.process_downstream_finish_direct(outlet, graph_outlet, output)
8651 } else {
8652 Ok(())
8653 }
8654 }
8655
8656 fn process_emit_direct<Out>(
8657 &mut self,
8658 outlet: PortId,
8659 value: DatumValue,
8660 graph_outlet: PortId,
8661 output: &mut impl FusedOutputSink<Out>,
8662 ) -> StreamResult<()>
8663 where
8664 Out: Send + 'static,
8665 {
8666 self.bump_event()?;
8667 if self.is_outlet_cancelled(outlet) {
8668 return Ok(());
8669 }
8670 if outlet == graph_outlet {
8671 output.emit(downcast_datum(value, "emit", || {
8672 format!("outlet#{}", outlet.as_usize())
8673 })?)?;
8674 return self.process_request_direct(outlet, graph_outlet, output);
8675 }
8676
8677 let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
8678 return Err(StreamError::GraphValidation(format!(
8679 "outlet {} is neither connected nor graph output",
8680 outlet.as_usize()
8681 )));
8682 };
8683 let should_repull = self
8684 .stage_by_outlet
8685 .get(&outlet)
8686 .copied()
8687 .is_some_and(|stage_index| {
8688 matches!(
8689 self.graph.stages[stage_index].spec.kind,
8690 StageKind::Opaque | StageKind::Unzip { .. } | StageKind::Partition { .. }
8691 )
8692 });
8693 self.process_deliver_direct(inlet, value, graph_outlet, output)?;
8694 if should_repull {
8695 self.process_request_direct(outlet, graph_outlet, output)?;
8696 }
8697 Ok(())
8698 }
8699
8700 fn process_emit_cyclic<Out>(
8701 &mut self,
8702 outlet: PortId,
8703 value: DatumValue,
8704 graph_outlet: PortId,
8705 output: &mut impl FusedOutputSink<Out>,
8706 ) -> StreamResult<()>
8707 where
8708 Out: Send + 'static,
8709 {
8710 self.bump_event()?;
8711 if self.is_outlet_cancelled(outlet) {
8712 return Ok(());
8713 }
8714 if outlet == graph_outlet {
8715 output.emit(downcast_datum(value, "emit", || {
8716 format!("outlet#{}", outlet.as_usize())
8717 })?)?;
8718 self.schedule_event(FusedEvent::Request { outlet });
8719 return Ok(());
8720 }
8721
8722 let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
8723 return Err(StreamError::GraphValidation(format!(
8724 "outlet {} is neither connected nor graph output",
8725 outlet.as_usize()
8726 )));
8727 };
8728 let should_repull = self
8729 .stage_by_outlet
8730 .get(&outlet)
8731 .copied()
8732 .is_some_and(|stage_index| {
8733 matches!(
8734 self.graph.stages[stage_index].spec.kind,
8735 StageKind::Opaque | StageKind::Unzip { .. } | StageKind::Partition { .. }
8736 )
8737 });
8738 if should_repull {
8739 self.schedule_event(FusedEvent::Request { outlet });
8740 }
8741 self.schedule_event(FusedEvent::Deliver { inlet, value });
8742 Ok(())
8743 }
8744
8745 fn process_complete_outlet_direct<Out>(
8746 &mut self,
8747 outlet: PortId,
8748 graph_outlet: PortId,
8749 output: &mut impl FusedOutputSink<Out>,
8750 ) -> StreamResult<()>
8751 where
8752 Out: Send + 'static,
8753 {
8754 self.bump_event()?;
8755 if outlet == graph_outlet {
8756 return Ok(());
8757 }
8758 let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
8759 return Err(StreamError::GraphValidation(format!(
8760 "outlet {} is neither connected nor graph output",
8761 outlet.as_usize()
8762 )));
8763 };
8764 self.process_complete_inlet_direct(inlet, graph_outlet, output)
8765 }
8766
8767 fn process_complete_outlet_cyclic<Out>(
8768 &mut self,
8769 outlet: PortId,
8770 graph_outlet: PortId,
8771 _output: &mut impl FusedOutputSink<Out>,
8772 ) -> StreamResult<()>
8773 where
8774 Out: Send + 'static,
8775 {
8776 self.bump_event()?;
8777 if outlet == graph_outlet {
8778 return Ok(());
8779 }
8780 let Some(inlet) = self.edge_by_outlet.get(&outlet).copied() else {
8781 return Err(StreamError::GraphValidation(format!(
8782 "outlet {} is neither connected nor graph output",
8783 outlet.as_usize()
8784 )));
8785 };
8786 self.schedule_event(FusedEvent::CompleteInlet { inlet });
8787 Ok(())
8788 }
8789
8790 fn process_stage(
8791 &mut self,
8792 stage_index: usize,
8793 inlet: PortId,
8794 value: DatumValue,
8795 ) -> StreamResult<StageTransition> {
8796 if !self.metrics_enabled {
8797 return self.process_stage_uninstrumented(stage_index, inlet, value);
8798 }
8799 let measurement = self.metrics.begin_node(stage_index, MetricUnits::scalar(1));
8800 let result = self.process_stage_uninstrumented(stage_index, inlet, value);
8801 let output_count = result
8802 .as_ref()
8803 .map_or(0, |transition| transition.emissions.len());
8804 self.metrics
8805 .end_node(measurement, MetricUnits::scalar(output_count as u64));
8806 self.metrics.observe_backpressure(stage_index, output_count);
8807 result
8808 }
8809
8810 fn process_stage_uninstrumented(
8811 &mut self,
8812 stage_index: usize,
8813 inlet: PortId,
8814 value: DatumValue,
8815 ) -> StreamResult<StageTransition> {
8816 let stage = &self.graph.stages[stage_index];
8817 match &stage.spec.kind {
8818 StageKind::Identity => Ok(StageTransition::emit(StageEmissions::One(
8819 single_outlet(stage)?,
8820 value,
8821 ))),
8822 StageKind::Opaque => {
8823 if let Some(logic) = self
8824 .opaque_logics
8825 .get_mut(stage_index)
8826 .and_then(|l| l.as_mut())
8827 {
8828 logic.drain_async_callbacks();
8829 let inlet_ref = stage.spec.inlets.iter().find(|i| i.id() == inlet).cloned();
8830 if let Some(inlet_ref) = inlet_ref {
8831 logic.offer_datum(inlet, value)?;
8832 let mut handler = logic.take_in_handler(inlet);
8833 let result = if let Some(ref mut handler) = handler {
8834 let inlet_any = inlet_ref;
8835 handler.on_push(logic, inlet_any)
8836 } else {
8837 Ok(())
8838 };
8839 if let Some(handler) = handler {
8840 logic.restore_in_handler(inlet, handler);
8841 }
8842 if result.is_err() {
8843 logic.cancel_all_timers();
8844 }
8845 result?;
8846 }
8847 self.collect_opaque_emissions(stage, stage_index)
8848 } else {
8849 Ok(StageTransition::emit(StageEmissions::One(
8850 single_outlet(stage)?,
8851 value,
8852 )))
8853 }
8854 }
8855 StageKind::Map(map) => Ok(StageTransition::emit(StageEmissions::One(
8856 single_outlet(stage)?,
8857 (map.erased)(value)?,
8858 ))),
8859 StageKind::AsyncBoundary => {
8860 self.async_boundary_crossings += 1;
8861 Ok(StageTransition::emit(StageEmissions::One(
8862 single_outlet(stage)?,
8863 value,
8864 )))
8865 }
8866 StageKind::Broadcast => {
8867 broadcast_emissions(&stage.spec.outlets, value).map(StageTransition::emit)
8868 }
8869 StageKind::Balance => {
8870 let outlets = &stage.spec.outlets;
8871 if outlets.is_empty() {
8872 return Err(StreamError::GraphValidation(
8873 "balance has no outlets".into(),
8874 ));
8875 }
8876 let StageState::Balance {
8877 next,
8878 cancelled_outlets,
8879 live_outlets,
8880 } = &mut self.stage_states[stage_index]
8881 else {
8882 return Err(StreamError::GraphValidation(
8883 "balance state is missing".into(),
8884 ));
8885 };
8886 if *live_outlets == 0 {
8887 return Ok(StageTransition::none());
8888 }
8889 let mut selected = None;
8890 for offset in 0..outlets.len() {
8891 let index = (*next + offset) % outlets.len();
8892 if !cancelled_outlets[index] {
8893 selected = Some(index);
8894 break;
8895 }
8896 }
8897 let index = selected.ok_or_else(|| {
8898 StreamError::GraphValidation("balance has no live outlets".into())
8899 })?;
8900 let outlet = outlets[index].id();
8901 *next = (index + 1) % outlets.len();
8902 Ok(StageTransition::emit(StageEmissions::One(outlet, value)))
8903 }
8904 StageKind::Merge | StageKind::MergePreferred | StageKind::MergePrioritized { .. } => {
8905 let StageState::Merge { completed, .. } = &self.stage_states[stage_index] else {
8906 return Err(StreamError::GraphValidation(
8907 "merge state is missing".into(),
8908 ));
8909 };
8910 if *completed {
8911 return Ok(StageTransition::none());
8912 }
8913 Ok(StageTransition::emit(StageEmissions::One(
8914 single_outlet(stage)?,
8915 value,
8916 )))
8917 }
8918 StageKind::Concat | StageKind::Interleave { .. } => {
8919 let StageState::Merge { completed, .. } = &self.stage_states[stage_index] else {
8920 return Err(StreamError::GraphValidation(
8921 "fan-in state is missing".into(),
8922 ));
8923 };
8924 if *completed {
8925 return Ok(StageTransition::none());
8926 }
8927 Ok(StageTransition::emit(StageEmissions::One(
8928 single_outlet(stage)?,
8929 value,
8930 )))
8931 }
8932 StageKind::OrElse { primary_inlet } => {
8933 let StageState::OrElse {
8934 primary_emitted,
8935 buffer,
8936 primary_closed,
8937 completed,
8938 ..
8939 } = &mut self.stage_states[stage_index]
8940 else {
8941 return Err(StreamError::GraphValidation(
8942 "or-else state is missing".into(),
8943 ));
8944 };
8945 if *completed {
8946 return Ok(StageTransition::none());
8947 }
8948 if inlet == *primary_inlet {
8949 *primary_emitted = true;
8950 buffer.clear();
8951 Ok(StageTransition::emit(StageEmissions::One(
8952 single_outlet(stage)?,
8953 value,
8954 )))
8955 } else if *primary_emitted {
8956 Ok(StageTransition::none())
8957 } else if *primary_closed {
8958 Ok(StageTransition::emit(StageEmissions::One(
8959 single_outlet(stage)?,
8960 value,
8961 )))
8962 } else {
8963 buffer.push_back(value);
8964 Ok(StageTransition::none())
8965 }
8966 }
8967 StageKind::Zip(zip) => {
8968 if stage.spec.inlets.len() != 2 {
8969 return Err(StreamError::GraphValidation(format!(
8970 "zip stage {} expected 2 inlets, got {}",
8971 stage.spec.name(),
8972 stage.spec.inlets.len()
8973 )));
8974 }
8975
8976 let ready = {
8977 let StageState::Zip {
8978 left_inlet,
8979 right_inlet,
8980 left,
8981 right,
8982 left_pending_complete,
8983 right_pending_complete,
8984 completed,
8985 } = &mut self.stage_states[stage_index]
8986 else {
8987 return Err(StreamError::GraphValidation("zip state is missing".into()));
8988 };
8989
8990 if *completed {
8991 return Ok(StageTransition::none());
8992 }
8993
8994 if inlet == *left_inlet {
8998 left.push_back(value);
8999 } else if inlet == *right_inlet {
9000 right.push_back(value);
9001 } else {
9002 return Err(StreamError::GraphValidation(format!(
9003 "zip inlet {} is not part of the stage",
9004 inlet.as_usize()
9005 )));
9006 }
9007
9008 match (left.front().is_some(), right.front().is_some()) {
9009 (true, true) => {
9010 let left_item =
9011 left.pop_front().expect("zip left buffer had an element");
9012 let right_item =
9013 right.pop_front().expect("zip right buffer had an element");
9014 let should_complete = (*left_pending_complete && left.is_empty())
9015 || (*right_pending_complete && right.is_empty());
9016 Some((left_item, right_item, should_complete))
9017 }
9018 _ => None,
9019 }
9020 };
9021
9022 if let Some((left, right, should_complete)) = ready {
9023 let outlet = single_outlet(stage)?;
9024 if should_complete {
9025 let StageState::Zip { completed, .. } = &mut self.stage_states[stage_index]
9026 else {
9027 return Err(StreamError::GraphValidation(
9028 "zip state is missing".into(),
9029 ));
9030 };
9031 *completed = true;
9032 }
9033 let mut transition =
9034 StageTransition::emit(StageEmissions::One(outlet, zip(left, right)?));
9035 if should_complete {
9036 transition.completed_outlets.push(outlet);
9037 }
9038 Ok(transition)
9039 } else {
9040 Ok(StageTransition::none())
9041 }
9042 }
9043 StageKind::Unzip { split, .. } => {
9044 let (fan_in, zip_fast) = match &self.stage_states[stage_index] {
9045 StageState::Unzip {
9046 fast_path,
9047 zip_fast_path,
9048 ..
9049 } => (*fast_path, *zip_fast_path),
9050 _ => (None, None),
9051 };
9052 if let Some(zip_fast) = zip_fast {
9053 let (out0_val, out1_val) = split(value);
9054 let zip_stage = &self.graph.stages[zip_fast.zip_stage_index];
9055 let StageKind::Zip(zip) = &zip_stage.spec.kind else {
9056 return Err(StreamError::GraphValidation(
9057 "unzip-zip fast path references a non-zip stage".into(),
9058 ));
9059 };
9060 let outlet = single_outlet(zip_stage)?;
9061 let zipped = zip(out0_val, out1_val)?;
9062 return Ok(StageTransition::emit(StageEmissions::One(outlet, zipped)));
9063 }
9064 if let Some(fast_path) = fan_in {
9065 let (out0_val, out1_val) = split(value);
9066 let target = &self.graph.stages[fast_path.fan_in_stage_index];
9067 match &target.spec.kind {
9068 StageKind::MergeSorted(compare) => {
9069 let result = {
9070 let StageState::MergeSorted {
9071 left,
9072 right,
9073 left_closed,
9074 right_closed,
9075 pending,
9076 completed,
9077 } = &mut self.stage_states[fast_path.fan_in_stage_index]
9078 else {
9079 return Err(StreamError::GraphValidation(
9080 "merge-sorted state is missing".into(),
9081 ));
9082 };
9083 if *completed {
9084 return Ok(StageTransition::none());
9085 }
9086 if fast_path.target_inlet_indices[0] == 0 {
9089 left.push_back(out0_val);
9090 right.push_back(out1_val);
9091 } else {
9092 left.push_back(out1_val);
9093 right.push_back(out0_val);
9094 }
9095
9096 loop {
9097 let next = match (left.front(), right.front()) {
9098 (Some(l), Some(r)) => {
9099 if compare(l, r) != std::cmp::Ordering::Greater {
9100 left.pop_front()
9101 } else {
9102 right.pop_front()
9103 }
9104 }
9105 (Some(_), None) if *right_closed => left.pop_front(),
9106 (None, Some(_)) if *left_closed => right.pop_front(),
9107 _ => break,
9108 };
9109 if let Some(val) = next {
9110 pending.push_back(val);
9111 } else {
9112 break;
9113 }
9114 }
9115
9116 if let Some(output) = pending.pop_front() {
9117 let outlet = single_outlet(target)?;
9118 let all_done = *left_closed
9119 && *right_closed
9120 && left.is_empty()
9121 && right.is_empty()
9122 && pending.is_empty();
9123 if all_done {
9124 *completed = true;
9125 StageTransition::emit(StageEmissions::One(outlet, output))
9126 .with_completion(vec![outlet])
9127 } else {
9128 StageTransition::emit(StageEmissions::One(outlet, output))
9129 }
9130 } else {
9131 StageTransition::none()
9132 }
9133 };
9134 Ok(result)
9135 }
9136 StageKind::MergeSequence {
9137 extract_sequence,
9138 input_count,
9139 ..
9140 } => {
9141 let result = {
9142 let StageState::MergeSequence {
9143 next_sequence,
9144 pending,
9145 completed_count,
9146 output_buffer,
9147 completed,
9148 } = &mut self.stage_states[fast_path.fan_in_stage_index]
9149 else {
9150 return Err(StreamError::GraphValidation(
9151 "merge-sequence state is missing".into(),
9152 ));
9153 };
9154 if *completed {
9155 return Ok(StageTransition::none());
9156 }
9157 for val in [out0_val, out1_val] {
9158 let seq = extract_sequence(&val);
9159 if seq == *next_sequence {
9160 output_buffer.push_back(val);
9161 *next_sequence += 1;
9162 while let Some(index) =
9163 pending.iter().position(|(s, _)| *s == *next_sequence)
9164 {
9165 let (_, item) = pending.remove(index);
9166 output_buffer.push_back(item);
9167 *next_sequence += 1;
9168 }
9169 } else {
9170 if pending.iter().any(|(s, _)| *s == seq) {
9171 return Err(StreamError::Failed(format!(
9172 "duplicate sequence {seq} on merge sequence"
9173 )));
9174 }
9175 pending.push((seq, val));
9176 pending.sort_by_key(|(s, _)| *s);
9177 while let Some(index) =
9178 pending.iter().position(|(s, _)| *s == *next_sequence)
9179 {
9180 let (_, item) = pending.remove(index);
9181 output_buffer.push_back(item);
9182 *next_sequence += 1;
9183 }
9184 }
9185 }
9186
9187 if !output_buffer.is_empty() {
9188 let outlet = single_outlet(target)?;
9189 let all_done = *completed_count >= *input_count;
9190 let emissions: Vec<_> =
9191 output_buffer.drain(..).map(|v| (outlet, v)).collect();
9192 if all_done {
9193 *completed = true;
9194 StageTransition::emit(StageEmissions::Many(emissions))
9195 .with_completion(vec![outlet])
9196 } else {
9197 StageTransition::emit(StageEmissions::Many(emissions))
9198 }
9199 } else {
9200 StageTransition::none()
9201 }
9202 };
9203 Ok(result)
9204 }
9205 StageKind::MergeLatest {
9206 input_count,
9207 build_snapshot,
9208 ..
9209 } => {
9210 let result = {
9211 let StageState::MergeLatest {
9212 latest,
9213 seen_count,
9214 completed_count,
9215 pending,
9216 completed,
9217 } = &mut self.stage_states[fast_path.fan_in_stage_index]
9218 else {
9219 return Err(StreamError::GraphValidation(
9220 "merge-latest state is missing".into(),
9221 ));
9222 };
9223 if *completed {
9224 return Ok(StageTransition::none());
9225 }
9226 let inlets = &target.spec.inlets;
9227 for (idx, val) in fast_path
9228 .target_inlet_indices
9229 .into_iter()
9230 .zip([out0_val, out1_val])
9231 {
9232 if idx < inlets.len() && latest[idx].is_none() {
9233 *seen_count += 1;
9234 }
9235 latest[idx] = Some(val);
9236 if *seen_count >= *input_count {
9237 let values: Vec<&DatumValue> =
9238 latest.iter().filter_map(|v| v.as_ref()).collect();
9239 let snapshot = build_snapshot(&values);
9240 pending.push_back(snapshot);
9241 }
9242 }
9243
9244 if !pending.is_empty() {
9245 let outlet = single_outlet(target)?;
9246 let all_done = *completed_count >= *input_count;
9247 let emissions: Vec<_> =
9248 pending.drain(..).map(|v| (outlet, v)).collect();
9249 if all_done {
9250 *completed = true;
9251 StageTransition::emit(StageEmissions::Many(emissions))
9252 .with_completion(vec![outlet])
9253 } else {
9254 StageTransition::emit(StageEmissions::Many(emissions))
9255 }
9256 } else {
9257 StageTransition::none()
9258 }
9259 };
9260 Ok(result)
9261 }
9262 _ => {
9263 let transition = {
9264 let StageState::Unzip { cancelled, .. } =
9265 &self.stage_states[stage_index]
9266 else {
9267 return Err(StreamError::GraphValidation(
9268 "unzip state is missing".into(),
9269 ));
9270 };
9271 let out0 = stage.spec.outlets.first().map(AnyOutlet::id);
9272 let out1 = stage.spec.outlets.get(1).map(AnyOutlet::id);
9273 let live0 = out0.is_some() && !cancelled[0];
9274 let live1 = out1.is_some() && !cancelled[1];
9275 if live0 && live1 {
9276 StageTransition::emit(StageEmissions::Two(
9277 (out0.unwrap(), out0_val),
9278 (out1.unwrap(), out1_val),
9279 ))
9280 } else if live0 {
9281 StageTransition::emit(StageEmissions::One(
9282 out0.unwrap(),
9283 out0_val,
9284 ))
9285 } else if live1 {
9286 StageTransition::emit(StageEmissions::One(
9287 out1.unwrap(),
9288 out1_val,
9289 ))
9290 } else {
9291 StageTransition::none()
9292 }
9293 };
9294 Ok(transition)
9295 }
9296 }
9297 } else {
9298 let transition = {
9299 let StageState::Unzip { cancelled, .. } = &self.stage_states[stage_index]
9300 else {
9301 return Err(StreamError::GraphValidation(
9302 "unzip state is missing".into(),
9303 ));
9304 };
9305 let (out0_val, out1_val) = split(value);
9306 let out0 = stage.spec.outlets.first().map(AnyOutlet::id);
9307 let out1 = stage.spec.outlets.get(1).map(AnyOutlet::id);
9308 let live0 = out0.is_some() && !cancelled[0];
9309 let live1 = out1.is_some() && !cancelled[1];
9310 if live0 && live1 {
9311 StageTransition::emit(StageEmissions::Two(
9312 (out0.unwrap(), out0_val),
9313 (out1.unwrap(), out1_val),
9314 ))
9315 } else if live0 {
9316 StageTransition::emit(StageEmissions::One(out0.unwrap(), out0_val))
9317 } else if live1 {
9318 StageTransition::emit(StageEmissions::One(out1.unwrap(), out1_val))
9319 } else {
9320 StageTransition::none()
9321 }
9322 };
9323 Ok(transition)
9324 }
9325 }
9326 StageKind::MergeSorted(compare) => {
9327 let result = {
9328 let StageState::MergeSorted {
9329 left,
9330 right,
9331 left_closed,
9332 right_closed,
9333 pending,
9334 completed,
9335 } = &mut self.stage_states[stage_index]
9336 else {
9337 return Err(StreamError::GraphValidation(
9338 "merge-sorted state is missing".into(),
9339 ));
9340 };
9341 if *completed {
9342 return Ok(StageTransition::none());
9343 }
9344 let is_left = stage.spec.inlets.first().is_some_and(|i| i.id() == inlet);
9345 if is_left {
9346 left.push_back(value);
9347 } else {
9348 right.push_back(value);
9349 }
9350
9351 loop {
9352 let next = match (left.front(), right.front()) {
9353 (Some(l), Some(r)) => {
9354 if compare(l, r) != std::cmp::Ordering::Greater {
9355 left.pop_front()
9356 } else {
9357 right.pop_front()
9358 }
9359 }
9360 (Some(_), None) if *right_closed => left.pop_front(),
9361 (None, Some(_)) if *left_closed => right.pop_front(),
9362 _ => break,
9363 };
9364 if let Some(val) = next {
9365 pending.push_back(val);
9366 } else {
9367 break;
9368 }
9369 }
9370
9371 if let Some(output) = pending.pop_front() {
9372 let outlet = single_outlet(stage)?;
9373 let all_done = *left_closed
9374 && *right_closed
9375 && left.is_empty()
9376 && right.is_empty()
9377 && pending.is_empty();
9378 if all_done {
9379 *completed = true;
9380 StageTransition::emit(StageEmissions::One(outlet, output))
9381 .with_completion(vec![outlet])
9382 } else {
9383 StageTransition::emit(StageEmissions::One(outlet, output))
9384 }
9385 } else {
9386 StageTransition::none()
9387 }
9388 };
9389 Ok(result)
9390 }
9391 StageKind::MergeSequence {
9392 input_count,
9393 extract_sequence,
9394 ..
9395 } => {
9396 let result = {
9397 let StageState::MergeSequence {
9398 next_sequence,
9399 pending,
9400 completed_count,
9401 output_buffer,
9402 completed,
9403 } = &mut self.stage_states[stage_index]
9404 else {
9405 return Err(StreamError::GraphValidation(
9406 "merge-sequence state is missing".into(),
9407 ));
9408 };
9409 if *completed {
9410 return Ok(StageTransition::none());
9411 }
9412 let seq = extract_sequence(&value);
9413 if seq == *next_sequence {
9414 output_buffer.push_back(value);
9415 *next_sequence += 1;
9416 while let Some(index) =
9417 pending.iter().position(|(s, _)| *s == *next_sequence)
9418 {
9419 let (_, item) = pending.remove(index);
9420 output_buffer.push_back(item);
9421 *next_sequence += 1;
9422 }
9423 } else {
9424 if pending.iter().any(|(s, _)| *s == seq) {
9425 return Err(StreamError::Failed(format!(
9426 "duplicate sequence {seq} on merge sequence"
9427 )));
9428 }
9429 pending.push((seq, value));
9430 pending.sort_by_key(|(s, _)| *s);
9431 while let Some(index) =
9432 pending.iter().position(|(s, _)| *s == *next_sequence)
9433 {
9434 let (_, item) = pending.remove(index);
9435 output_buffer.push_back(item);
9436 *next_sequence += 1;
9437 }
9438 }
9439
9440 if !output_buffer.is_empty() {
9441 let outlet = single_outlet(stage)?;
9442 let all_done = *completed_count >= *input_count;
9443 let emissions: Vec<_> =
9444 output_buffer.drain(..).map(|v| (outlet, v)).collect();
9445 if all_done {
9446 *completed = true;
9447 StageTransition::emit(StageEmissions::Many(emissions))
9448 .with_completion(vec![outlet])
9449 } else {
9450 StageTransition::emit(StageEmissions::Many(emissions))
9451 }
9452 } else {
9453 StageTransition::none()
9454 }
9455 };
9456 Ok(result)
9457 }
9458 StageKind::MergeLatest {
9459 input_count,
9460 build_snapshot,
9461 ..
9462 } => {
9463 let result = {
9464 let StageState::MergeLatest {
9465 latest,
9466 seen_count,
9467 completed_count,
9468 pending,
9469 completed,
9470 } = &mut self.stage_states[stage_index]
9471 else {
9472 return Err(StreamError::GraphValidation(
9473 "merge-latest state is missing".into(),
9474 ));
9475 };
9476 if *completed {
9477 return Ok(StageTransition::none());
9478 }
9479 let inlet_index = stage
9480 .spec
9481 .inlets
9482 .iter()
9483 .position(|i| i.id() == inlet)
9484 .ok_or_else(|| {
9485 StreamError::GraphValidation(format!(
9486 "merge-latest inlet {} not part of stage",
9487 inlet.as_usize()
9488 ))
9489 })?;
9490 if latest[inlet_index].is_none() {
9491 *seen_count += 1;
9492 }
9493 latest[inlet_index] = Some(value);
9494 if *seen_count >= *input_count {
9495 let values: Vec<&DatumValue> =
9496 latest.iter().filter_map(|v| v.as_ref()).collect();
9497 let snapshot = build_snapshot(&values);
9498 pending.push_back(snapshot);
9499 }
9500
9501 if !pending.is_empty() {
9502 let outlet = single_outlet(stage)?;
9503 let all_done = *completed_count >= *input_count;
9504 let emissions: Vec<_> = pending.drain(..).map(|v| (outlet, v)).collect();
9505 if all_done {
9506 *completed = true;
9507 StageTransition::emit(StageEmissions::Many(emissions))
9508 .with_completion(vec![outlet])
9509 } else {
9510 StageTransition::emit(StageEmissions::Many(emissions))
9511 }
9512 } else {
9513 StageTransition::none()
9514 }
9515 };
9516 Ok(result)
9517 }
9518 StageKind::Partition {
9519 output_count,
9520 partitioner,
9521 ..
9522 } => {
9523 let result = {
9524 let StageState::Partition {
9525 pending,
9526 upstream_closed: _,
9527 demand,
9528 cancelled,
9529 output_count: _,
9530 completed,
9531 ..
9532 } = &mut self.stage_states[stage_index]
9533 else {
9534 return Err(StreamError::GraphValidation(
9535 "partition state is missing".into(),
9536 ));
9537 };
9538 if *completed {
9539 return Ok(StageTransition::none());
9540 }
9541 let idx = partitioner(&value);
9542 if idx >= *output_count {
9543 return Err(StreamError::Failed(format!(
9544 "partitioner returned out-of-bounds index {idx} for {output_count} outputs"
9545 )));
9546 }
9547 if cancelled[idx] {
9548 return Ok(StageTransition::none());
9549 }
9550 if demand[idx] {
9551 demand[idx] = false;
9552 let outlet = stage.spec.outlets[idx].id();
9553 StageTransition::emit(StageEmissions::One(outlet, value))
9554 } else {
9555 *pending = Some((idx, value));
9556 StageTransition::none()
9557 }
9558 };
9559 Ok(result)
9560 }
9561 }
9562 }
9563
9564 fn process_completion(
9565 &mut self,
9566 stage_index: usize,
9567 inlet: PortId,
9568 ) -> StreamResult<StageTransition> {
9569 let stage = &self.graph.stages[stage_index];
9570 match &stage.spec.kind {
9571 StageKind::Identity | StageKind::Map(_) | StageKind::AsyncBoundary => {
9572 Ok(StageTransition::emit(StageEmissions::None)
9573 .with_completion(vec![single_outlet(stage)?]))
9574 }
9575 StageKind::Opaque => {
9576 if let Some(logic) = self
9577 .opaque_logics
9578 .get_mut(stage_index)
9579 .and_then(|l| l.as_mut())
9580 {
9581 logic.drain_async_callbacks();
9582 logic.complete_inlet_by_id(inlet)?;
9583 let inlet_ref = stage.spec.inlets.iter().find(|i| i.id() == inlet).cloned();
9584 if let Some(inlet_ref) = inlet_ref {
9585 let mut handler = logic.take_in_handler(inlet);
9586 let result = if let Some(ref mut handler) = handler {
9587 let inlet_any = inlet_ref;
9588 handler.on_upstream_finish(logic, inlet_any)
9589 } else {
9590 Ok(())
9591 };
9592 if let Some(handler) = handler {
9593 logic.restore_in_handler(inlet, handler);
9594 }
9595 if result.is_err() {
9596 logic.cancel_all_timers();
9597 }
9598 result?;
9599 }
9600 self.collect_opaque_emissions(stage, stage_index)
9601 } else {
9602 Ok(StageTransition::emit(StageEmissions::None)
9603 .with_completion(vec![single_outlet(stage)?]))
9604 }
9605 }
9606 StageKind::Broadcast | StageKind::Balance => {
9607 Ok(StageTransition::emit(StageEmissions::None)
9608 .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect()))
9609 }
9610 StageKind::Merge | StageKind::MergePreferred | StageKind::MergePrioritized { .. } => {
9611 let StageState::Merge {
9612 open_inputs,
9613 eager_complete,
9614 completed,
9615 } = &mut self.stage_states[stage_index]
9616 else {
9617 return Err(StreamError::GraphValidation(
9618 "merge state is missing".into(),
9619 ));
9620 };
9621
9622 if *completed {
9623 return Ok(StageTransition::none());
9624 }
9625 if *open_inputs == 0 {
9626 return Ok(StageTransition::none());
9627 }
9628 *open_inputs -= 1;
9629 if *eager_complete || *open_inputs == 0 {
9630 *completed = true;
9631 Ok(StageTransition::emit(StageEmissions::None)
9632 .with_completion(vec![single_outlet(stage)?]))
9633 } else {
9634 Ok(StageTransition::none())
9635 }
9636 }
9637 StageKind::Concat | StageKind::Interleave { .. } => {
9638 let StageState::Merge {
9639 open_inputs,
9640 eager_complete,
9641 completed,
9642 } = &mut self.stage_states[stage_index]
9643 else {
9644 return Err(StreamError::GraphValidation(
9645 "fan-in state is missing".into(),
9646 ));
9647 };
9648
9649 if *completed {
9650 return Ok(StageTransition::none());
9651 }
9652 if *open_inputs == 0 {
9653 return Ok(StageTransition::none());
9654 }
9655 *open_inputs -= 1;
9656 if *eager_complete || *open_inputs == 0 {
9657 *completed = true;
9658 Ok(StageTransition::emit(StageEmissions::None)
9659 .with_completion(vec![single_outlet(stage)?]))
9660 } else {
9661 Ok(StageTransition::none())
9662 }
9663 }
9664 StageKind::OrElse { primary_inlet } => {
9665 let StageState::OrElse {
9666 primary_emitted,
9667 buffer,
9668 primary_closed,
9669 secondary_closed,
9670 completed,
9671 ..
9672 } = &mut self.stage_states[stage_index]
9673 else {
9674 return Err(StreamError::GraphValidation(
9675 "or-else state is missing".into(),
9676 ));
9677 };
9678 if *completed {
9679 return Ok(StageTransition::none());
9680 }
9681 if inlet == *primary_inlet {
9682 *primary_closed = true;
9683 if *primary_emitted {
9684 *completed = true;
9685 buffer.clear();
9686 Ok(StageTransition::emit(StageEmissions::None)
9687 .with_completion(vec![single_outlet(stage)?]))
9688 } else {
9689 let outlet = single_outlet(stage)?;
9690 let emissions: Vec<_> = buffer.drain(..).map(|v| (outlet, v)).collect();
9691 if *secondary_closed {
9692 *completed = true;
9693 let transition = if emissions.is_empty() {
9694 StageTransition::emit(StageEmissions::None)
9695 } else {
9696 StageTransition::emit(StageEmissions::Many(emissions))
9697 };
9698 Ok(transition.with_completion(vec![outlet]))
9699 } else {
9700 if emissions.is_empty() {
9701 Ok(StageTransition::none())
9702 } else {
9703 Ok(StageTransition::emit(StageEmissions::Many(emissions)))
9704 }
9705 }
9706 }
9707 } else {
9708 *secondary_closed = true;
9709 if *primary_closed && !*primary_emitted {
9710 let outlet = single_outlet(stage)?;
9711 let emissions: Vec<_> = buffer.drain(..).map(|v| (outlet, v)).collect();
9712 *completed = true;
9713 if emissions.is_empty() {
9714 Ok(StageTransition::emit(StageEmissions::None)
9715 .with_completion(vec![outlet]))
9716 } else {
9717 Ok(StageTransition::emit(StageEmissions::Many(emissions))
9718 .with_completion(vec![outlet]))
9719 }
9720 } else {
9721 Ok(StageTransition::none())
9722 }
9723 }
9724 }
9725 StageKind::Zip(_) => {
9726 let StageState::Zip {
9727 left_inlet,
9728 right_inlet,
9729 left,
9730 right,
9731 left_pending_complete,
9732 right_pending_complete,
9733 completed,
9734 } = &mut self.stage_states[stage_index]
9735 else {
9736 return Err(StreamError::GraphValidation("zip state is missing".into()));
9737 };
9738 if *completed {
9739 return Ok(StageTransition::none());
9740 }
9741 let finishes_left = inlet == *left_inlet;
9742 let finishes_right = inlet == *right_inlet;
9743 if (finishes_left && left.is_empty()) || (finishes_right && right.is_empty()) {
9744 *completed = true;
9745 Ok(StageTransition::emit(StageEmissions::None)
9746 .with_completion(vec![single_outlet(stage)?]))
9747 } else {
9748 if finishes_left {
9749 *left_pending_complete = true;
9750 }
9751 if finishes_right {
9752 *right_pending_complete = true;
9753 }
9754 Ok(StageTransition::none())
9755 }
9756 }
9757 StageKind::Unzip { .. } => {
9758 let (fan_in, zip_fast) = match &self.stage_states[stage_index] {
9759 StageState::Unzip {
9760 fast_path,
9761 zip_fast_path,
9762 ..
9763 } => (*fast_path, *zip_fast_path),
9764 _ => (None, None),
9765 };
9766 if let Some(zip_fast) = zip_fast {
9767 let StageState::Unzip {
9768 upstream_closed, ..
9769 } = &mut self.stage_states[stage_index]
9770 else {
9771 return Err(StreamError::GraphValidation(
9772 "unzip state is missing".into(),
9773 ));
9774 };
9775 *upstream_closed = true;
9776 let zip_stage = &self.graph.stages[zip_fast.zip_stage_index];
9777 return Ok(StageTransition::emit(StageEmissions::None)
9778 .with_completion(vec![single_outlet(zip_stage)?]));
9779 }
9780 if let Some(fast_path) = fan_in {
9781 let StageState::Unzip {
9782 upstream_closed, ..
9783 } = &mut self.stage_states[stage_index]
9784 else {
9785 return Err(StreamError::GraphValidation(
9786 "unzip state is missing".into(),
9787 ));
9788 };
9789 *upstream_closed = true;
9790 let target_stage = &self.graph.stages[fast_path.fan_in_stage_index];
9791 let target_inlets = [
9795 target_stage.spec.inlets[fast_path.target_inlet_indices[0]].id(),
9796 target_stage.spec.inlets[fast_path.target_inlet_indices[1]].id(),
9797 ];
9798 let mut combined = StageTransition::none();
9799 for target_inlet in target_inlets {
9800 let t =
9801 self.process_completion(fast_path.fan_in_stage_index, target_inlet)?;
9802 combined.emissions = merge_emissions(combined.emissions, t.emissions);
9803 combined.completed_outlets.extend(t.completed_outlets);
9804 combined.cancelled_inlets.extend(t.cancelled_inlets);
9805 }
9806 Ok(combined)
9807 } else {
9808 let StageState::Unzip {
9809 upstream_closed, ..
9810 } = &mut self.stage_states[stage_index]
9811 else {
9812 return Err(StreamError::GraphValidation(
9813 "unzip state is missing".into(),
9814 ));
9815 };
9816 *upstream_closed = true;
9817 Ok(StageTransition::emit(StageEmissions::None)
9818 .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect()))
9819 }
9820 }
9821 StageKind::MergeSorted(compare) => {
9822 let result = {
9823 let StageState::MergeSorted {
9824 left,
9825 right,
9826 left_closed,
9827 right_closed,
9828 pending,
9829 completed,
9830 } = &mut self.stage_states[stage_index]
9831 else {
9832 return Err(StreamError::GraphValidation(
9833 "merge-sorted state is missing".into(),
9834 ));
9835 };
9836 if *completed {
9837 return Ok(StageTransition::none());
9838 }
9839 let is_left = stage.spec.inlets.first().is_some_and(|i| i.id() == inlet);
9840 if is_left {
9841 *left_closed = true;
9842 } else {
9843 *right_closed = true;
9844 }
9845
9846 loop {
9847 let next = match (left.front(), right.front()) {
9848 (Some(l), Some(r)) => {
9849 if compare(l, r) != std::cmp::Ordering::Greater {
9850 left.pop_front()
9851 } else {
9852 right.pop_front()
9853 }
9854 }
9855 (Some(_), None) if *right_closed => left.pop_front(),
9856 (None, Some(_)) if *left_closed => right.pop_front(),
9857 _ => break,
9858 };
9859 if let Some(val) = next {
9860 pending.push_back(val);
9861 } else {
9862 break;
9863 }
9864 }
9865
9866 if pending.is_empty() {
9867 let all_done =
9868 *left_closed && *right_closed && left.is_empty() && right.is_empty();
9869 if all_done {
9870 *completed = true;
9871 StageTransition::emit(StageEmissions::None)
9872 .with_completion(vec![single_outlet(stage)?])
9873 } else {
9874 StageTransition::none()
9875 }
9876 } else {
9877 let outlet = single_outlet(stage)?;
9878 let emissions: Vec<_> = pending.drain(..).map(|v| (outlet, v)).collect();
9879 let all_done =
9880 *left_closed && *right_closed && left.is_empty() && right.is_empty();
9881 if all_done {
9882 *completed = true;
9883 StageTransition::emit(StageEmissions::Many(emissions))
9884 .with_completion(vec![outlet])
9885 } else {
9886 StageTransition::emit(StageEmissions::Many(emissions))
9887 }
9888 }
9889 };
9890 Ok(result)
9891 }
9892 StageKind::MergeSequence { input_count, .. } => {
9893 let result = {
9894 let StageState::MergeSequence {
9895 next_sequence,
9896 pending,
9897 completed_count,
9898 output_buffer,
9899 completed,
9900 } = &mut self.stage_states[stage_index]
9901 else {
9902 return Err(StreamError::GraphValidation(
9903 "merge-sequence state is missing".into(),
9904 ));
9905 };
9906 if *completed {
9907 return Ok(StageTransition::none());
9908 }
9909 *completed_count += 1;
9910 if *completed_count >= *input_count && output_buffer.is_empty() {
9911 if !pending.is_empty() {
9912 return Err(StreamError::Failed(format!(
9916 "expected sequence {next_sequence}, but all input ports have pushed or are complete",
9917 )));
9918 }
9919 *completed = true;
9920 StageTransition::emit(StageEmissions::None)
9921 .with_completion(vec![single_outlet(stage)?])
9922 } else {
9923 StageTransition::none()
9924 }
9925 };
9926 Ok(result)
9927 }
9928 StageKind::MergeLatest {
9929 input_count,
9930 eager_complete,
9931 ..
9932 } => {
9933 let result = {
9934 let StageState::MergeLatest {
9935 completed_count,
9936 pending,
9937 completed,
9938 ..
9939 } = &mut self.stage_states[stage_index]
9940 else {
9941 return Err(StreamError::GraphValidation(
9942 "merge-latest state is missing".into(),
9943 ));
9944 };
9945 if *completed {
9946 return Ok(StageTransition::none());
9947 }
9948 *completed_count += 1;
9949 let all_done = *completed_count >= *input_count;
9954 let eager_done = *eager_complete && pending.is_empty();
9955 if all_done || eager_done {
9956 *completed = true;
9957 StageTransition::emit(StageEmissions::None)
9958 .with_completion(vec![single_outlet(stage)?])
9959 } else {
9960 StageTransition::none()
9961 }
9962 };
9963 Ok(result)
9964 }
9965 StageKind::Partition { .. } => {
9966 let result = {
9967 let StageState::Partition {
9968 pending,
9969 upstream_closed,
9970 completed,
9971 ..
9972 } = &mut self.stage_states[stage_index]
9973 else {
9974 return Err(StreamError::GraphValidation(
9975 "partition state is missing".into(),
9976 ));
9977 };
9978 if *completed {
9979 return Ok(StageTransition::none());
9980 }
9981 *upstream_closed = true;
9982 if pending.is_none() {
9983 *completed = true;
9984 StageTransition::emit(StageEmissions::None)
9985 .with_completion(stage.spec.outlets.iter().map(AnyOutlet::id).collect())
9986 } else {
9987 StageTransition::none()
9988 }
9989 };
9990 Ok(result)
9991 }
9992 }
9993 }
9994
9995 fn process_pull(
9996 &mut self,
9997 stage_index: usize,
9998 outlet: PortId,
9999 ) -> StreamResult<StageTransition> {
10000 let stage = &self.graph.stages[stage_index];
10001 match &stage.spec.kind {
10002 StageKind::Opaque => {
10003 if let Some(logic) = self
10004 .opaque_logics
10005 .get_mut(stage_index)
10006 .and_then(|l| l.as_mut())
10007 {
10008 logic.drain_async_callbacks();
10009 logic.set_demand_by_id(outlet)?;
10010 let outlet_ref = stage
10011 .spec
10012 .outlets
10013 .iter()
10014 .find(|o| o.id() == outlet)
10015 .cloned();
10016 if let Some(outlet_ref) = outlet_ref {
10017 let mut handler = logic.take_out_handler(outlet);
10018 let result = if let Some(ref mut handler) = handler {
10019 handler.on_pull(logic, outlet_ref)
10020 } else {
10021 Ok(())
10022 };
10023 if let Some(handler) = handler
10024 && handler.keep_handler()
10025 && logic.get_out_handler_mut(outlet).is_none()
10026 {
10027 logic.restore_out_handler(outlet, handler);
10028 }
10029 if result.is_err() {
10030 logic.cancel_all_timers();
10031 }
10032 result?;
10033 }
10034 self.collect_opaque_emissions(stage, stage_index)
10035 } else {
10036 Ok(StageTransition::none())
10037 }
10038 }
10039 StageKind::Unzip { .. } => {
10040 let StageState::Unzip {
10041 demand, cancelled, ..
10042 } = &mut self.stage_states[stage_index]
10043 else {
10044 return Ok(StageTransition::none());
10045 };
10046 let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
10047 return Ok(StageTransition::none());
10048 };
10049 if idx < 2 && !cancelled[idx] {
10050 demand[idx] = true;
10051 }
10052 Ok(StageTransition::none())
10053 }
10054 StageKind::Partition { .. } => {
10055 let result = {
10056 let StageState::Partition {
10057 pending,
10058 upstream_closed,
10059 demand,
10060 cancelled,
10061 completed,
10062 ..
10063 } = &mut self.stage_states[stage_index]
10064 else {
10065 return Ok(StageTransition::none());
10066 };
10067 if *completed {
10068 return Ok(StageTransition::none());
10069 }
10070 let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
10071 return Ok(StageTransition::none());
10072 };
10073 if cancelled[idx] {
10074 return Ok(StageTransition::none());
10075 }
10076
10077 if let Some((p_idx, p_val)) = pending.take() {
10078 if p_idx == idx {
10079 let out = stage.spec.outlets[idx].id();
10080 if *upstream_closed {
10081 *completed = true;
10082 StageTransition::emit(StageEmissions::One(out, p_val))
10083 .with_completion(
10084 stage.spec.outlets.iter().map(AnyOutlet::id).collect(),
10085 )
10086 } else {
10087 StageTransition::emit(StageEmissions::One(out, p_val))
10088 }
10089 } else {
10090 *pending = Some((p_idx, p_val));
10091 demand[idx] = true;
10092 StageTransition::none()
10093 }
10094 } else {
10095 demand[idx] = true;
10096 StageTransition::none()
10097 }
10098 };
10099 Ok(result)
10100 }
10101 _ => Ok(StageTransition::none()),
10102 }
10103 }
10104
10105 fn process_downstream_finish(
10106 &mut self,
10107 stage_index: usize,
10108 outlet: PortId,
10109 ) -> StreamResult<StageTransition> {
10110 let stage = &self.graph.stages[stage_index];
10111 match &stage.spec.kind {
10112 StageKind::Broadcast => {
10113 let StageState::Broadcast {
10114 cancelled_outlets,
10115 live_outlets,
10116 ..
10117 } = &mut self.stage_states[stage_index]
10118 else {
10119 return Err(StreamError::GraphValidation(
10120 "broadcast state is missing".into(),
10121 ));
10122 };
10123 let index = stage
10124 .spec
10125 .outlets
10126 .iter()
10127 .position(|candidate| candidate.id() == outlet)
10128 .ok_or_else(|| {
10129 StreamError::GraphValidation(format!(
10130 "broadcast outlet {} is not part of the stage",
10131 outlet.as_usize()
10132 ))
10133 })?;
10134 if cancelled_outlets[index] {
10135 return Ok(StageTransition::none());
10136 }
10137 cancelled_outlets[index] = true;
10138 *live_outlets -= 1;
10139 if *live_outlets == 0 {
10140 Ok(StageTransition::none()
10141 .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
10142 } else {
10143 Ok(StageTransition::none())
10144 }
10145 }
10146 StageKind::Balance => {
10147 let StageState::Balance {
10148 cancelled_outlets,
10149 live_outlets,
10150 ..
10151 } = &mut self.stage_states[stage_index]
10152 else {
10153 return Err(StreamError::GraphValidation(
10154 "balance state is missing".into(),
10155 ));
10156 };
10157 let index = stage
10158 .spec
10159 .outlets
10160 .iter()
10161 .position(|candidate| candidate.id() == outlet)
10162 .ok_or_else(|| {
10163 StreamError::GraphValidation(format!(
10164 "balance outlet {} is not part of the stage",
10165 outlet.as_usize()
10166 ))
10167 })?;
10168 if cancelled_outlets[index] {
10169 return Ok(StageTransition::none());
10170 }
10171 cancelled_outlets[index] = true;
10172 *live_outlets -= 1;
10173 if *live_outlets == 0 {
10174 Ok(StageTransition::none()
10175 .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
10176 } else {
10177 Ok(StageTransition::none())
10178 }
10179 }
10180 StageKind::Unzip { .. } => {
10181 let StageState::Unzip { cancelled, .. } = &mut self.stage_states[stage_index]
10182 else {
10183 return Err(StreamError::GraphValidation(
10184 "unzip state is missing".into(),
10185 ));
10186 };
10187 let idx = stage
10188 .spec
10189 .outlets
10190 .iter()
10191 .position(|o| o.id() == outlet)
10192 .unwrap_or(0);
10193 if idx < 2 && !cancelled[idx] {
10194 cancelled[idx] = true;
10195 let all_cancelled = cancelled.iter().all(|c| *c);
10196 if all_cancelled {
10197 Ok(StageTransition::none().with_cancellations(
10198 stage.spec.inlets.iter().map(AnyInlet::id).collect(),
10199 ))
10200 } else {
10201 Ok(StageTransition::none())
10202 }
10203 } else {
10204 Ok(StageTransition::none())
10205 }
10206 }
10207 StageKind::MergeSorted(_)
10208 | StageKind::MergeSequence { .. }
10209 | StageKind::MergeLatest { .. } => Ok(StageTransition::none()
10210 .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect())),
10211 StageKind::Partition { eager_cancel, .. } => {
10212 let result = {
10213 let StageState::Partition {
10214 pending,
10215 cancelled,
10216 completed,
10217 ..
10218 } = &mut self.stage_states[stage_index]
10219 else {
10220 return Err(StreamError::GraphValidation(
10221 "partition state is missing".into(),
10222 ));
10223 };
10224 if *completed {
10225 return Ok(StageTransition::none());
10226 }
10227 let Some(idx) = stage.spec.outlets.iter().position(|o| o.id() == outlet) else {
10228 return Ok(StageTransition::none());
10229 };
10230 if cancelled[idx] {
10231 return Ok(StageTransition::none());
10232 }
10233 cancelled[idx] = true;
10234 if let Some((p_idx, _)) = pending
10236 && *p_idx == idx
10237 {
10238 *pending = None;
10239 }
10240 let all_cancelled = cancelled.iter().all(|c| *c);
10241 if all_cancelled || *eager_cancel {
10242 *completed = true;
10243 StageTransition::none().with_cancellations(
10244 stage.spec.inlets.iter().map(AnyInlet::id).collect(),
10245 )
10246 } else {
10247 StageTransition::none()
10248 }
10249 };
10250 Ok(result)
10251 }
10252 StageKind::Opaque => {
10253 let no_cancelled_outlets = self.cancelled_outlets.is_empty();
10254 if let Some(logic) = self
10255 .opaque_logics
10256 .get_mut(stage_index)
10257 .and_then(|l| l.as_mut())
10258 {
10259 logic.drain_async_callbacks();
10260 logic.downstream_finish_by_id(outlet, "downstream_finish")?;
10261 let outlet_ref = stage
10262 .spec
10263 .outlets
10264 .iter()
10265 .find(|o| o.id() == outlet)
10266 .cloned();
10267 if let Some(outlet_ref) = outlet_ref {
10268 let mut handler = logic.take_out_handler(outlet);
10269 let result = if let Some(ref mut handler) = handler {
10270 handler.on_downstream_finish(logic, outlet_ref)
10271 } else {
10272 Ok(())
10273 };
10274 if let Some(handler) = handler
10275 && handler.keep_handler()
10276 && logic.get_out_handler_mut(outlet).is_none()
10277 {
10278 logic.restore_out_handler(outlet, handler);
10279 }
10280 if result.is_err() {
10281 logic.cancel_all_timers();
10282 }
10283 result?;
10284 }
10285 let all_outlets_closed = stage.spec.outlets.iter().all(|candidate| {
10286 logic.is_closed_by_id(candidate.id())
10287 || (!no_cancelled_outlets
10288 && self.cancelled_outlets.contains(&candidate.id()))
10289 });
10290 let mut transition = self.collect_opaque_emissions(stage, stage_index)?;
10291 if all_outlets_closed {
10292 transition.cancelled_inlets =
10293 stage.spec.inlets.iter().map(AnyInlet::id).collect();
10294 }
10295 Ok(transition)
10296 } else {
10297 Ok(StageTransition::none()
10298 .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect()))
10299 }
10300 }
10301 _ => Ok(StageTransition::none()
10302 .with_cancellations(stage.spec.inlets.iter().map(AnyInlet::id).collect())),
10303 }
10304 }
10305
10306 fn collect_opaque_emissions(
10307 &mut self,
10308 stage: &StageRecord,
10309 stage_index: usize,
10310 ) -> StreamResult<StageTransition> {
10311 if let Some(logic) = self
10312 .opaque_logics
10313 .get_mut(stage_index)
10314 .and_then(|l| l.as_mut())
10315 {
10316 let emissions_slots = std::mem::take(&mut logic.pending_emissions);
10317 let completions = std::mem::take(&mut logic.pending_completions);
10318 let has_stage_failed = logic.stage_error().is_some();
10319
10320 let emissions = if emissions_slots.is_empty() {
10321 StageEmissions::None
10322 } else if emissions_slots.len() == 1 {
10323 let (port, val) = emissions_slots.into_iter().next().unwrap();
10324 StageEmissions::One(port, val)
10325 } else {
10326 StageEmissions::Many(emissions_slots)
10327 };
10328
10329 if has_stage_failed {
10330 let _ = has_stage_failed;
10331 }
10332
10333 Ok(StageTransition {
10334 emissions,
10335 completed_outlets: completions,
10336 cancelled_inlets: Vec::new(),
10337 })
10338 } else {
10339 Ok(StageTransition::emit(StageEmissions::None)
10340 .with_completion(vec![single_outlet(stage)?]))
10341 }
10342 }
10343
10344 fn bump_event(&mut self) -> StreamResult<()> {
10345 bump_fused_event(&mut self.events, self.config)
10346 }
10347
10348 fn prime_connected_demands(&mut self) {
10349 for (stage_index, stage) in self.graph.stages.iter().enumerate() {
10350 match &stage.spec.kind {
10351 StageKind::Opaque => {
10352 let Some(logic) = self
10353 .opaque_logics
10354 .get_mut(stage_index)
10355 .and_then(|logic| logic.as_mut())
10356 else {
10357 continue;
10358 };
10359 for outlet in &stage.spec.outlets {
10360 if self.edge_by_outlet.contains_key(&outlet.id()) {
10361 let _ = logic.set_demand_by_id(outlet.id());
10362 }
10363 }
10364 }
10365 StageKind::Unzip { .. } => {
10366 let StageState::Unzip { demand, .. } = &mut self.stage_states[stage_index]
10367 else {
10368 continue;
10369 };
10370 for (idx, outlet) in stage.spec.outlets.iter().enumerate() {
10371 if self.edge_by_outlet.contains_key(&outlet.id()) {
10372 demand[idx] = true;
10373 }
10374 }
10375 }
10376 StageKind::Partition { .. } => {
10377 let StageState::Partition {
10378 demand,
10379 output_count,
10380 ..
10381 } = &mut self.stage_states[stage_index]
10382 else {
10383 continue;
10384 };
10385 for (idx, demand_slot) in demand.iter_mut().enumerate().take(*output_count) {
10386 if idx < stage.spec.outlets.len()
10387 && self
10388 .edge_by_outlet
10389 .contains_key(&stage.spec.outlets[idx].id())
10390 {
10391 *demand_slot = true;
10392 }
10393 }
10394 }
10395 _ => {}
10396 }
10397 }
10398 }
10399}
10400
10401impl<Left, Right> GraphBlueprint<ZipShape<Left, Right>>
10402where
10403 Left: Clone + Send + 'static,
10404 Right: Clone + Send + 'static,
10405{
10406 pub fn run_zip(&self, left: Vec<Left>, right: Vec<Right>) -> StreamResult<Vec<(Left, Right)>> {
10407 Ok(self
10408 .run_zip_report(left, right, FusedExecutionConfig::default())?
10409 .output)
10410 }
10411
10412 pub fn run_zip_report(
10413 &self,
10414 left: Vec<Left>,
10415 right: Vec<Right>,
10416 config: FusedExecutionConfig,
10417 ) -> StreamResult<FusedExecutionReport<(Left, Right)>> {
10418 let selection = self.resolve_executor_selection(ExecutorMode::Auto)?;
10419 if selection.mode == ExecutorMode::TypedOnly {
10420 return Err(self.typed_unsupported_error(&selection));
10421 }
10422
10423 let mut left = left.into_iter();
10424 let mut right = right.into_iter();
10425 let left_inlet = self.shape.in0().id();
10426 let right_inlet = self.shape.in1().id();
10427 let outlet = self.shape.outlet().id();
10428 let mut executor = FusedExecutor::new(self, config);
10429 let mut output = Vec::with_capacity(left.len().min(right.len()));
10430 let mut left_completed = false;
10431 let mut right_completed = false;
10432
10433 {
10434 let mut output_sink = VecOutputSink {
10435 output: &mut output,
10436 };
10437 if left.len() == 0 {
10438 executor.complete(left_inlet, outlet, &mut output_sink)?;
10439 left_completed = true;
10440 }
10441 if right.len() == 0 {
10442 executor.complete(right_inlet, outlet, &mut output_sink)?;
10443 right_completed = true;
10444 }
10445
10446 while left.len() > 0 || right.len() > 0 {
10447 if let Some(item) = left.next() {
10448 executor.deliver(left_inlet, datum(item), outlet, &mut output_sink)?;
10449 if left.len() == 0 && !left_completed {
10450 executor.complete(left_inlet, outlet, &mut output_sink)?;
10451 left_completed = true;
10452 }
10453 }
10454 if let Some(item) = right.next() {
10455 executor.deliver(right_inlet, datum(item), outlet, &mut output_sink)?;
10456 if right.len() == 0 && !right_completed {
10457 executor.complete(right_inlet, outlet, &mut output_sink)?;
10458 right_completed = true;
10459 }
10460 }
10461 }
10462 }
10463
10464 Ok(FusedExecutionReport::new(
10465 output,
10466 executor.events,
10467 executor.async_boundary_crossings,
10468 selection.erased_plan_report(),
10469 )
10470 .with_node_metrics(executor.metrics.snapshot()))
10471 }
10472}
10473
10474fn unzip_fan_in_fast_path<S: Shape>(
10475 stage: &StageRecord,
10476 graph: &GraphBlueprint<S>,
10477 edge_by_outlet: &HashMap<PortId, PortId>,
10478 stage_by_inlet: &HashMap<PortId, usize>,
10479) -> Option<UnzipFanInFastPath> {
10480 let outlets = &stage.spec.outlets;
10481 if outlets.len() != 2 {
10482 return None;
10483 }
10484 let inlet0 = edge_by_outlet.get(&outlets[0].id()).copied()?;
10485 let inlet1 = edge_by_outlet.get(&outlets[1].id()).copied()?;
10486 let stage0 = *stage_by_inlet.get(&inlet0)?;
10487 let stage1 = *stage_by_inlet.get(&inlet1)?;
10488 if stage0 != stage1 {
10489 return None;
10490 }
10491 let target = graph.stages.get(stage0)?;
10492 if !matches!(
10493 target.spec.kind,
10494 StageKind::MergeSorted(_) | StageKind::MergeSequence { .. } | StageKind::MergeLatest { .. }
10495 ) {
10496 return None;
10497 }
10498 let idx0 = target.spec.inlets.iter().position(|i| i.id() == inlet0)?;
10501 let idx1 = target.spec.inlets.iter().position(|i| i.id() == inlet1)?;
10502 Some(UnzipFanInFastPath {
10503 fan_in_stage_index: stage0,
10504 target_inlet_indices: [idx0, idx1],
10505 })
10506}
10507
10508fn unzip_zip_fast_path<S: Shape>(
10509 stage: &StageRecord,
10510 graph: &GraphBlueprint<S>,
10511 edge_by_outlet: &HashMap<PortId, PortId>,
10512 stage_by_inlet: &HashMap<PortId, usize>,
10513) -> Option<UnzipZipFastPath> {
10514 let outlets = &stage.spec.outlets;
10515 if outlets.len() != 2 {
10516 return None;
10517 }
10518 let inlet0 = edge_by_outlet.get(&outlets[0].id()).copied()?;
10519 let inlet1 = edge_by_outlet.get(&outlets[1].id()).copied()?;
10520 let stage0 = *stage_by_inlet.get(&inlet0)?;
10521 let stage1 = *stage_by_inlet.get(&inlet1)?;
10522 if stage0 != stage1 {
10523 return None;
10524 }
10525 let target = graph.stages.get(stage0)?;
10526 if !matches!(target.spec.kind, StageKind::Zip(_)) {
10527 return None;
10528 }
10529 Some(UnzipZipFastPath {
10530 zip_stage_index: stage0,
10531 })
10532}
10533
10534pub(crate) fn bump_fused_event(
10540 events: &mut usize,
10541 config: FusedExecutionConfig,
10542) -> StreamResult<()> {
10543 *events += 1;
10544 if *events > config.event_limit {
10545 return Err(StreamError::EventLimitExceeded {
10546 limit: config.event_limit,
10547 });
10548 }
10549 Ok(())
10550}
10551
10552fn broadcast_emissions(outlets: &[AnyOutlet], value: DatumValue) -> StreamResult<StageEmissions> {
10553 match outlets {
10554 [] => Err(StreamError::GraphValidation(
10555 "broadcast has no outlets".into(),
10556 )),
10557 [outlet] => Ok(StageEmissions::One(outlet.id(), value)),
10558 [first, second] => Ok(StageEmissions::Two(
10559 (first.id(), value.clone_box()),
10560 (second.id(), value),
10561 )),
10562 outlets => {
10563 let mut emitted = Vec::with_capacity(outlets.len());
10564 for outlet in &outlets[..outlets.len() - 1] {
10565 emitted.push((outlet.id(), value.clone_box()));
10566 }
10567 emitted.push((outlets[outlets.len() - 1].id(), value));
10568 Ok(StageEmissions::Many(emitted))
10569 }
10570 }
10571}
10572
10573fn single_outlet(stage: &StageRecord) -> StreamResult<PortId> {
10574 stage
10575 .spec
10576 .outlets
10577 .first()
10578 .map(AnyOutlet::id)
10579 .ok_or_else(|| {
10580 StreamError::GraphValidation(format!("stage {} has no outlet", stage.spec.name()))
10581 })
10582}
10583
10584fn merge_emissions(first: StageEmissions, second: StageEmissions) -> StageEmissions {
10585 match (first, second) {
10586 (StageEmissions::None, other) | (other, StageEmissions::None) => other,
10587 (StageEmissions::One(p1, v1), StageEmissions::One(p2, v2)) => {
10588 StageEmissions::Many(vec![(p1, v1), (p2, v2)])
10589 }
10590 (StageEmissions::One(p, v), StageEmissions::Many(mut vec))
10591 | (StageEmissions::Many(mut vec), StageEmissions::One(p, v)) => {
10592 vec.push((p, v));
10593 StageEmissions::Many(vec)
10594 }
10595 (StageEmissions::Many(mut v1), StageEmissions::Many(v2)) => {
10596 v1.extend(v2);
10597 StageEmissions::Many(v1)
10598 }
10599 (a, b) => {
10600 let mut all = Vec::new();
10601 push_emissions(&mut all, a);
10602 push_emissions(&mut all, b);
10603 StageEmissions::Many(all)
10604 }
10605 }
10606}
10607
10608fn push_emissions(out: &mut Vec<(PortId, DatumValue)>, emissions: StageEmissions) {
10609 match emissions {
10610 StageEmissions::None => {}
10611 StageEmissions::One(p, v) => out.push((p, v)),
10612 StageEmissions::Two((p1, v1), (p2, v2)) => {
10613 out.push((p1, v1));
10614 out.push((p2, v2));
10615 }
10616 StageEmissions::Many(vec) => out.extend(vec),
10617 }
10618}