1use super::*;
12use crate::{Attribute, Attributes};
13
14type PartialGraphBuilder<S> = dyn Fn(&mut GraphBuilder) -> StreamResult<S> + Send + Sync;
15
16#[derive(Clone, Debug)]
17struct PortRecord {
18 kind: PortKind,
19 type_id: TypeId,
20 type_name: &'static str,
21 name: Arc<str>,
22}
23
24#[derive(Clone, Debug)]
25pub(super) struct Edge {
26 pub(super) outlet: PortId,
27 pub(super) inlet: PortId,
28}
29
30#[derive(Clone)]
31pub(super) struct StageRecord {
32 pub(super) spec: StageSpec,
33 pub(super) logic_factory: Option<Arc<dyn Fn() -> GraphStageLogic + Send + Sync>>,
34}
35
36impl std::fmt::Debug for StageRecord {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("StageRecord")
39 .field("spec", &self.spec)
40 .field("has_logic", &self.logic_factory.is_some())
41 .finish()
42 }
43}
44
45#[derive(Debug, Default)]
56pub struct GraphBuilder {
57 allocator: PortAllocator,
58 ports: HashMap<PortId, PortRecord>,
59 stages: Vec<StageRecord>,
60 edges: Vec<Edge>,
61 errors: Vec<StreamError>,
62}
63
64impl GraphBuilder {
65 #[must_use]
66 pub fn add<G: GraphStage>(&mut self, stage: G) -> G::Shape {
67 self.add_with_attributes(stage, Attributes::default())
68 }
69
70 #[must_use]
71 pub fn add_with_attributes<G: GraphStage>(
72 &mut self,
73 stage: G,
74 attributes: Attributes,
75 ) -> G::Shape {
76 let shape = stage.allocate_shape(&mut self.allocator);
77 let inlets = shape.inlets();
78 let outlets = shape.outlets();
79 self.ports.reserve(inlets.len() + outlets.len());
80
81 for inlet in &inlets {
82 self.register_inlet(inlet);
83 }
84 for outlet in &outlets {
85 self.register_outlet(outlet);
86 }
87 let spec = stage
88 .stage_spec_with_ports(&shape, inlets, outlets)
89 .add_attributes(attributes);
90 let logic_factory = if matches!(spec.kind, StageKind::Opaque) {
91 let shape_clone = shape.clone();
92 Some(Arc::new(move || stage.create_logic(&shape_clone))
93 as Arc<dyn Fn() -> GraphStageLogic + Send + Sync>)
94 } else {
95 None
96 };
97 self.stages.push(StageRecord {
98 spec,
99 logic_factory,
100 });
101 shape
102 }
103
104 #[must_use]
105 pub fn add_named<G: GraphStage>(&mut self, stage: G, name: impl Into<String>) -> G::Shape {
106 self.add_with_attributes(stage, Attributes::named(name))
107 }
108
109 pub fn connect<T: 'static>(&mut self, outlet: Outlet<T>, inlet: Inlet<T>) -> StreamResult<()> {
110 self.connect_any(outlet.erase(), inlet.erase())
111 }
112
113 pub fn connect_any(&mut self, outlet: AnyOutlet, inlet: AnyInlet) -> StreamResult<()> {
120 match self.connect_any_unrecorded(outlet, inlet) {
121 Ok(()) => Ok(()),
122 Err(error) => self.record_error(error),
123 }
124 }
125
126 pub fn import<S: Shape>(&mut self, graph: &PartialGraph<S>) -> StreamResult<S> {
127 graph.build(self)
128 }
129
130 pub fn wire<W: WireSpec>(&mut self, spec: W) -> &mut Self {
136 if let Err(error) = spec.apply(self) {
137 let _ = self.record_error(error);
138 }
139 self
140 }
141
142 pub fn try_wire<W: WireSpec>(&mut self, spec: W) -> StreamResult<&mut Self> {
148 match spec.apply(self) {
149 Ok(()) => Ok(self),
150 Err(error) => {
151 self.errors.push(error.clone());
152 Err(error)
153 }
154 }
155 }
156
157 pub(super) fn connect_any_unrecorded(
158 &mut self,
159 outlet: AnyOutlet,
160 inlet: AnyInlet,
161 ) -> StreamResult<()> {
162 self.validate_connection(&outlet, &inlet)?;
163 self.edges.push(Edge {
164 outlet: outlet.id(),
165 inlet: inlet.id(),
166 });
167 Ok(())
168 }
169
170 pub(super) fn is_outlet_connected(&self, outlet: &AnyOutlet) -> bool {
171 self.edges.iter().any(|edge| edge.outlet == outlet.id())
172 }
173
174 pub(super) fn is_inlet_connected(&self, inlet: &AnyInlet) -> bool {
175 self.edges.iter().any(|edge| edge.inlet == inlet.id())
176 }
177
178 fn record_error(&mut self, error: StreamError) -> StreamResult<()> {
179 self.errors.push(error.clone());
180 Err(error)
181 }
182
183 fn register_inlet(&mut self, inlet: &AnyInlet) {
184 self.ports.insert(
185 inlet.id(),
186 PortRecord {
187 kind: PortKind::Inlet,
188 type_id: inlet.type_id(),
189 type_name: inlet.type_name(),
190 name: Arc::clone(&inlet.name),
191 },
192 );
193 }
194
195 fn register_outlet(&mut self, outlet: &AnyOutlet) {
196 self.ports.insert(
197 outlet.id(),
198 PortRecord {
199 kind: PortKind::Outlet,
200 type_id: outlet.type_id(),
201 type_name: outlet.type_name(),
202 name: Arc::clone(&outlet.name),
203 },
204 );
205 }
206
207 fn validate_connection(&self, outlet: &AnyOutlet, inlet: &AnyInlet) -> StreamResult<()> {
208 let outlet_record = self.ports.get(&outlet.id()).ok_or_else(|| {
209 StreamError::GraphValidation(format!("unknown outlet {}", outlet.name()))
210 })?;
211 let inlet_record = self.ports.get(&inlet.id()).ok_or_else(|| {
212 StreamError::GraphValidation(format!("unknown inlet {}", inlet.name()))
213 })?;
214
215 if outlet_record.kind != PortKind::Outlet {
216 return Err(StreamError::GraphValidation(format!(
217 "{} is not an outlet",
218 outlet_record.name
219 )));
220 }
221 if inlet_record.kind != PortKind::Inlet {
222 return Err(StreamError::GraphValidation(format!(
223 "{} is not an inlet",
224 inlet_record.name
225 )));
226 }
227 if outlet_record.type_id != inlet_record.type_id {
228 return Err(StreamError::GraphValidation(format!(
229 "cannot connect outlet {} ({}) to inlet {} ({})",
230 outlet_record.name,
231 outlet_record.type_name,
232 inlet_record.name,
233 inlet_record.type_name
234 )));
235 }
236 if self.edges.iter().any(|edge| edge.outlet == outlet.id()) {
237 return Err(StreamError::GraphValidation(format!(
238 "outlet {} is already connected",
239 outlet_record.name
240 )));
241 }
242 if self.edges.iter().any(|edge| edge.inlet == inlet.id()) {
243 return Err(StreamError::GraphValidation(format!(
244 "inlet {} is already connected",
245 inlet_record.name
246 )));
247 }
248
249 Ok(())
250 }
251
252 pub(super) fn finish<S: Shape>(self, shape: S) -> StreamResult<GraphBlueprint<S>> {
253 let mut errors = self.errors;
254 let connected_inlets: HashSet<PortId> = self.edges.iter().map(|edge| edge.inlet).collect();
255 let connected_outlets: HashSet<PortId> =
256 self.edges.iter().map(|edge| edge.outlet).collect();
257
258 let open_inlets: HashSet<PortId> = self
259 .ports
260 .iter()
261 .filter_map(|(id, port)| {
262 (port.kind == PortKind::Inlet && !connected_inlets.contains(id)).then_some(*id)
263 })
264 .collect();
265 let open_outlets: HashSet<PortId> = self
266 .ports
267 .iter()
268 .filter_map(|(id, port)| {
269 (port.kind == PortKind::Outlet && !connected_outlets.contains(id)).then_some(*id)
270 })
271 .collect();
272
273 let result_inlets: HashSet<PortId> = shape.inlets().iter().map(AnyInlet::id).collect();
274 let result_outlets: HashSet<PortId> = shape.outlets().iter().map(AnyOutlet::id).collect();
275
276 for inlet in shape.inlets() {
277 match self.ports.get(&inlet.id()) {
278 Some(port)
279 if port.kind == PortKind::Inlet
280 && port.type_id == inlet.type_id()
281 && port.name.as_ref() == inlet.name() => {}
282 Some(port) if port.kind == PortKind::Inlet => {
283 errors.push(StreamError::GraphValidation(format!(
284 "result shape inlet {} does not match registered inlet {} ({})",
285 inlet.name(),
286 port.name,
287 port.type_name
288 )));
289 }
290 Some(port) => errors.push(StreamError::GraphValidation(format!(
291 "result shape references non-inlet port {}",
292 port.name
293 ))),
294 None => errors.push(StreamError::GraphValidation(format!(
295 "result shape references unknown inlet {}",
296 inlet.name()
297 ))),
298 }
299 }
300 for outlet in shape.outlets() {
301 match self.ports.get(&outlet.id()) {
302 Some(port)
303 if port.kind == PortKind::Outlet
304 && port.type_id == outlet.type_id()
305 && port.name.as_ref() == outlet.name() => {}
306 Some(port) if port.kind == PortKind::Outlet => {
307 errors.push(StreamError::GraphValidation(format!(
308 "result shape outlet {} does not match registered outlet {} ({})",
309 outlet.name(),
310 port.name,
311 port.type_name
312 )));
313 }
314 Some(port) => errors.push(StreamError::GraphValidation(format!(
315 "result shape references non-outlet port {}",
316 port.name
317 ))),
318 None => errors.push(StreamError::GraphValidation(format!(
319 "result shape references unknown outlet {}",
320 outlet.name()
321 ))),
322 }
323 }
324
325 if open_inlets != result_inlets {
326 errors.push(StreamError::GraphValidation(format!(
327 "result shape inlets do not match open inlets: open={:?}, result={:?}",
328 describe_ports(&self.ports, &open_inlets),
329 describe_ports(&self.ports, &result_inlets)
330 )));
331 }
332 if open_outlets != result_outlets {
333 errors.push(StreamError::GraphValidation(format!(
334 "result shape outlets do not match open outlets: open={:?}, result={:?}",
335 describe_ports(&self.ports, &open_outlets),
336 describe_ports(&self.ports, &result_outlets)
337 )));
338 }
339
340 if !errors.is_empty() {
341 return Err(StreamError::GraphValidation(
342 errors
343 .into_iter()
344 .map(|error| error.to_string())
345 .collect::<Vec<_>>()
346 .join("; "),
347 ));
348 }
349
350 let segments = compute_segments(&self.stages);
351 Ok(GraphBlueprint {
352 shape,
353 stages: self.stages,
354 edges: self.edges,
355 segments,
356 attributes: Attributes::default(),
357 })
358 }
359}
360
361fn describe_ports(ports: &HashMap<PortId, PortRecord>, ids: &HashSet<PortId>) -> Vec<String> {
362 let mut names = ids
363 .iter()
364 .map(|id| {
365 ports
366 .get(id)
367 .map(|port| port.name.as_ref().to_owned())
368 .unwrap_or_else(|| format!("unknown#{}", id.as_usize()))
369 })
370 .collect::<Vec<_>>();
371 names.sort();
372 names
373}
374
375fn compute_segments(stages: &[StageRecord]) -> Vec<FusedSegment> {
376 let mut segments = Vec::with_capacity(1);
377 let mut current = Vec::with_capacity(stages.len());
378
379 for (index, stage) in stages.iter().enumerate() {
380 if stage.spec.async_boundary && !current.is_empty() {
381 segments.push(FusedSegment {
382 stage_indices: std::mem::take(&mut current),
383 });
384 }
385 current.push(index);
386 if stage.spec.async_boundary {
387 segments.push(FusedSegment {
388 stage_indices: std::mem::take(&mut current),
389 });
390 }
391 }
392
393 if !current.is_empty() {
394 segments.push(FusedSegment {
395 stage_indices: current,
396 });
397 }
398
399 segments
400}
401
402pub struct GraphDsl;
407
408impl GraphDsl {
409 pub fn create<S, F>(build: F) -> StreamResult<GraphBlueprint<S>>
410 where
411 S: Shape,
412 F: FnOnce(&mut GraphBuilder) -> S,
413 {
414 let mut builder = GraphBuilder::default();
415 let shape = build(&mut builder);
416 builder.finish(shape)
417 }
418
419 pub fn try_create<S, F>(build: F) -> StreamResult<GraphBlueprint<S>>
420 where
421 S: Shape,
422 F: FnOnce(&mut GraphBuilder) -> StreamResult<S>,
423 {
424 let mut builder = GraphBuilder::default();
425 let shape = build(&mut builder)?;
426 builder.finish(shape)
427 }
428
429 pub fn partial<S, F>(build: F) -> PartialGraph<S>
430 where
431 S: Shape,
432 F: Fn(&mut GraphBuilder) -> StreamResult<S> + Send + Sync + 'static,
433 {
434 PartialGraph {
435 build: Arc::new(build),
436 attributes: Attributes::default(),
437 }
438 }
439}
440
441pub trait Graph {
443 type Shape: Shape;
444
445 fn shape(&self) -> Self::Shape;
446}
447
448#[derive(Clone, Debug)]
452pub struct FusedSegment {
453 stage_indices: Vec<usize>,
454}
455
456impl FusedSegment {
457 #[must_use]
458 pub fn stage_indices(&self) -> &[usize] {
459 &self.stage_indices
460 }
461}
462
463pub struct GraphBlueprint<S: Shape> {
469 pub(super) shape: S,
470 pub(super) stages: Vec<StageRecord>,
471 pub(super) edges: Vec<Edge>,
472 pub(super) segments: Vec<FusedSegment>,
473 pub(super) attributes: Attributes,
474}
475
476impl<S: Shape + Clone> Clone for GraphBlueprint<S> {
477 fn clone(&self) -> Self {
478 Self {
479 shape: self.shape.clone(),
480 stages: self.stages.clone(),
481 edges: self.edges.clone(),
482 segments: self.segments.clone(),
483 attributes: self.attributes.clone(),
484 }
485 }
486}
487
488impl<S: Shape + fmt::Debug> fmt::Debug for GraphBlueprint<S> {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 f.debug_struct("GraphBlueprint")
491 .field("shape", &self.shape)
492 .field("stages", &self.stages)
493 .field("edges", &self.edges)
494 .field("segments", &self.segments)
495 .field("attributes", &self.attributes)
496 .finish()
497 }
498}
499
500impl<S: Shape> GraphBlueprint<S> {
501 #[must_use]
502 pub fn shape(&self) -> S {
503 self.shape.clone()
504 }
505
506 #[must_use]
507 pub fn stage_count(&self) -> usize {
508 self.stages.len()
509 }
510
511 #[must_use]
512 pub fn edge_count(&self) -> usize {
513 self.edges.len()
514 }
515
516 #[must_use]
517 pub fn segments(&self) -> &[FusedSegment] {
518 &self.segments
519 }
520
521 #[must_use]
522 pub fn attributes(&self) -> &Attributes {
523 &self.attributes
524 }
525
526 #[must_use]
531 pub fn effective_stage_attributes(&self) -> Vec<FusedNodeAttributes> {
532 self.stage_attribute_reports()
533 }
534
535 #[must_use]
536 pub fn with_attributes(mut self, attributes: Attributes) -> Self {
537 self.attributes = attributes;
538 self
539 }
540
541 #[must_use]
542 pub fn add_attributes(mut self, attributes: Attributes) -> Self {
543 self.attributes = self.attributes.and(attributes);
544 self
545 }
546
547 #[must_use]
548 pub fn named(self, name: impl Into<String>) -> Self {
549 self.add_attributes(Attributes::named(name))
550 }
551
552 pub(super) fn effective_attributes_for_stage(&self, stage: &StageRecord) -> Attributes {
553 self.attributes.clone().and(stage.spec.attributes().clone())
554 }
555
556 pub(super) fn stage_attribute_reports(&self) -> Vec<FusedNodeAttributes> {
557 self.stages
558 .iter()
559 .enumerate()
560 .map(|(index, stage)| {
561 let attributes = self.effective_attributes_for_stage(stage);
562 FusedNodeAttributes {
563 stage_index: index,
564 stage_name: stage.spec.name().to_owned(),
565 effective_name: attributes
566 .name()
567 .map(str::to_owned)
568 .unwrap_or_else(|| stage.spec.name().to_owned()),
569 attributes,
570 }
571 })
572 .collect()
573 }
574
575 pub(super) fn plan_report(
576 &self,
577 selected_tier: FusedExecutorTier,
578 tier_changes: Vec<FusedTierChange>,
579 ) -> FusedPlanReport {
580 FusedPlanReport {
581 selected_tier,
582 stage_attributes: self.stage_attribute_reports(),
583 tier_changes,
584 }
585 }
586}
587
588impl<S: Shape> Graph for GraphBlueprint<S> {
589 type Shape = S;
590
591 fn shape(&self) -> Self::Shape {
592 self.shape()
593 }
594}
595
596#[derive(Clone)]
601pub struct PartialGraph<S: Shape> {
602 build: Arc<PartialGraphBuilder<S>>,
603 attributes: Attributes,
604}
605
606impl<S: Shape> PartialGraph<S> {
607 pub fn build(&self, builder: &mut GraphBuilder) -> StreamResult<S> {
608 (self.build)(builder)
609 }
610
611 #[must_use]
612 pub fn attributes(&self) -> &Attributes {
613 &self.attributes
614 }
615
616 #[must_use]
617 pub fn with_attributes(mut self, attributes: Attributes) -> Self {
618 self.attributes = attributes;
619 self
620 }
621
622 #[must_use]
623 pub fn add_attributes(mut self, attributes: Attributes) -> Self {
624 self.attributes = self.attributes.and(attributes);
625 self
626 }
627
628 #[must_use]
629 pub fn named(self, name: impl Into<String>) -> Self {
630 self.add_attributes(Attributes::named(name))
631 }
632}
633
634impl<S: Shape> std::fmt::Debug for PartialGraph<S> {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 f.debug_struct("PartialGraph")
637 .field("attributes", &self.attributes)
638 .finish_non_exhaustive()
639 }
640}
641
642pub type ImportedGraph<S> = PartialGraph<S>;
643
644#[derive(Clone, Copy, Debug, PartialEq, Eq)]
648pub struct FusedExecutionConfig {
649 pub event_limit: usize,
650}
651
652impl Default for FusedExecutionConfig {
653 fn default() -> Self {
654 Self {
655 event_limit: 100_000_000,
656 }
657 }
658}
659
660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
665pub struct AsyncBoundaryExecutionConfig {
666 pub fused: FusedExecutionConfig,
667 pub buffer_size: usize,
668}
669
670impl Default for AsyncBoundaryExecutionConfig {
671 fn default() -> Self {
672 Self {
673 fused: FusedExecutionConfig::default(),
674 buffer_size: 16,
675 }
676 }
677}
678
679#[derive(Clone, Debug, PartialEq, Eq)]
683pub struct FusedExecutionReport<T> {
684 pub output: Vec<T>,
685 pub events: usize,
686 pub async_boundary_crossings: usize,
687 pub plan: FusedPlanReport,
688 pub node_metrics: Vec<FusedNodeMetrics>,
689}
690
691#[derive(Clone, Debug, PartialEq, Eq)]
695pub struct FusedTerminalReport<T> {
696 pub result: T,
697 pub events: usize,
698 pub async_boundary_crossings: usize,
699 pub plan: FusedPlanReport,
700 pub node_metrics: Vec<FusedNodeMetrics>,
701}
702
703impl<T> FusedExecutionReport<T> {
704 pub(super) fn new(
705 output: Vec<T>,
706 events: usize,
707 async_boundary_crossings: usize,
708 plan: FusedPlanReport,
709 ) -> Self {
710 Self {
711 output,
712 events,
713 async_boundary_crossings,
714 plan,
715 node_metrics: Vec::new(),
716 }
717 }
718}
719
720impl<T> FusedTerminalReport<T> {
721 pub(super) fn new(
722 result: T,
723 events: usize,
724 async_boundary_crossings: usize,
725 plan: FusedPlanReport,
726 ) -> Self {
727 Self {
728 result,
729 events,
730 async_boundary_crossings,
731 plan,
732 node_metrics: Vec::new(),
733 }
734 }
735}
736
737impl<T> FusedExecutionReport<T> {
738 pub(super) fn with_node_metrics(mut self, node_metrics: Vec<FusedNodeMetrics>) -> Self {
739 self.node_metrics = node_metrics;
740 self
741 }
742
743 #[must_use]
745 pub fn metrics_snapshot(&self) -> Vec<FusedNodeMetrics> {
746 self.node_metrics.clone()
747 }
748}
749
750impl<T> FusedTerminalReport<T> {
751 pub(super) fn with_node_metrics(mut self, node_metrics: Vec<FusedNodeMetrics>) -> Self {
752 self.node_metrics = node_metrics;
753 self
754 }
755
756 #[must_use]
758 pub fn metrics_snapshot(&self) -> Vec<FusedNodeMetrics> {
759 self.node_metrics.clone()
760 }
761}
762
763#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
765pub enum FusedExecutorTier {
766 TypedLinear,
767 TypedAcyclicJunction,
768 TypedMergeSequence,
769 TypedMergeLatest,
770 TypedCyclicFeedback,
771 AsyncBoundary,
772 TypedFanIn,
773 TypedConcat,
774 TypedInterleave,
775 TypedMergePreferred,
776 Erased,
777}
778
779#[derive(Clone, Debug, PartialEq, Eq)]
781pub struct FusedPlanReport {
782 pub selected_tier: FusedExecutorTier,
783 pub stage_attributes: Vec<FusedNodeAttributes>,
784 pub tier_changes: Vec<FusedTierChange>,
785}
786
787impl FusedPlanReport {
788 pub(super) fn empty(selected_tier: FusedExecutorTier) -> Self {
789 Self {
790 selected_tier,
791 stage_attributes: Vec::new(),
792 tier_changes: Vec::new(),
793 }
794 }
795}
796
797#[derive(Clone, Debug, PartialEq, Eq)]
799pub struct FusedNodeAttributes {
800 pub stage_index: usize,
801 pub stage_name: String,
802 pub effective_name: String,
803 pub attributes: Attributes,
804}
805
806#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
809pub enum FusedTierChangeReason {
810 FusionErasedOnlyRequested,
811 FusionTypedOnlyConstrained,
812 AutoTypedUnsupportedFallback,
813}
814
815#[derive(Clone, Debug, PartialEq, Eq)]
817pub struct FusedTierChange {
818 pub requested_attribute: Option<Attribute>,
819 pub reason: FusedTierChangeReason,
820 pub selected_tier: FusedExecutorTier,
821 pub affected_nodes: Vec<String>,
822}