1use crate::component::{
5 self, Component, ComponentBuilder, Endpoint, EndpointDiscoverySource, Instance, Namespace,
6 RoutingOccupancyState,
7};
8use crate::config::environment_names::tcp_response_stream;
9use crate::pipeline::PipelineError;
10use crate::pipeline::network::manager::NetworkManager;
11use crate::service::{ServiceClient, ServiceSet};
12use crate::storage::kv;
13use crate::{discovery, system_status_server, transports};
14use crate::{
15 discovery::Discovery,
16 metrics::PrometheusUpdateCallback,
17 metrics::{MetricsHierarchy, MetricsRegistry},
18 transports::{etcd, nats, tcp},
19};
20
21use super::utils::GracefulShutdownTracker;
22use crate::SystemHealth;
23use crate::runtime::Runtime;
24
25use async_once_cell::OnceCell;
27
28use std::fmt;
29use std::sync::{Arc, OnceLock, Weak};
30use std::time::Duration;
31use tokio::sync::watch::Receiver;
32
33use anyhow::Result;
34use derive_getters::Dissolve;
35use figment::error;
36use std::collections::HashMap;
37use tokio::sync::Mutex;
38use tokio_util::sync::CancellationToken;
39
40type EndpointDiscoverySourceMap = HashMap<Endpoint, Weak<EndpointDiscoverySource>>;
41type RoutingOccupancyMap = HashMap<Endpoint, Weak<RoutingOccupancyState>>;
42
43#[derive(Clone)]
46pub struct DistributedRuntime {
47 runtime: Runtime,
49
50 nats_client: Option<transports::nats::Client>,
51 network_manager: Arc<NetworkManager>,
52 tcp_server: Arc<OnceCell<Arc<transports::tcp::server::TcpStreamServer>>>,
53 system_status_server: Arc<OnceLock<Arc<system_status_server::SystemStatusServerInfo>>>,
54 request_plane: RequestPlaneMode,
55
56 discovery_client: Arc<dyn discovery::Discovery>,
58
59 discovery_metadata: Option<Arc<tokio::sync::RwLock<discovery::DiscoveryMetadata>>>,
62
63 component_registry: component::Registry,
69
70 endpoint_discovery_sources: Arc<tokio::sync::Mutex<EndpointDiscoverySourceMap>>,
71 routing_occupancy_states: Arc<tokio::sync::Mutex<RoutingOccupancyMap>>,
72
73 system_health: Arc<parking_lot::Mutex<SystemHealth>>,
75
76 local_endpoint_registry: crate::local_endpoint_registry::LocalEndpointRegistry,
78
79 metrics_registry: MetricsRegistry,
81
82 engine_routes: crate::engine_routes::EngineRouteRegistry,
84
85 metadata_artifacts: crate::metadata_registry::MetadataArtifactRegistry,
87
88 event_transport_kind: crate::discovery::EventTransportKind,
91}
92
93impl MetricsHierarchy for DistributedRuntime {
94 fn basename(&self) -> String {
95 "".to_string() }
97
98 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
99 vec![] }
101
102 fn get_metrics_registry(&self) -> &MetricsRegistry {
103 &self.metrics_registry
104 }
105
106 fn connection_id(&self) -> Option<u64> {
107 Some(self.discovery_client.instance_id())
108 }
109}
110
111impl std::fmt::Debug for DistributedRuntime {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "DistributedRuntime")
114 }
115}
116
117impl DistributedRuntime {
118 pub async fn new(runtime: Runtime, config: DistributedConfig) -> Result<Self> {
119 let (discovery_backend, nats_config, request_plane, event_transport_kind) =
120 config.dissolve();
121
122 let nats_client = match nats_config {
123 Some(nc) => Some(nc.connect().await?),
124 None => None,
125 };
126
127 let config = crate::config::RuntimeConfig::from_settings().unwrap_or_default();
129 let cancel_token = if config.system_server_enabled() {
132 Some(runtime.clone().child_token())
133 } else {
134 None
135 };
136 let starting_health_status = config.starting_health_status.clone();
137 let use_endpoint_health_status = config.use_endpoint_health_status.clone();
138 let health_endpoint_path = config.system_health_path.clone();
139 let live_endpoint_path = config.system_live_path.clone();
140 let system_health = Arc::new(parking_lot::Mutex::new(SystemHealth::new(
141 starting_health_status,
142 use_endpoint_health_status,
143 config.health_check_enabled,
144 health_endpoint_path,
145 live_endpoint_path,
146 )));
147
148 let (discovery_client, discovery_metadata) = match discovery_backend {
150 DiscoveryBackend::Kubernetes => {
151 tracing::info!("Initializing Kubernetes discovery backend");
152 let metadata = Arc::new(tokio::sync::RwLock::new(
153 crate::discovery::DiscoveryMetadata::new(),
154 ));
155 let client = crate::discovery::KubeDiscoveryClient::new(
156 metadata.clone(),
157 runtime.primary_token(),
158 )
159 .await
160 .inspect_err(
161 |err| tracing::error!(%err, "Failed to initialize Kubernetes discovery client"),
162 )?;
163 (Arc::new(client) as Arc<dyn Discovery>, Some(metadata))
164 }
165 DiscoveryBackend::KvStore(kv_selector) => {
166 tracing::info!("Initializing KV store discovery backend: {kv_selector}");
167 let runtime_clone = runtime.clone();
168 let store = match kv_selector {
169 kv::Selector::Etcd(etcd_config) => {
170 let etcd_client = etcd::Client::new(*etcd_config, runtime_clone).await.inspect_err(|err|
171 tracing::error!(%err, "Could not connect to etcd. Pass `--discovery-backend ..` to use a different backend or start etcd."))?;
172 kv::Manager::etcd(etcd_client)
173 }
174 kv::Selector::File(root) => kv::Manager::file(runtime.primary_token(), root),
175 kv::Selector::Memory => kv::Manager::memory(),
176 };
177 use crate::discovery::KVStoreDiscovery;
178 (
179 Arc::new(KVStoreDiscovery::new(store, runtime.primary_token()))
180 as Arc<dyn Discovery>,
181 None,
182 )
183 }
184 };
185
186 let component_registry = component::Registry::new();
187
188 let network_manager = NetworkManager::new(
190 runtime.child_token(),
191 nats_client.clone().map(|c| c.client().clone()),
192 component_registry.clone(),
193 request_plane,
194 );
195
196 let distributed_runtime = Self {
197 runtime,
198 network_manager: Arc::new(network_manager),
199 nats_client,
200 tcp_server: Arc::new(OnceCell::new()),
201 system_status_server: Arc::new(OnceLock::new()),
202 discovery_client,
203 discovery_metadata,
204 component_registry,
205 endpoint_discovery_sources: Arc::new(Mutex::new(HashMap::new())),
206 routing_occupancy_states: Arc::new(Mutex::new(HashMap::new())),
207 metrics_registry: crate::MetricsRegistry::new(),
208 system_health,
209 request_plane,
210 local_endpoint_registry: crate::local_endpoint_registry::LocalEndpointRegistry::new(),
211 engine_routes: crate::engine_routes::EngineRouteRegistry::new(),
212 metadata_artifacts: crate::metadata_registry::MetadataArtifactRegistry::new(),
213 event_transport_kind,
214 };
215
216 distributed_runtime
218 .system_health
219 .lock()
220 .initialize_uptime_gauge(&distributed_runtime)?;
221
222 {
225 let system_health = distributed_runtime.system_health.clone();
226 distributed_runtime
227 .metrics_registry
228 .add_update_callback(std::sync::Arc::new(move || {
229 system_health.lock().update_uptime_gauge();
230 Ok(())
231 }));
232 }
233
234 if let Some(cancel_token) = cancel_token {
236 let host = config.system_host.clone();
238 let port = config.system_port as u16;
239
240 match crate::system_status_server::spawn_system_status_server(
242 &host,
243 port,
244 cancel_token,
245 Arc::new(distributed_runtime.clone()),
246 distributed_runtime.discovery_metadata.clone(),
247 )
248 .await
249 {
250 Ok((addr, handle)) => {
251 tracing::info!("System status server started successfully on {addr}");
252
253 let system_status_server_info =
255 crate::system_status_server::SystemStatusServerInfo::new(
256 addr,
257 Some(handle),
258 );
259
260 distributed_runtime
262 .system_status_server
263 .set(Arc::new(system_status_server_info))
264 .expect("System status server info should only be set once");
265 }
266 Err(e) => {
267 tracing::error!("System status server startup failed: {e}");
268 }
269 }
270 } else {
271 tracing::debug!(
273 "System status server HTTP endpoints disabled, but uptime metrics are being tracked"
274 );
275 }
276
277 if config.health_check_enabled {
279 let health_check_config = crate::health_check::HealthCheckConfig {
280 canary_wait_time: std::time::Duration::from_secs(config.canary_wait_time_secs),
281 request_timeout: std::time::Duration::from_secs(
282 config.health_check_request_timeout_secs,
283 ),
284 };
285
286 match crate::health_check::start_health_check_manager(
288 distributed_runtime.clone(),
289 Some(health_check_config),
290 )
291 .await
292 {
293 Ok(()) => tracing::info!(
294 "Health check manager started (canary_wait_time: {}s, request_timeout: {}s)",
295 config.canary_wait_time_secs,
296 config.health_check_request_timeout_secs
297 ),
298 Err(e) => tracing::error!("Health check manager failed to start: {e}"),
299 }
300 }
301
302 Ok(distributed_runtime)
303 }
304
305 pub async fn from_settings(runtime: Runtime) -> Result<Self> {
306 let config = DistributedConfig::from_settings();
307 Self::new(runtime, config).await
308 }
309
310 pub fn runtime(&self) -> &Runtime {
311 &self.runtime
312 }
313
314 pub fn primary_token(&self) -> CancellationToken {
315 self.runtime.primary_token()
316 }
317
318 pub fn component_registry(&self) -> &component::Registry {
321 &self.component_registry
322 }
323
324 pub fn system_health(&self) -> Arc<parking_lot::Mutex<SystemHealth>> {
326 self.system_health.clone()
327 }
328
329 pub fn local_endpoint_registry(
331 &self,
332 ) -> &crate::local_endpoint_registry::LocalEndpointRegistry {
333 &self.local_endpoint_registry
334 }
335
336 pub fn engine_routes(&self) -> &crate::engine_routes::EngineRouteRegistry {
338 &self.engine_routes
339 }
340
341 pub fn metadata_artifacts(&self) -> &crate::metadata_registry::MetadataArtifactRegistry {
342 &self.metadata_artifacts
343 }
344
345 pub fn connection_id(&self) -> u64 {
346 self.discovery_client.instance_id()
347 }
348
349 pub fn shutdown(&self) {
350 self.runtime.shutdown();
351 self.discovery_client.shutdown();
352 }
353
354 pub fn namespace(&self, name: impl Into<String>) -> Result<Namespace> {
356 Namespace::new(self.clone(), name.into())
357 }
358
359 pub fn discovery(&self) -> Arc<dyn Discovery> {
361 self.discovery_client.clone()
362 }
363
364 pub async fn tcp_server(&self) -> Result<Arc<tcp::server::TcpStreamServer>> {
365 Ok(self
366 .tcp_server
367 .get_or_try_init(async move {
368 let port = match std::env::var(tcp_response_stream::DYN_TCP_RESPONSE_STREAM_PORT) {
369 Ok(p) => p.parse::<u16>().map_err(|_| {
370 PipelineError::Generic(format!(
371 "invalid {}: '{}' is not a valid port number",
372 tcp_response_stream::DYN_TCP_RESPONSE_STREAM_PORT,
373 p
374 ))
375 })?,
376 Err(_) => 0,
377 };
378 let interface = std::env::var(tcp_response_stream::DYN_TCP_RESPONSE_STREAM_HOST)
379 .ok()
380 .filter(|h| !h.is_empty());
381
382 let host_suffix = interface
383 .as_ref()
384 .map_or(String::new(), |h| format!(" on host {h}"));
385 if port == 0 {
386 tracing::info!(
387 "TCP response stream server using OS-assigned port{host_suffix}"
388 );
389 } else {
390 tracing::info!(
391 "TCP response stream server using fixed port {port}{host_suffix}"
392 );
393 }
394
395 let options = tcp::server::ServerOptions { port, interface };
396 let server = tcp::server::TcpStreamServer::new(options).await?;
397 Ok::<_, PipelineError>(server)
398 })
399 .await?
400 .clone())
401 }
402
403 pub fn network_manager(&self) -> Arc<NetworkManager> {
408 self.network_manager.clone()
409 }
410
411 pub async fn request_plane_server(
415 &self,
416 ) -> Result<Arc<dyn crate::pipeline::network::ingress::unified_server::RequestPlaneServer>>
417 {
418 self.network_manager().server().await
419 }
420
421 pub fn system_status_server_info(
423 &self,
424 ) -> Option<Arc<crate::system_status_server::SystemStatusServerInfo>> {
425 self.system_status_server.get().cloned()
426 }
427
428 pub fn request_plane(&self) -> RequestPlaneMode {
430 self.request_plane
431 }
432
433 pub fn default_event_transport_kind(&self) -> crate::discovery::EventTransportKind {
441 self.event_transport_kind
442 }
443
444 pub fn child_token(&self) -> CancellationToken {
445 self.runtime.child_token()
446 }
447
448 pub(crate) fn graceful_shutdown_tracker(&self) -> Arc<GracefulShutdownTracker> {
449 self.runtime.graceful_shutdown_tracker()
450 }
451
452 pub(crate) fn endpoint_discovery_sources(&self) -> Arc<Mutex<EndpointDiscoverySourceMap>> {
453 self.endpoint_discovery_sources.clone()
454 }
455
456 pub fn register_graceful_task(&self) -> crate::utils::GracefulTaskGuard {
462 self.runtime.graceful_shutdown_tracker().register_task()
463 }
464
465 pub(crate) fn routing_occupancy_states(&self) -> Arc<Mutex<RoutingOccupancyMap>> {
466 self.routing_occupancy_states.clone()
467 }
468
469 pub async fn kv_router_nats_publish(
475 &self,
476 subject: String,
477 payload: bytes::Bytes,
478 ) -> anyhow::Result<()> {
479 self.kv_router_nats_publish_subject(subject.into(), payload)
480 .await
481 }
482
483 pub(crate) async fn kv_router_nats_publish_subject(
484 &self,
485 subject: async_nats::Subject,
486 payload: bytes::Bytes,
487 ) -> anyhow::Result<()> {
488 let Some(nats_client) = self.nats_client.as_ref() else {
489 tracing::trace!("Skipping NATS publish (NATS not configured): {subject}");
491 return Ok(());
492 };
493 Ok(nats_client.client().publish(subject, payload).await?)
494 }
495
496 pub(crate) async fn kv_router_nats_subscribe(
499 &self,
500 subject: String,
501 ) -> Result<async_nats::Subscriber> {
502 let Some(nats_client) = self.nats_client.as_ref() else {
503 anyhow::bail!("KV router's EventSubscriber requires NATS");
504 };
505 Ok(nats_client.client().subscribe(subject).await?)
506 }
507
508 pub async fn kv_router_nats_request(
512 &self,
513 subject: String,
514 payload: bytes::Bytes,
515 timeout: std::time::Duration,
516 ) -> anyhow::Result<async_nats::Message> {
517 let Some(nats_client) = self.nats_client.as_ref() else {
518 anyhow::bail!("KV router's request requires NATS");
519 };
520 let response =
521 tokio::time::timeout(timeout, nats_client.client().request(subject, payload))
522 .await
523 .map_err(|_| anyhow::anyhow!("Request timed out after {:?}", timeout))??;
524 Ok(response)
525 }
526
527 pub fn register_nats_service(
534 &self,
535 component: Component,
536 ) -> tokio::sync::mpsc::Receiver<Result<(), String>> {
537 let (tx, rx) = tokio::sync::mpsc::channel::<Result<(), String>>(1);
539
540 let drt = self.clone();
541 self.runtime().secondary().spawn(async move {
542 let service_name = component.service_name();
543
544 if drt
546 .component_registry()
547 .inner
548 .lock()
549 .await
550 .services
551 .contains_key(&service_name)
552 {
553 tracing::trace!("Service {service_name} already exists");
556 let _ = tx.send(Ok(())).await;
558 return;
559 }
560
561 let Some(nats_client) = drt.nats_client.as_ref() else {
562 tracing::error!("Cannot create NATS service without NATS.");
563 let _ = tx
564 .send(Err("Cannot create NATS service without NATS".to_string()))
565 .await;
566 return;
567 };
568 let description = None;
569 let nats_service = match crate::component::service::build_nats_service(
570 nats_client,
571 &component,
572 description,
573 )
574 .await
575 {
576 Ok(service) => service,
577 Err(err) => {
578 tracing::error!(error = %err, component = service_name, "Failed to build NATS service");
579 let _ = tx.send(Err(format!("Failed to build NATS service: {err}"))).await;
580 return;
581 }
582 };
583
584 let mut guard = drt.component_registry().inner.lock().await;
585 if !guard.services.contains_key(&service_name) {
586 guard.services.insert(service_name.clone(), nats_service);
588
589 tracing::info!("Added NATS service {service_name}");
590
591 drop(guard);
592 } else {
593 drop(guard);
594 let _ = nats_service.stop().await;
595 }
599
600 let _ = tx.send(Ok(())).await;
602 });
603
604 rx
605 }
606}
607
608#[derive(Clone, Debug)]
610pub enum DiscoveryBackend {
611 Kubernetes,
613 KvStore(kv::Selector),
615}
616
617impl DiscoveryBackend {
618 pub fn is_local(&self) -> bool {
622 matches!(
623 self,
624 DiscoveryBackend::KvStore(kv::Selector::File(_))
625 | DiscoveryBackend::KvStore(kv::Selector::Memory)
626 )
627 }
628
629 pub fn resolve_event_transport_kind(&self) -> crate::discovery::EventTransportKind {
638 use crate::config::environment_names::event_plane::DYN_EVENT_PLANE;
639 use crate::discovery::EventTransportKind;
640 match std::env::var(DYN_EVENT_PLANE).as_deref() {
641 Ok("nats") => EventTransportKind::Nats,
642 Ok("zmq") => EventTransportKind::Zmq,
643 Ok("") | Err(_) => EventTransportKind::Zmq,
645 Ok(other) => {
646 tracing::warn!(
647 "Invalid DYN_EVENT_PLANE value '{}'. Valid values: 'nats', 'zmq'. \
648 Defaulting to ZMQ.",
649 other
650 );
651 EventTransportKind::Zmq
652 }
653 }
654 }
655}
656
657#[derive(Dissolve)]
658pub struct DistributedConfig {
659 pub discovery_backend: DiscoveryBackend,
660 pub nats_config: Option<nats::ClientOptions>,
661 pub request_plane: RequestPlaneMode,
662 pub event_transport_kind: crate::discovery::EventTransportKind,
667}
668
669impl DistributedConfig {
670 pub fn from_settings() -> DistributedConfig {
671 let request_plane = RequestPlaneMode::from_env();
672
673 let backend_str =
676 std::env::var("DYN_DISCOVERY_BACKEND").unwrap_or_else(|_| "etcd".to_string());
677
678 let discovery_backend = match backend_str.as_str() {
679 "kubernetes" => {
680 tracing::info!("Using Kubernetes discovery backend");
681 DiscoveryBackend::Kubernetes
682 }
683 other => {
684 let selector: kv::Selector = other.parse().unwrap_or_else(|_| {
685 panic!(
686 "Unknown DYN_DISCOVERY_BACKEND value: '{other}'. \
687 Valid options: kubernetes, etcd, file, mem"
688 )
689 });
690 DiscoveryBackend::KvStore(selector)
691 }
692 };
693
694 let event_transport_kind = discovery_backend.resolve_event_transport_kind();
698
699 let nats_enabled = request_plane.is_nats()
708 || std::env::var(crate::config::environment_names::nats::NATS_SERVER).is_ok()
709 || matches!(
710 event_transport_kind,
711 crate::discovery::EventTransportKind::Nats
712 );
713
714 DistributedConfig {
715 discovery_backend,
716 nats_config: if nats_enabled {
717 Some(nats::ClientOptions::default())
718 } else {
719 None
720 },
721 request_plane,
722 event_transport_kind,
723 }
724 }
725
726 pub fn for_cli() -> DistributedConfig {
727 let etcd_config = etcd::ClientOptions {
728 attach_lease: false,
729 ..Default::default()
730 };
731 let request_plane = RequestPlaneMode::from_env();
732 let discovery_backend =
733 DiscoveryBackend::KvStore(kv::Selector::Etcd(Box::new(etcd_config)));
734 let event_transport_kind = discovery_backend.resolve_event_transport_kind();
735 let nats_enabled = request_plane.is_nats()
736 || std::env::var(crate::config::environment_names::nats::NATS_SERVER).is_ok()
737 || matches!(
738 event_transport_kind,
739 crate::discovery::EventTransportKind::Nats
740 );
741 DistributedConfig {
742 discovery_backend,
743 nats_config: if nats_enabled {
744 Some(nats::ClientOptions::default())
745 } else {
746 None
747 },
748 request_plane,
749 event_transport_kind,
750 }
751 }
752
753 pub fn process_local() -> DistributedConfig {
756 DistributedConfig {
757 discovery_backend: DiscoveryBackend::KvStore(kv::Selector::Memory),
758 nats_config: None,
759 request_plane: RequestPlaneMode::Tcp,
762 event_transport_kind: crate::discovery::EventTransportKind::Zmq,
763 }
764 }
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
773pub enum RequestPlaneMode {
774 Nats,
776 #[default]
778 Tcp,
779}
780
781impl fmt::Display for RequestPlaneMode {
782 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
783 match self {
784 Self::Nats => write!(f, "nats"),
785 Self::Tcp => write!(f, "tcp"),
786 }
787 }
788}
789
790impl std::str::FromStr for RequestPlaneMode {
791 type Err = anyhow::Error;
792
793 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
794 match s.to_lowercase().as_str() {
795 "nats" => Ok(Self::Nats),
796 "tcp" => Ok(Self::Tcp),
797 _ => Err(anyhow::anyhow!(
798 "Invalid request plane mode: '{}'. Valid options are: 'nats', 'tcp'",
799 s
800 )),
801 }
802 }
803}
804
805impl RequestPlaneMode {
806 fn from_env() -> Self {
809 std::env::var("DYN_REQUEST_PLANE")
810 .ok()
811 .and_then(|s| s.parse().ok())
812 .unwrap_or_default()
813 }
814
815 pub fn is_nats(&self) -> bool {
816 matches!(self, RequestPlaneMode::Nats)
817 }
818}
819
820pub mod distributed_test_utils {
821 #[cfg(feature = "integration")]
827 pub async fn create_test_drt_async() -> super::DistributedRuntime {
828 use crate::transports::nats;
829
830 let rt = crate::Runtime::from_current().unwrap();
831 let config = super::DistributedConfig {
832 discovery_backend: super::DiscoveryBackend::KvStore(
833 crate::storage::kv::Selector::Memory,
834 ),
835 nats_config: Some(nats::ClientOptions::default()),
836 request_plane: crate::distributed::RequestPlaneMode::default(),
837 event_transport_kind: crate::discovery::EventTransportKind::Nats,
838 };
839 super::DistributedRuntime::new(rt, config).await.unwrap()
840 }
841
842 pub async fn create_test_shared_drt_async(
849 store_path: &std::path::Path,
850 ) -> super::DistributedRuntime {
851 use crate::transports::nats;
852
853 let rt = crate::Runtime::from_current().unwrap();
854 let config = super::DistributedConfig {
855 discovery_backend: super::DiscoveryBackend::KvStore(
856 crate::storage::kv::Selector::File(store_path.to_path_buf()),
857 ),
858 nats_config: Some(nats::ClientOptions::default()),
859 request_plane: crate::distributed::RequestPlaneMode::default(),
860 event_transport_kind: crate::discovery::EventTransportKind::Nats,
861 };
862 super::DistributedRuntime::new(rt, config).await.unwrap()
863 }
864}
865
866#[cfg(all(test, feature = "integration"))]
867mod tests {
868 use super::RequestPlaneMode;
869 use super::distributed_test_utils::create_test_drt_async;
870
871 #[tokio::test]
872 async fn test_drt_uptime_after_delay_system_disabled() {
873 use crate::config::environment_names::runtime::system as env_system;
874 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
876 let drt = create_test_drt_async().await;
878
879 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
881
882 let uptime = drt.system_health.lock().uptime();
884 assert!(
885 uptime >= std::time::Duration::from_millis(50),
886 "Expected uptime to be at least 50ms, but got {:?}",
887 uptime
888 );
889
890 println!(
891 "✓ DRT uptime test passed (system disabled): uptime = {:?}",
892 uptime
893 );
894 })
895 .await;
896 }
897
898 #[tokio::test]
899 async fn test_drt_uptime_after_delay_system_enabled() {
900 use crate::config::environment_names::runtime::system as env_system;
901 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, Some("8081"))], async {
903 let drt = create_test_drt_async().await;
905
906 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
908
909 let uptime = drt.system_health.lock().uptime();
911 assert!(
912 uptime >= std::time::Duration::from_millis(50),
913 "Expected uptime to be at least 50ms, but got {:?}",
914 uptime
915 );
916
917 println!(
918 "✓ DRT uptime test passed (system enabled): uptime = {:?}",
919 uptime
920 );
921 })
922 .await;
923 }
924
925 #[test]
926 fn test_request_plane_mode_from_str() {
927 assert_eq!(
928 "nats".parse::<RequestPlaneMode>().unwrap(),
929 RequestPlaneMode::Nats
930 );
931 assert_eq!(
932 "tcp".parse::<RequestPlaneMode>().unwrap(),
933 RequestPlaneMode::Tcp
934 );
935 assert_eq!(
936 "NATS".parse::<RequestPlaneMode>().unwrap(),
937 RequestPlaneMode::Nats
938 );
939 assert_eq!(
940 "TCP".parse::<RequestPlaneMode>().unwrap(),
941 RequestPlaneMode::Tcp
942 );
943 assert!("invalid".parse::<RequestPlaneMode>().is_err());
944 }
945}