1use std::{collections::HashMap, future::Future, num::NonZeroUsize, pin::Pin, sync::Mutex, time::Duration};
2
3use async_trait::async_trait;
4use saluki_common::resource_tracking::Track as _;
5use saluki_common::sync::shutdown::ShutdownHandle;
6use saluki_error::{generic_error, ErrorContext as _, GenericError};
7use snafu::Snafu;
8use tokio::{pin, runtime::Handle, select, sync::oneshot};
9use tracing::info;
10
11use super::{
12 built::{BuiltTopology, WorkerPoolConfiguration},
13 graph::{Graph, GraphError},
14 ComponentId,
15};
16use crate::accounting::{ComponentRegistry, MemoryLimiter, UsageExpr};
17use crate::{
18 components::{
19 decoders::DecoderBuilder, destinations::DestinationBuilder, encoders::EncoderBuilder,
20 forwarders::ForwarderBuilder, relays::RelayBuilder, sources::SourceBuilder, transforms::TransformBuilder,
21 ComponentContext, ComponentType,
22 },
23 data_model::event::Event,
24 health::HealthRegistry,
25 runtime::{state::DataspaceRegistry, InitializationError, ShutdownStrategy, Supervisable, SupervisorFuture},
26 support::SubsystemIdentifier,
27 topology::{ids::AsComponentIds, topology_identifier, EventsBuffer, DEFAULT_EVENTS_BUFFER_CAPACITY},
28};
29
30const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
31
32#[derive(Debug, Snafu)]
34#[snafu(context(suffix(false)))]
35pub enum BlueprintError {
36 #[snafu(display("Failed to build/validate topology graph: {}", source))]
38 InvalidGraph {
39 source: GraphError,
41 },
42
43 #[snafu(display("Failed to build component '{}': {}", id, source))]
45 FailedToBuildComponent {
46 id: ComponentId,
48
49 source: GenericError,
51 },
52}
53
54pub struct TopologyBlueprint {
60 name: String,
61 build_state: Mutex<Option<TopologyBuildState>>,
62 health_registry: Option<HealthRegistry>,
63 memory_limiter: Option<MemoryLimiter>,
64}
65
66struct TopologyBuildState {
70 topology_id: SubsystemIdentifier,
71 graph: Graph,
72 sources: HashMap<ComponentId, Box<dyn SourceBuilder + Send>>,
73 relays: HashMap<ComponentId, Box<dyn RelayBuilder + Send>>,
74 decoders: HashMap<ComponentId, Box<dyn DecoderBuilder + Send>>,
75 transforms: HashMap<ComponentId, Box<dyn TransformBuilder + Send>>,
76 destinations: HashMap<ComponentId, Box<dyn DestinationBuilder + Send>>,
77 encoders: HashMap<ComponentId, Box<dyn EncoderBuilder + Send>>,
78 forwarders: HashMap<ComponentId, Box<dyn ForwarderBuilder + Send>>,
79 component_registry: ComponentRegistry,
80 interconnect_capacity: NonZeroUsize,
81 shutdown_timeout: Duration,
82 worker_pool_config: WorkerPoolConfiguration,
83 environment_ready: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
84 ready_signal: Option<oneshot::Sender<()>>,
85}
86
87impl TopologyBlueprint {
88 pub fn new(name: &str, component_registry: &ComponentRegistry) -> Self {
90 let topology_id = topology_identifier(name);
91 let component_registry = component_registry.clone();
92
93 let build_state = TopologyBuildState {
94 topology_id,
95 graph: Graph::default(),
96 sources: HashMap::new(),
97 relays: HashMap::new(),
98 decoders: HashMap::new(),
99 transforms: HashMap::new(),
100 destinations: HashMap::new(),
101 encoders: HashMap::new(),
102 forwarders: HashMap::new(),
103 component_registry,
104 interconnect_capacity: super::DEFAULT_INTERCONNECT_CAPACITY,
105 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
106 worker_pool_config: WorkerPoolConfiguration::Dedicated,
107 environment_ready: None,
108 ready_signal: None,
109 };
110
111 Self {
112 name: name.to_string(),
113 build_state: Mutex::new(Some(build_state)),
114 health_registry: None,
115 memory_limiter: None,
116 }
117 }
118
119 fn state_mut(&mut self) -> &mut TopologyBuildState {
125 self.build_state
126 .get_mut()
127 .expect("topology blueprint mutex poisoned")
128 .as_mut()
129 .expect("topology blueprint already initialized")
130 }
131
132 pub fn with_interconnect_capacity(&mut self, capacity: NonZeroUsize) -> &mut Self {
140 self.state_mut().set_interconnect_capacity(capacity);
141 self
142 }
143
144 pub fn with_shutdown_timeout(&mut self, timeout: Duration) -> &mut Self {
148 self.state_mut().shutdown_timeout = timeout;
149 self
150 }
151
152 pub fn with_health_registry(&mut self, health_registry: HealthRegistry) -> &mut Self {
156 self.health_registry = Some(health_registry);
157 self
158 }
159
160 pub fn with_memory_limiter(&mut self, memory_limiter: MemoryLimiter) -> &mut Self {
164 self.memory_limiter = Some(memory_limiter);
165 self
166 }
167
168 pub fn with_environment_readiness<F>(&mut self, ready: F) -> &mut Self
174 where
175 F: Future<Output = ()> + Send + 'static,
176 {
177 self.state_mut().environment_ready = Some(Box::pin(ready));
178 self
179 }
180
181 pub fn topology_ready(&mut self) -> TopologyReady {
190 let health_registry = self
191 .health_registry
192 .clone()
193 .expect("health registry must be set before acquiring a topology readiness handle");
194 let component_root = super::topology_identifier(&self.name);
195
196 let (registered_tx, registered_rx) = oneshot::channel();
197 self.state_mut().ready_signal = Some(registered_tx);
198
199 TopologyReady {
200 registered_rx,
201 health_registry,
202 component_root,
203 }
204 }
205
206 pub fn with_ambient_worker_pool(&mut self) -> &mut Self {
211 self.state_mut().worker_pool_config = WorkerPoolConfiguration::Ambient;
212 self
213 }
214
215 pub fn with_explicit_worker_pool(&mut self, handle: Handle) -> &mut Self {
219 self.state_mut().worker_pool_config = WorkerPoolConfiguration::Explicit(handle);
220 self
221 }
222
223 pub fn add_source<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
229 where
230 I: AsRef<str>,
231 B: SourceBuilder + Send + 'static,
232 {
233 self.state_mut().add_source(component_id, builder)?;
234 Ok(self)
235 }
236
237 pub fn add_relay<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
243 where
244 I: AsRef<str>,
245 B: RelayBuilder + Send + 'static,
246 {
247 self.state_mut().add_relay(component_id, builder)?;
248 Ok(self)
249 }
250
251 pub fn add_decoder<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
257 where
258 I: AsRef<str>,
259 B: DecoderBuilder + Send + 'static,
260 {
261 self.state_mut().add_decoder(component_id, builder)?;
262 Ok(self)
263 }
264
265 pub fn add_transform<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
271 where
272 I: AsRef<str>,
273 B: TransformBuilder + Send + 'static,
274 {
275 self.state_mut().add_transform(component_id, builder)?;
276 Ok(self)
277 }
278
279 pub fn add_destination<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
285 where
286 I: AsRef<str>,
287 B: DestinationBuilder + Send + 'static,
288 {
289 self.state_mut().add_destination(component_id, builder)?;
290 Ok(self)
291 }
292
293 pub fn add_encoder<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
299 where
300 I: AsRef<str>,
301 B: EncoderBuilder + Send + 'static,
302 {
303 self.state_mut().add_encoder(component_id, builder)?;
304 Ok(self)
305 }
306
307 pub fn add_forwarder<I, B>(&mut self, component_id: I, builder: B) -> Result<&mut Self, GenericError>
313 where
314 I: AsRef<str>,
315 B: ForwarderBuilder + Send + 'static,
316 {
317 self.state_mut().add_forwarder(component_id, builder)?;
318 Ok(self)
319 }
320
321 pub fn connect_components<MS, SI, MD, DI>(
336 &mut self, upstream_output_component_ids: SI, downstream_component_ids: DI,
337 ) -> Result<&mut Self, GenericError>
338 where
339 SI: AsComponentIds<MS>,
340 DI: AsComponentIds<MD>,
341 {
342 self.state_mut()
343 .connect_components(upstream_output_component_ids, downstream_component_ids)?;
344 Ok(self)
345 }
346
347 pub fn connect_components_in_order<IT, I>(&mut self, ordered_component_ids: IT) -> Result<&mut Self, GenericError>
369 where
370 IT: IntoIterator<Item = I>,
371 I: AsRef<str>,
372 {
373 self.state_mut().connect_components_in_order(ordered_component_ids)?;
374 Ok(self)
375 }
376}
377
378pub struct TopologyReady {
380 registered_rx: oneshot::Receiver<()>,
381 health_registry: HealthRegistry,
382 component_root: SubsystemIdentifier,
383}
384
385impl TopologyReady {
386 pub async fn wait(self) -> bool {
392 if self.registered_rx.await.is_err() {
397 return false;
398 }
399
400 self.health_registry.all_ready_under(self.component_root).await;
402
403 true
404 }
405}
406
407impl TopologyBuildState {
408 fn set_interconnect_capacity(&mut self, capacity: NonZeroUsize) {
409 self.interconnect_capacity = capacity;
410 self.recalculate_bounds();
411 }
412
413 fn recalculate_bounds(&mut self) {
414 let interconnect_capacity = self.interconnect_capacity.get();
415
416 let mut bounds_builder = self.component_registry.bounds_builder(&self.topology_id);
417 let mut bounds_builder = bounds_builder.subcomponent("interconnects");
418 bounds_builder.reset();
419
420 let total_interconnect_capacity = interconnect_capacity * (self.transforms.len() + self.destinations.len());
425 bounds_builder
426 .minimum()
427 .with_array::<EventsBuffer>("events", total_interconnect_capacity);
428
429 let max_in_flight_event_buffers = ((self.transforms.len() + self.destinations.len()) * interconnect_capacity)
440 + self.sources.len()
441 + self.decoders.len()
442 + self.transforms.len();
443
444 bounds_builder
445 .firm()
446 .with_expr(UsageExpr::product(
448 "events",
449 UsageExpr::constant("max in-flight event buffers", max_in_flight_event_buffers),
450 UsageExpr::sum(
451 "",
452 UsageExpr::struct_size::<EventsBuffer>("events buffer"),
453 UsageExpr::product(
454 "",
455 UsageExpr::struct_size::<Event>("event"),
456 UsageExpr::constant("default event buffer capacity", DEFAULT_EVENTS_BUFFER_CAPACITY),
457 ),
458 ),
459 ));
460 }
461
462 fn component_identity(&self, component_type: ComponentType, component_id: &ComponentId) -> SubsystemIdentifier {
463 ComponentContext::new(&self.topology_id, component_id.clone(), component_type).identity()
464 }
465
466 fn add_source<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
467 where
468 I: AsRef<str>,
469 B: SourceBuilder + Send + 'static,
470 {
471 let component_id = self
472 .graph
473 .add_source(component_id, &builder)
474 .error_context("Failed to add source to topology graph.")?;
475
476 let identity = self.component_identity(ComponentType::Source, &component_id);
477 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
478
479 self.recalculate_bounds();
480
481 let _ = self.sources.insert(component_id, Box::new(builder));
482
483 Ok(())
484 }
485
486 fn add_relay<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
487 where
488 I: AsRef<str>,
489 B: RelayBuilder + Send + 'static,
490 {
491 let component_id = self
492 .graph
493 .add_relay(component_id, &builder)
494 .error_context("Failed to add relay to topology graph.")?;
495
496 let identity = self.component_identity(ComponentType::Relay, &component_id);
497 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
498
499 self.recalculate_bounds();
500
501 let _ = self.relays.insert(component_id, Box::new(builder));
502
503 Ok(())
504 }
505
506 fn add_decoder<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
507 where
508 I: AsRef<str>,
509 B: DecoderBuilder + Send + 'static,
510 {
511 let component_id = self
512 .graph
513 .add_decoder(component_id, &builder)
514 .error_context("Failed to add decoder to topology graph.")?;
515
516 let identity = self.component_identity(ComponentType::Decoder, &component_id);
517 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
518
519 self.recalculate_bounds();
520
521 let _ = self.decoders.insert(component_id, Box::new(builder));
522
523 Ok(())
524 }
525
526 fn add_transform<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
527 where
528 I: AsRef<str>,
529 B: TransformBuilder + Send + 'static,
530 {
531 let component_id = self
532 .graph
533 .add_transform(component_id, &builder)
534 .error_context("Failed to add transform to topology graph.")?;
535
536 let identity = self.component_identity(ComponentType::Transform, &component_id);
537 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
538
539 self.recalculate_bounds();
540
541 let _ = self.transforms.insert(component_id, Box::new(builder));
542
543 Ok(())
544 }
545
546 fn add_destination<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
547 where
548 I: AsRef<str>,
549 B: DestinationBuilder + Send + 'static,
550 {
551 let component_id = self
552 .graph
553 .add_destination(component_id, &builder)
554 .error_context("Failed to add destination to topology graph.")?;
555
556 let identity = self.component_identity(ComponentType::Destination, &component_id);
557 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
558
559 self.recalculate_bounds();
560
561 let _ = self.destinations.insert(component_id, Box::new(builder));
562
563 Ok(())
564 }
565
566 fn add_encoder<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
567 where
568 I: AsRef<str>,
569 B: EncoderBuilder + Send + 'static,
570 {
571 let component_id = self
572 .graph
573 .add_encoder(component_id, &builder)
574 .error_context("Failed to add encoder to topology graph.")?;
575
576 let identity = self.component_identity(ComponentType::Encoder, &component_id);
577 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
578
579 self.recalculate_bounds();
580
581 let _ = self.encoders.insert(component_id, Box::new(builder));
582
583 Ok(())
584 }
585
586 fn add_forwarder<I, B>(&mut self, component_id: I, builder: B) -> Result<(), GenericError>
587 where
588 I: AsRef<str>,
589 B: ForwarderBuilder + Send + 'static,
590 {
591 let component_id = self
592 .graph
593 .add_forwarder(component_id, &builder)
594 .error_context("Failed to add forwarder to topology graph.")?;
595
596 let identity = self.component_identity(ComponentType::Forwarder, &component_id);
597 builder.specify_bounds(&mut self.component_registry.bounds_builder(&identity));
598
599 self.recalculate_bounds();
600
601 let _ = self.forwarders.insert(component_id, Box::new(builder));
602
603 Ok(())
604 }
605
606 fn connect_components<MS, SI, MD, DI>(
607 &mut self, upstream_output_component_ids: SI, downstream_component_ids: DI,
608 ) -> Result<(), GenericError>
609 where
610 SI: AsComponentIds<MS>,
611 DI: AsComponentIds<MD>,
612 {
613 for upstream_output_component_id in upstream_output_component_ids.as_component_ids() {
614 for downstream_component_id in downstream_component_ids.as_component_ids() {
615 self.graph
616 .add_edge(upstream_output_component_id.as_ref(), downstream_component_id.as_ref())
617 .error_context("Failed to add component connection to topology graph.")?;
618 }
619 }
620
621 Ok(())
622 }
623
624 fn connect_components_in_order<IT, I>(&mut self, ordered_component_ids: IT) -> Result<(), GenericError>
625 where
626 IT: IntoIterator<Item = I>,
627 I: AsRef<str>,
628 {
629 let mut pending_output_component_id: Option<I> = None;
630 let mut connected_any = false;
631
632 for component_id in ordered_component_ids.into_iter() {
633 if let Some(output_component_id) = pending_output_component_id.take() {
634 self.graph
635 .add_edge(output_component_id.as_ref(), component_id.as_ref())
636 .error_context("Failed to add component connection to topology graph.")?;
637
638 connected_any = true;
639 }
640
641 pending_output_component_id = Some(component_id);
643 }
644
645 if !connected_any {
647 return Err(generic_error!(
648 "Two or more components must be provided for connection."
649 ));
650 }
651
652 Ok(())
653 }
654
655 async fn build(self, name: String) -> Result<BuiltTopology, GenericError> {
661 self.graph.validate().error_context("Failed to build topology graph.")?;
662
663 let mut sources = HashMap::new();
664 for (id, builder) in self.sources {
665 let component_context = ComponentContext::source(&self.topology_id, id.clone());
666 let allocation_token = self
667 .component_registry
668 .get_resource_group_token(&component_context.identity());
669 let source = builder
670 .build(component_context.clone())
671 .track_resources(allocation_token)
672 .await
673 .with_error_context(|| format!("Failed to build source '{}'.", id))?;
674
675 sources.insert(component_context, (source, self.component_registry.clone()));
676 }
677
678 let mut relays = HashMap::new();
679 for (id, builder) in self.relays {
680 let component_context = ComponentContext::relay(&self.topology_id, id.clone());
681 let allocation_token = self
682 .component_registry
683 .get_resource_group_token(&component_context.identity());
684 let relay = builder
685 .build(component_context.clone())
686 .track_resources(allocation_token)
687 .await
688 .with_error_context(|| format!("Failed to build relay '{}'.", id))?;
689
690 relays.insert(component_context, (relay, self.component_registry.clone()));
691 }
692
693 let mut decoders = HashMap::new();
694 for (id, builder) in self.decoders {
695 let component_context = ComponentContext::decoder(&self.topology_id, id.clone());
696 let allocation_token = self
697 .component_registry
698 .get_resource_group_token(&component_context.identity());
699 let decoder = builder
700 .build(component_context.clone())
701 .track_resources(allocation_token)
702 .await
703 .with_error_context(|| format!("Failed to build decoder '{}'.", id))?;
704
705 decoders.insert(component_context, (decoder, self.component_registry.clone()));
706 }
707
708 let mut transforms = HashMap::new();
709 for (id, builder) in self.transforms {
710 let component_context = ComponentContext::transform(&self.topology_id, id.clone());
711 let allocation_token = self
712 .component_registry
713 .get_resource_group_token(&component_context.identity());
714 let transform = builder
715 .build(component_context.clone())
716 .track_resources(allocation_token)
717 .await
718 .with_error_context(|| format!("Failed to build transform '{}'.", id))?;
719
720 transforms.insert(component_context, (transform, self.component_registry.clone()));
721 }
722
723 let mut destinations = HashMap::new();
724 for (id, builder) in self.destinations {
725 let component_context = ComponentContext::destination(&self.topology_id, id.clone());
726 let allocation_token = self
727 .component_registry
728 .get_resource_group_token(&component_context.identity());
729 let destination = builder
730 .build(component_context.clone())
731 .track_resources(allocation_token)
732 .await
733 .with_error_context(|| format!("Failed to build destination '{}'.", id))?;
734
735 destinations.insert(component_context, (destination, self.component_registry.clone()));
736 }
737
738 let mut encoders = HashMap::new();
739 for (id, builder) in self.encoders {
740 let component_context = ComponentContext::encoder(&self.topology_id, id.clone());
741 let allocation_token = self
742 .component_registry
743 .get_resource_group_token(&component_context.identity());
744 let encoder = builder
745 .build(component_context.clone())
746 .track_resources(allocation_token)
747 .await
748 .with_error_context(|| format!("Failed to build encoder '{}'.", id))?;
749
750 encoders.insert(component_context, (encoder, self.component_registry.clone()));
751 }
752
753 let mut forwarders = HashMap::new();
754 for (id, builder) in self.forwarders {
755 let component_context = ComponentContext::forwarder(&self.topology_id, id.clone());
756 let allocation_token = self
757 .component_registry
758 .get_resource_group_token(&component_context.identity());
759 let forwarder = builder
760 .build(component_context.clone())
761 .track_resources(allocation_token)
762 .await
763 .with_error_context(|| format!("Failed to build forwarder '{}'.", id))?;
764
765 forwarders.insert(component_context, (forwarder, self.component_registry.clone()));
766 }
767
768 let topology_token = self.component_registry.get_resource_group_token(&self.topology_id);
769
770 Ok(BuiltTopology::from_parts(
771 name,
772 self.topology_id,
773 self.graph,
774 sources,
775 relays,
776 decoders,
777 transforms,
778 destinations,
779 encoders,
780 forwarders,
781 topology_token,
782 self.interconnect_capacity,
783 self.worker_pool_config,
784 ))
785 }
786}
787
788#[async_trait]
789impl Supervisable for TopologyBlueprint {
790 fn name(&self) -> &str {
791 &self.name
792 }
793
794 fn shutdown_strategy(&self) -> ShutdownStrategy {
795 ShutdownStrategy::Graceful(Duration::MAX)
798 }
799
800 async fn initialize(&self, shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
801 let mut build_state = self
805 .build_state
806 .lock()
807 .expect("topology blueprint mutex poisoned")
808 .take()
809 .ok_or_else(|| generic_error!("Topology has already been initialized and cannot be run more than once."))?;
810
811 let health_registry = self
812 .health_registry
813 .clone()
814 .ok_or_else(|| generic_error!("Topology blueprint is missing its health registry."))?;
815 let memory_limiter = self
816 .memory_limiter
817 .clone()
818 .ok_or_else(|| generic_error!("Topology blueprint is missing its memory limiter."))?;
819
820 let dataspace = DataspaceRegistry::try_current()
821 .ok_or_else(|| generic_error!("Topology must be initialized within a supervised process context."))?;
822
823 let environment_ready = build_state.environment_ready.take();
831 let ready_signal = build_state.ready_signal.take();
832 let shutdown_timeout = build_state.shutdown_timeout;
833 let built = build_state.build(self.name.clone()).await?;
834
835 Ok(Box::pin(async move {
836 pin!(shutdown);
837
838 if let Some(environment_ready) = environment_ready {
841 select! {
842 _ = &mut shutdown => return Ok(()),
843 _ = environment_ready => {},
844 }
845 }
846
847 let mut topology_sup = built
850 .spawn_inner(&health_registry, memory_limiter, dataspace.clone(), shutdown_timeout)
851 .await?;
852
853 if let Some(ready_signal) = ready_signal {
857 let _ = ready_signal.send(());
858 }
859
860 let (topology_shutdown_trigger, topology_shutdown) = ShutdownHandle::paired();
867 let run = topology_sup.run_with_shutdown_inner(topology_shutdown, Some(dataspace));
868 pin!(run);
869
870 let mut topology_shutdown_trigger = Some(topology_shutdown_trigger);
871 loop {
872 select! {
873 result = &mut run => return result.map_err(Into::into),
874 _ = &mut shutdown, if topology_shutdown_trigger.is_some() => {
875 info!("Topology received shutdown signal. Shutting down...");
876 topology_shutdown_trigger.take().expect("present per select guard").shutdown();
877 }
878 }
879 }
880 }))
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use std::num::NonZeroUsize;
887 use std::sync::atomic::{AtomicUsize, Ordering};
888 use std::sync::Arc;
889 use std::time::Duration;
890
891 use async_trait::async_trait;
892 use saluki_common::sync::shutdown::ShutdownHandle;
893 use saluki_error::GenericError;
894 use tokio::sync::oneshot;
895
896 use super::{TopologyBlueprint, TopologyReady, WorkerPoolConfiguration};
897 use crate::accounting::{ComponentRegistry, MemoryBounds, MemoryBoundsBuilder, MemoryLimiter};
898 use crate::data_model::event::Event;
899 use crate::runtime::Name;
900 use crate::test_support::wait_until;
901 use crate::topology::{ids::get_component_relative_identifier, topology_identifier, ComponentId};
902 use crate::topology::{EventsBuffer, DEFAULT_EVENTS_BUFFER_CAPACITY};
903 use crate::{
904 components::{
905 destinations::{Destination, DestinationBuilder, DestinationContext},
906 sources::{Source, SourceBuilder, SourceContext},
907 ComponentContext,
908 },
909 data_model::event::EventType,
910 health::HealthRegistry,
911 runtime::{
912 InitializationError, RestartMode, RestartStrategy, Supervisable, Supervisor, SupervisorError,
913 SupervisorFuture,
914 },
915 support::SubsystemIdentifier,
916 topology::{
917 test_util::{TestDestinationBuilder, TestSourceBuilder, TestTransformBuilder},
918 OutputDefinition,
919 },
920 };
921
922 struct CountingChild {
926 started: Arc<AtomicUsize>,
927 }
928
929 #[async_trait]
930 impl Supervisable for CountingChild {
931 fn name(&self) -> &str {
932 "child"
933 }
934
935 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
936 let started = Arc::clone(&self.started);
937 Ok(Box::pin(async move {
938 started.fetch_add(1, Ordering::SeqCst);
939 process_shutdown.await;
941 Ok(())
942 }))
943 }
944 }
945
946 struct ControlSource {
950 started: Arc<AtomicUsize>,
951 spawned_child: Option<Arc<AtomicUsize>>,
952 }
953
954 #[async_trait]
955 impl Source for ControlSource {
956 async fn run(self: Box<Self>, mut context: SourceContext) -> Result<(), GenericError> {
957 self.started.fetch_add(1, Ordering::SeqCst);
958 let shutdown = context.take_shutdown_handle();
959 if let Some(started) = self.spawned_child {
960 context
961 .spawn_handle()
962 .spawn(CountingChild { started })
963 .await
964 .expect("should spawn dynamic child");
965 }
966 shutdown.await;
967 Ok(())
968 }
969 }
970
971 struct ControlSourceBuilder {
972 outputs: Vec<OutputDefinition<EventType>>,
973 started: Arc<AtomicUsize>,
974 spawned_child: Option<Arc<AtomicUsize>>,
975 }
976
977 impl ControlSourceBuilder {
978 fn new(started: Arc<AtomicUsize>, spawned_child: Option<Arc<AtomicUsize>>) -> Self {
979 Self {
980 outputs: vec![OutputDefinition::default_output(EventType::EventD)],
981 started,
982 spawned_child,
983 }
984 }
985 }
986
987 #[async_trait]
988 impl SourceBuilder for ControlSourceBuilder {
989 fn outputs(&self) -> &[OutputDefinition<EventType>] {
990 &self.outputs
991 }
992
993 async fn build(&self, _: ComponentContext) -> Result<Box<dyn Source + Send>, GenericError> {
994 Ok(Box::new(ControlSource {
995 started: Arc::clone(&self.started),
996 spawned_child: self.spawned_child.clone(),
997 }))
998 }
999 }
1000
1001 impl MemoryBounds for ControlSourceBuilder {
1002 fn specify_bounds(&self, _builder: &mut MemoryBoundsBuilder) {}
1003 }
1004
1005 struct DrainingDestination;
1007
1008 #[async_trait]
1009 impl Destination for DrainingDestination {
1010 async fn run(self: Box<Self>, mut context: DestinationContext) -> Result<(), GenericError> {
1011 while context.events().next().await.is_some() {}
1012 Ok(())
1013 }
1014 }
1015
1016 struct DrainingDestinationBuilder {
1017 input_event_ty: EventType,
1018 }
1019
1020 #[async_trait]
1021 impl DestinationBuilder for DrainingDestinationBuilder {
1022 fn input_event_type(&self) -> EventType {
1023 self.input_event_ty
1024 }
1025
1026 async fn build(&self, _: ComponentContext) -> Result<Box<dyn Destination + Send>, GenericError> {
1027 Ok(Box::new(DrainingDestination))
1028 }
1029 }
1030
1031 impl MemoryBounds for DrainingDestinationBuilder {
1032 fn specify_bounds(&self, _builder: &mut MemoryBoundsBuilder) {}
1033 }
1034
1035 struct StuckDestination {
1039 started: Arc<AtomicUsize>,
1040 }
1041
1042 #[async_trait]
1043 impl Destination for StuckDestination {
1044 async fn run(self: Box<Self>, _context: DestinationContext) -> Result<(), GenericError> {
1045 self.started.fetch_add(1, Ordering::SeqCst);
1046 std::future::pending::<()>().await;
1047 Ok(())
1048 }
1049 }
1050
1051 struct StuckDestinationBuilder {
1052 input_event_ty: EventType,
1053 started: Arc<AtomicUsize>,
1054 }
1055
1056 #[async_trait]
1057 impl DestinationBuilder for StuckDestinationBuilder {
1058 fn input_event_type(&self) -> EventType {
1059 self.input_event_ty
1060 }
1061
1062 async fn build(&self, _: ComponentContext) -> Result<Box<dyn Destination + Send>, GenericError> {
1063 Ok(Box::new(StuckDestination {
1064 started: Arc::clone(&self.started),
1065 }))
1066 }
1067 }
1068
1069 impl MemoryBounds for StuckDestinationBuilder {
1070 fn specify_bounds(&self, _builder: &mut MemoryBoundsBuilder) {}
1071 }
1072
1073 fn long_running_blueprint(spawned_child: Option<Arc<AtomicUsize>>) -> (TopologyBlueprint, Arc<AtomicUsize>) {
1081 let source_started = Arc::new(AtomicUsize::new(0));
1082 let component_registry = ComponentRegistry::default();
1083 let mut blueprint = TopologyBlueprint::new("test", &component_registry);
1084 blueprint
1085 .add_source(
1086 "source",
1087 ControlSourceBuilder::new(Arc::clone(&source_started), spawned_child),
1088 )
1089 .expect("should not fail to add source")
1090 .add_destination(
1091 "destination",
1092 DrainingDestinationBuilder {
1093 input_event_ty: EventType::EventD,
1094 },
1095 )
1096 .expect("should not fail to add destination");
1097 blueprint
1098 .connect_components_in_order(["source", "destination"])
1099 .expect("should not fail to connect components");
1100 blueprint
1101 .with_health_registry(HealthRegistry::new())
1102 .with_memory_limiter(MemoryLimiter::noop());
1103 (blueprint, source_started)
1104 }
1105
1106 fn stuck_destination_blueprint(shutdown_timeout: Duration) -> (TopologyBlueprint, Arc<AtomicUsize>) {
1115 let destination_started = Arc::new(AtomicUsize::new(0));
1116 let component_registry = ComponentRegistry::default();
1117 let mut blueprint = TopologyBlueprint::new("test", &component_registry);
1118 blueprint
1119 .add_source("source", ControlSourceBuilder::new(Arc::new(AtomicUsize::new(0)), None))
1120 .expect("should not fail to add source")
1121 .add_destination(
1122 "destination",
1123 StuckDestinationBuilder {
1124 input_event_ty: EventType::EventD,
1125 started: Arc::clone(&destination_started),
1126 },
1127 )
1128 .expect("should not fail to add destination");
1129 blueprint
1130 .connect_components_in_order(["source", "destination"])
1131 .expect("should not fail to connect components");
1132 blueprint
1133 .with_health_registry(HealthRegistry::new())
1134 .with_memory_limiter(MemoryLimiter::noop())
1135 .with_shutdown_timeout(shutdown_timeout);
1136 (blueprint, destination_started)
1137 }
1138
1139 fn spawn_supervised_blueprint(
1145 blueprint: TopologyBlueprint,
1146 ) -> (
1147 oneshot::Sender<()>,
1148 tokio::task::JoinHandle<Result<(), SupervisorError>>,
1149 ) {
1150 let mut supervisor = Supervisor::new("test-topology").expect("should not fail to create supervisor");
1151 supervisor.add_worker(blueprint);
1152
1153 let (tx, rx) = oneshot::channel::<()>();
1154 let handle = tokio::spawn(async move { supervisor.run_with_shutdown(rx).await });
1155 (tx, handle)
1156 }
1157
1158 async fn run_blueprint_until_exit(
1161 blueprint: TopologyBlueprint, strategy: RestartStrategy,
1162 ) -> Result<(), SupervisorError> {
1163 let mut supervisor = Supervisor::new("test-topology")
1164 .expect("should not fail to create supervisor")
1165 .with_restart_strategy(strategy);
1166 supervisor.add_worker(blueprint);
1167
1168 let (_tx, rx) = oneshot::channel::<()>();
1170 tokio::time::timeout(Duration::from_secs(5), supervisor.run_with_shutdown(rx))
1171 .await
1172 .expect("supervisor should exit promptly")
1173 }
1174
1175 async fn join_topology(
1177 handle: tokio::task::JoinHandle<Result<(), SupervisorError>>,
1178 ) -> Result<(), SupervisorError> {
1179 tokio::time::timeout(Duration::from_secs(5), handle)
1180 .await
1181 .expect("supervisor should exit promptly")
1182 .expect("supervisor task should not panic")
1183 }
1184
1185 fn blueprint_with_components() -> TopologyBlueprint {
1189 let component_registry = ComponentRegistry::default();
1190 let mut blueprint = TopologyBlueprint::new("test", &component_registry);
1191
1192 blueprint
1193 .add_source("source", TestSourceBuilder::default_output(EventType::EventD))
1194 .expect("should not fail to add source")
1195 .add_transform(
1196 "transform",
1197 TestTransformBuilder::default_output(EventType::EventD, EventType::EventD),
1198 )
1199 .expect("should not fail to add transform")
1200 .add_destination(
1201 "destination",
1202 TestDestinationBuilder::with_input_type(EventType::EventD),
1203 )
1204 .expect("should not fail to add destination");
1205
1206 blueprint
1207 }
1208
1209 fn blueprint_with_sources_and_destinations(source_ids: &[&str], destination_ids: &[&str]) -> TopologyBlueprint {
1214 let component_registry = ComponentRegistry::default();
1215 let mut blueprint = TopologyBlueprint::new("test", &component_registry);
1216
1217 for source_id in source_ids {
1218 blueprint
1219 .add_source(*source_id, TestSourceBuilder::default_output(EventType::EventD))
1220 .expect("should not fail to add source");
1221 }
1222
1223 for destination_id in destination_ids {
1224 blueprint
1225 .add_destination(
1226 *destination_id,
1227 TestDestinationBuilder::with_input_type(EventType::EventD),
1228 )
1229 .expect("should not fail to add destination");
1230 }
1231
1232 blueprint
1233 }
1234
1235 fn connected_pairs(blueprint: &TopologyBlueprint) -> Vec<(String, String)> {
1237 let guard = blueprint.build_state.lock().expect("topology blueprint mutex poisoned");
1238 let outbound_edges = guard
1239 .as_ref()
1240 .expect("topology blueprint already initialized")
1241 .graph
1242 .get_outbound_directed_edges();
1243
1244 let mut pairs = Vec::new();
1245 for (from, outputs) in &outbound_edges {
1246 for targets in outputs.values() {
1247 for to in targets {
1248 pairs.push((from.component_id().to_string(), to.component_id().to_string()));
1249 }
1250 }
1251 }
1252 pairs.sort();
1253 pairs
1254 }
1255
1256 #[test]
1257 fn connect_components_in_order_errors_with_fewer_than_two_ids() {
1258 let mut blueprint = blueprint_with_components();
1259
1260 let result = blueprint.connect_components_in_order(Vec::<&str>::new()).map(|_| ());
1262 assert!(result.is_err());
1263
1264 let result = blueprint.connect_components_in_order(["source"]).map(|_| ());
1266 assert!(result.is_err());
1267
1268 assert!(connected_pairs(&blueprint).is_empty());
1270 }
1271
1272 #[test]
1273 fn connect_components_in_order_connects_pairwise_left_to_right() {
1274 let mut blueprint = blueprint_with_components();
1275
1276 blueprint
1277 .connect_components_in_order(["source", "transform", "destination"])
1278 .expect("should not fail to connect components in order");
1279
1280 assert_eq!(
1283 connected_pairs(&blueprint),
1284 vec![
1285 ("source".to_string(), "transform".to_string()),
1286 ("transform".to_string(), "destination".to_string()),
1287 ],
1288 );
1289 }
1290
1291 #[test]
1292 fn connect_component_one_to_many_fans_out() {
1293 let mut blueprint = blueprint_with_sources_and_destinations(&["source"], &["dest_a", "dest_b"]);
1296
1297 blueprint
1298 .connect_components("source", ["dest_a", "dest_b"])
1299 .expect("should not fail to connect component");
1300
1301 assert_eq!(
1302 connected_pairs(&blueprint),
1303 vec![
1304 ("source".to_string(), "dest_a".to_string()),
1305 ("source".to_string(), "dest_b".to_string()),
1306 ],
1307 );
1308 }
1309
1310 #[test]
1311 fn connect_component_many_to_one_fans_in() {
1312 let mut blueprint = blueprint_with_sources_and_destinations(&["source_a", "source_b"], &["dest"]);
1315
1316 blueprint
1317 .connect_components(["source_a", "source_b"], "dest")
1318 .expect("should not fail to connect component");
1319
1320 assert_eq!(
1321 connected_pairs(&blueprint),
1322 vec![
1323 ("source_a".to_string(), "dest".to_string()),
1324 ("source_b".to_string(), "dest".to_string()),
1325 ],
1326 );
1327 }
1328
1329 #[test]
1330 fn connect_component_many_to_many_creates_mesh() {
1331 let mut blueprint = blueprint_with_sources_and_destinations(&["source_a", "source_b"], &["dest_a", "dest_b"]);
1334
1335 blueprint
1336 .connect_components(["source_a", "source_b"], ["dest_a", "dest_b"])
1337 .expect("should not fail to connect component");
1338
1339 assert_eq!(
1340 connected_pairs(&blueprint),
1341 vec![
1342 ("source_a".to_string(), "dest_a".to_string()),
1343 ("source_a".to_string(), "dest_b".to_string()),
1344 ("source_b".to_string(), "dest_a".to_string()),
1345 ("source_b".to_string(), "dest_b".to_string()),
1346 ],
1347 );
1348 }
1349
1350 fn connected_blueprint() -> TopologyBlueprint {
1352 let mut blueprint = blueprint_with_components();
1353 blueprint
1354 .connect_components_in_order(["source", "transform", "destination"])
1355 .expect("should not fail to connect components");
1356 blueprint
1357 }
1358
1359 #[tokio::test]
1360 async fn topology_failure_shuts_down_supervisor() {
1361 let mut blueprint = connected_blueprint();
1365 blueprint
1366 .with_health_registry(HealthRegistry::new())
1367 .with_memory_limiter(MemoryLimiter::noop());
1368
1369 let result = run_blueprint_until_exit(
1370 blueprint,
1371 RestartStrategy::new(RestartMode::OneForOne, 0, Duration::from_secs(5)),
1372 )
1373 .await;
1374
1375 assert!(matches!(result, Err(SupervisorError::Shutdown)));
1376 }
1377
1378 #[tokio::test]
1379 async fn topology_cannot_be_initialized_more_than_once() {
1380 let mut blueprint = connected_blueprint();
1385 blueprint
1386 .with_health_registry(HealthRegistry::new())
1387 .with_memory_limiter(MemoryLimiter::noop());
1388
1389 let result = run_blueprint_until_exit(blueprint, RestartStrategy::default()).await;
1390
1391 assert!(matches!(result, Err(SupervisorError::FailedToInitialize { .. })));
1392 }
1393
1394 #[tokio::test]
1395 async fn topology_waits_for_environment_readiness_before_starting() {
1396 let gate_reached = Arc::new(AtomicUsize::new(0));
1403 let gate = {
1404 let gate_reached = Arc::clone(&gate_reached);
1405 async move {
1406 gate_reached.fetch_add(1, Ordering::SeqCst);
1407 std::future::pending::<()>().await;
1408 }
1409 };
1410
1411 let mut blueprint = connected_blueprint();
1412 blueprint
1413 .with_health_registry(HealthRegistry::new())
1414 .with_memory_limiter(MemoryLimiter::noop())
1415 .with_environment_readiness(gate);
1416
1417 let (tx, handle) = spawn_supervised_blueprint(blueprint);
1418
1419 wait_until("the topology reached the readiness gate", || {
1422 gate_reached.load(Ordering::SeqCst) >= 1
1423 })
1424 .await;
1425 tx.send(()).expect("should send shutdown signal");
1426
1427 let result = join_topology(handle).await;
1428 assert!(result.is_ok(), "supervisor should shut down cleanly, got: {:?}", result);
1429 }
1430
1431 #[test]
1432 fn topology_ready_waits_for_registration_before_checking_readiness() {
1433 use tokio_test::{assert_pending, assert_ready, task::spawn};
1434
1435 let health_registry = HealthRegistry::new();
1436
1437 let mut other = health_registry
1440 .register_component(&SubsystemIdentifier::from_dotted("env_provider.workload.foo"))
1441 .expect("should register component");
1442 other.mark_ready();
1443
1444 let (registered_tx, registered_rx) = oneshot::channel();
1445 let topology_ready = TopologyReady {
1446 registered_rx,
1447 health_registry: health_registry.clone(),
1448 component_root: topology_identifier("primary"),
1449 };
1450
1451 let mut wait = spawn(topology_ready.wait());
1452
1453 assert_pending!(wait.poll());
1456
1457 let mut source = health_registry
1459 .register_component(&SubsystemIdentifier::from_dotted("topology.primary.sources.in"))
1460 .expect("should register component");
1461
1462 registered_tx.send(()).expect("receiver should be alive");
1465 assert_pending!(wait.poll());
1466
1467 source.mark_ready();
1469 assert!(assert_ready!(wait.poll()));
1470 }
1471
1472 #[tokio::test]
1473 async fn topology_ready_returns_false_when_torn_down_before_registration() {
1474 let health_registry = HealthRegistry::new();
1475
1476 let (registered_tx, registered_rx) = oneshot::channel::<()>();
1477 let topology_ready = TopologyReady {
1478 registered_rx,
1479 health_registry,
1480 component_root: topology_identifier("primary"),
1481 };
1482
1483 drop(registered_tx);
1486
1487 assert!(!topology_ready.wait().await);
1488 }
1489
1490 #[tokio::test]
1491 async fn topology_clean_shutdown_returns_ok() {
1492 let (blueprint, source_started) = long_running_blueprint(None);
1497 let (tx, handle) = spawn_supervised_blueprint(blueprint);
1498
1499 wait_until("the source has started", || source_started.load(Ordering::SeqCst) == 1).await;
1502 tx.send(()).expect("should send shutdown signal");
1503
1504 let result = join_topology(handle).await;
1505 assert!(result.is_ok(), "topology should shut down cleanly, got: {:?}", result);
1506 }
1507
1508 #[tokio::test]
1509 async fn component_can_spawn_dynamic_child_via_spawn_handle() {
1510 let child_started = Arc::new(AtomicUsize::new(0));
1515 let (blueprint, _source_started) = long_running_blueprint(Some(Arc::clone(&child_started)));
1516 let (tx, handle) = spawn_supervised_blueprint(blueprint);
1517
1518 wait_until("the dynamic child has started", || {
1520 child_started.load(Ordering::SeqCst) == 1
1521 })
1522 .await;
1523 assert_eq!(
1524 child_started.load(Ordering::SeqCst),
1525 1,
1526 "dynamic child should have started exactly once"
1527 );
1528
1529 tx.send(()).expect("should send shutdown signal");
1530 let result = join_topology(handle).await;
1531 assert!(result.is_ok(), "topology should shut down cleanly, got: {:?}", result);
1532 }
1533
1534 #[tokio::test]
1535 async fn forced_abort_count_survives_topology_boundary_to_root() {
1536 let (blueprint, destination_started) = stuck_destination_blueprint(Duration::from_millis(100));
1546 let (tx, handle) = spawn_supervised_blueprint(blueprint);
1547
1548 wait_until("the stuck destination has started", || {
1551 destination_started.load(Ordering::SeqCst) == 1
1552 })
1553 .await;
1554 tx.send(()).expect("should send shutdown signal");
1555
1556 let result = join_topology(handle).await;
1557 assert!(
1558 matches!(result, Err(SupervisorError::ShutdownTimedOut { aborted: 1 })),
1559 "a component that ignored shutdown must surface as an unclean shutdown with a count of 1, got {result:?}"
1560 );
1561 }
1562
1563 #[test]
1564 fn component_identity_is_byte_identical_across_subsystems() {
1565 for (topology_name, raw_id) in [("primary", "dsd_in"), ("secondary", "dsd_agg")] {
1570 let topology_root = topology_identifier(topology_name);
1572 let component_context = ComponentContext::source(&topology_root, ComponentId::try_from(raw_id).unwrap());
1573 let component_canonical_id = component_context.identity().to_string();
1574 let component_relative_id =
1575 get_component_relative_identifier(component_context.component_type(), component_context.component_id());
1576
1577 assert_eq!(
1578 component_canonical_id,
1579 format!("topology.{topology_name}.sources.{raw_id}")
1580 );
1581
1582 let registry = ComponentRegistry::default();
1585 registry
1586 .bounds_builder(&component_context.identity())
1587 .firm()
1588 .with_fixed_amount("marker", 1);
1589
1590 let bounds = registry.as_bounds();
1591 let mut node = &bounds;
1592 for segment in component_canonical_id.split('.') {
1593 node = node
1594 .subcomponents()
1595 .into_iter()
1596 .find_map(|(name, child)| (name.as_str() == segment).then_some(child))
1597 .expect("resource-accounting node path must match the canonical identity segment by segment");
1598 }
1599 assert_eq!(
1600 node.total_firm_limit_bytes(),
1601 1,
1602 "the resource-accounting node at the canonical path must hold the declared bounds"
1603 );
1604
1605 let topology_sup = Name::root(topology_root.to_string()).expect("topology name is non-empty");
1608 let process_name =
1609 Name::scoped(&topology_sup, component_relative_id.to_string()).expect("component name is non-empty");
1610 assert_eq!(
1611 &*process_name,
1612 component_canonical_id.as_str(),
1613 "per-component supervisor process name must match the canonical identity"
1614 );
1615 }
1616 }
1617
1618 #[test]
1619 fn recalculate_bounds_accounts_for_interconnect_and_event_buffer_memory() {
1620 let interconnect_capacity = 4usize;
1625 let mut blueprint = blueprint_with_components();
1626 blueprint.with_interconnect_capacity(NonZeroUsize::new(interconnect_capacity).unwrap());
1627
1628 let (sources, transforms, destinations, decoders) = (1usize, 1usize, 1usize, 0usize);
1630
1631 let total_interconnect_capacity = interconnect_capacity * (transforms + destinations);
1634 let expected_min = total_interconnect_capacity * std::mem::size_of::<EventsBuffer>();
1635
1636 let max_in_flight = ((transforms + destinations) * interconnect_capacity) + sources + decoders + transforms;
1639 let per_buffer =
1640 std::mem::size_of::<EventsBuffer>() + (std::mem::size_of::<Event>() * DEFAULT_EVENTS_BUFFER_CAPACITY);
1641 let expected_firm = expected_min + (max_in_flight * per_buffer);
1642
1643 let bounds = {
1644 let guard = blueprint.build_state.lock().expect("topology blueprint mutex poisoned");
1645 guard
1646 .as_ref()
1647 .expect("topology blueprint already initialized")
1648 .component_registry
1649 .as_bounds()
1650 };
1651
1652 assert_eq!(
1653 bounds.total_minimum_required_bytes(),
1654 expected_min,
1655 "interconnect minimum bytes should be capacity * non-source components * size_of::<EventsBuffer>()"
1656 );
1657 assert_eq!(
1658 bounds.total_firm_limit_bytes(),
1659 expected_firm,
1660 "firm bytes should add the max in-flight event-buffer memory on top of the minimum"
1661 );
1662 }
1663
1664 #[test]
1665 fn worker_pool_configuration_defaults_to_dedicated() {
1666 let blueprint = blueprint_with_components();
1667 let guard = blueprint.build_state.lock().expect("topology blueprint mutex poisoned");
1668 let config = &guard
1669 .as_ref()
1670 .expect("topology blueprint already initialized")
1671 .worker_pool_config;
1672 assert!(
1673 matches!(config, WorkerPoolConfiguration::Dedicated),
1674 "the default worker-pool configuration must be dedicated"
1675 );
1676 }
1677
1678 #[test]
1679 fn with_ambient_worker_pool_selects_ambient() {
1680 let mut blueprint = blueprint_with_components();
1681 blueprint.with_ambient_worker_pool();
1682
1683 let guard = blueprint.build_state.lock().expect("topology blueprint mutex poisoned");
1684 let config = &guard
1685 .as_ref()
1686 .expect("topology blueprint already initialized")
1687 .worker_pool_config;
1688 assert!(matches!(config, WorkerPoolConfiguration::Ambient));
1689 }
1690
1691 #[tokio::test]
1692 async fn with_explicit_worker_pool_selects_explicit() {
1693 let mut blueprint = blueprint_with_components();
1694 blueprint.with_explicit_worker_pool(tokio::runtime::Handle::current());
1695
1696 let guard = blueprint.build_state.lock().expect("topology blueprint mutex poisoned");
1697 let config = &guard
1698 .as_ref()
1699 .expect("topology blueprint already initialized")
1700 .worker_pool_config;
1701 assert!(matches!(config, WorkerPoolConfiguration::Explicit(_)));
1702 }
1703}