1use std::fmt;
33
34use crate::{
35 config::HealthStatus,
36 distributed::RequestPlaneMode,
37 metrics::{MetricsHierarchy, MetricsRegistry, prometheus_names},
38 service::ServiceClient,
39 service::ServiceSet,
40};
41
42use super::{DistributedRuntime, Runtime, traits::*, transports::nats::Slug, utils::Duration};
43
44use crate::pipeline::network::{
45 PushWorkHandler, RequestPlanePayloadCodec, ingress::push_endpoint::PushEndpoint,
46};
47use crate::protocols::EndpointId;
48use async_nats::{
49 rustls::quic,
50 service::{Service, ServiceExt},
51};
52use dashmap::DashMap;
53use derive_builder::Builder;
54use derive_getters::Getters;
55use educe::Educe;
56use serde::{Deserialize, Serialize};
57use std::{
58 collections::HashMap,
59 hash::Hash,
60 sync::{Arc, OnceLock},
61};
62use validator::{Validate, ValidationError};
63
64mod client;
65#[allow(clippy::module_inception)]
66mod component;
67mod endpoint;
68mod namespace;
69mod registry;
70pub mod service;
71
72pub(crate) use client::EndpointDiscoverySource;
73pub(crate) use client::RoutingInstances;
74pub use client::{Client, RoutingInstanceCounts};
75pub use endpoint::{StartedEndpoint, build_transport_type};
76
77#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
78#[serde(rename_all = "snake_case")]
79pub enum TransportType {
80 #[serde(rename = "nats_tcp")]
81 Nats(String),
82 Tcp(String),
83}
84
85impl TransportType {
86 pub fn address(&self) -> &str {
87 match self {
88 TransportType::Nats(address) | TransportType::Tcp(address) => address,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
94#[serde(rename_all = "snake_case")]
95pub enum DeviceType {
96 Cpu,
97 Cuda,
98}
99
100#[derive(Default)]
101pub struct RegistryInner {
102 pub(crate) services: HashMap<String, Service>,
103}
104
105#[derive(Clone)]
106pub struct Registry {
107 pub(crate) inner: Arc<tokio::sync::Mutex<RegistryInner>>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111pub struct Instance {
112 pub component: String,
113 pub endpoint: String,
114 pub namespace: String,
115 pub instance_id: u64,
116 pub transport: TransportType,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub device_type: Option<DeviceType>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub request_plane_codec: Option<RequestPlanePayloadCodec>,
123}
124
125impl Instance {
126 pub fn id(&self) -> u64 {
127 self.instance_id
128 }
129
130 pub fn endpoint_id(&self) -> EndpointId {
131 EndpointId {
132 namespace: self.namespace.clone(),
133 component: self.component.clone(),
134 name: self.endpoint.clone(),
135 }
136 }
137
138 pub fn endpoint_instance_id(&self) -> crate::discovery::EndpointInstanceId {
139 crate::discovery::EndpointInstanceId {
140 namespace: self.namespace.clone(),
141 component: self.component.clone(),
142 endpoint: self.endpoint.clone(),
143 instance_id: self.instance_id,
144 }
145 }
146}
147
148impl fmt::Display for Instance {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(
151 f,
152 "{}/{}/{}/{}",
153 self.namespace, self.component, self.endpoint, self.instance_id
154 )
155 }
156}
157
158impl std::cmp::Ord for Instance {
160 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
161 self.to_string().cmp(&other.to_string())
162 }
163}
164
165impl PartialOrd for Instance {
166 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
167 Some(self.cmp(other))
169 }
170}
171
172#[derive(Educe, Builder, Clone, Validate)]
178#[educe(Debug)]
179#[builder(pattern = "owned", build_fn(private, name = "build_internal"))]
180pub struct Component {
181 #[builder(private)]
182 #[educe(Debug(ignore))]
183 drt: Arc<DistributedRuntime>,
184
185 #[builder(setter(into))]
187 #[validate(custom(function = "validate_allowed_chars"))]
188 name: String,
189
190 #[builder(default = "Vec::new()")]
192 labels: Vec<(String, String)>,
193
194 #[builder(setter(into))]
197 namespace: Namespace,
198
199 #[builder(default = "crate::MetricsRegistry::new()")]
201 metrics_registry: crate::MetricsRegistry,
202}
203
204impl Hash for Component {
205 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
206 self.namespace.name().hash(state);
207 self.name.hash(state);
208 }
209}
210
211impl PartialEq for Component {
212 fn eq(&self, other: &Self) -> bool {
213 self.namespace.name() == other.namespace.name() && self.name == other.name
214 }
215}
216
217impl Eq for Component {}
218
219impl std::fmt::Display for Component {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 write!(f, "{}.{}", self.namespace.name(), self.name)
222 }
223}
224
225impl DistributedRuntimeProvider for Component {
226 fn drt(&self) -> &DistributedRuntime {
227 &self.drt
228 }
229}
230
231impl RuntimeProvider for Component {
232 fn rt(&self) -> &Runtime {
233 self.drt.rt()
234 }
235}
236
237impl MetricsHierarchy for Component {
238 fn basename(&self) -> String {
239 self.name.clone()
240 }
241
242 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
243 let mut parents = vec![];
244
245 parents.extend(self.namespace.parent_hierarchies());
247
248 parents.push(&self.namespace as &dyn MetricsHierarchy);
250
251 parents
252 }
253
254 fn get_metrics_registry(&self) -> &MetricsRegistry {
255 &self.metrics_registry
256 }
257
258 fn connection_id(&self) -> Option<u64> {
259 Some(self.drt.connection_id())
260 }
261}
262
263impl Component {
264 pub fn service_name(&self) -> String {
265 let service_name = format!("{}_{}", self.namespace.name(), self.name);
266 Slug::slugify(&service_name).to_string()
267 }
268
269 pub fn namespace(&self) -> &Namespace {
270 &self.namespace
271 }
272
273 pub fn name(&self) -> &str {
274 &self.name
275 }
276
277 pub fn labels(&self) -> &[(String, String)] {
278 &self.labels
279 }
280
281 pub fn endpoint(&self, endpoint: impl Into<String>) -> Endpoint {
282 let endpoint = Endpoint {
283 component: self.clone(),
284 name: endpoint.into(),
285 labels: Vec::new(),
286 metrics_registry: crate::MetricsRegistry::new(),
287 lifecycle_operation_role: Arc::new(OnceLock::new()),
288 };
289 self.get_metrics_registry()
291 .add_child_registry(endpoint.get_metrics_registry());
292 endpoint
293 }
294
295 pub async fn list_instances(&self) -> anyhow::Result<Vec<Instance>> {
296 let discovery = self.drt.discovery();
297
298 let discovery_query = crate::discovery::DiscoveryQuery::ComponentEndpoints {
299 namespace: self.namespace.name(),
300 component: self.name.clone(),
301 };
302
303 let discovery_instances = discovery.list(discovery_query).await?;
304
305 let mut instances: Vec<Instance> = discovery_instances
307 .into_iter()
308 .filter_map(|di| match di {
309 crate::discovery::DiscoveryInstance::Endpoint(instance) => Some(instance),
310 _ => None, })
312 .collect();
313
314 instances.sort();
315 Ok(instances)
316 }
317}
318
319impl ComponentBuilder {
320 pub fn from_runtime(drt: Arc<DistributedRuntime>) -> Self {
321 Self::default().drt(drt)
322 }
323
324 pub fn build(self) -> Result<Component, anyhow::Error> {
325 let component = self.build_internal()?;
326 let drt = component.drt();
330 if drt.request_plane().is_nats() {
331 let mut rx = drt.register_nats_service(component.clone());
332 let result = tokio::task::block_in_place(|| rx.blocking_recv());
337 match result {
338 Some(Ok(())) => {
339 tracing::debug!(
340 component = component.service_name(),
341 "NATS service registration completed"
342 );
343 }
344 Some(Err(e)) => {
345 return Err(anyhow::anyhow!(
346 "NATS service registration failed for component '{}': {}",
347 component.service_name(),
348 e
349 ));
350 }
351 None => {
352 return Err(anyhow::anyhow!(
353 "NATS service registration channel closed unexpectedly for component '{}'",
354 component.service_name()
355 ));
356 }
357 }
358 }
359 Ok(component)
360 }
361}
362
363#[derive(Debug, Clone)]
364pub struct Endpoint {
365 component: Component,
366
367 name: String,
370
371 labels: Vec<(String, String)>,
373
374 metrics_registry: crate::MetricsRegistry,
376
377 lifecycle_operation_role: Arc<OnceLock<crate::telemetry::LifecycleOperationRole>>,
379}
380
381impl Hash for Endpoint {
382 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
383 self.component.hash(state);
384 self.name.hash(state);
385 }
386}
387
388impl PartialEq for Endpoint {
389 fn eq(&self, other: &Self) -> bool {
390 self.component == other.component && self.name == other.name
391 }
392}
393
394impl Eq for Endpoint {}
395
396impl DistributedRuntimeProvider for Endpoint {
397 fn drt(&self) -> &DistributedRuntime {
398 self.component.drt()
399 }
400}
401
402impl RuntimeProvider for Endpoint {
403 fn rt(&self) -> &Runtime {
404 self.component.rt()
405 }
406}
407
408impl MetricsHierarchy for Endpoint {
409 fn basename(&self) -> String {
410 self.name.clone()
411 }
412
413 fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
414 let mut parents = vec![];
415
416 parents.extend(self.component.parent_hierarchies());
418
419 parents.push(&self.component as &dyn MetricsHierarchy);
421
422 parents
423 }
424
425 fn get_metrics_registry(&self) -> &MetricsRegistry {
426 &self.metrics_registry
427 }
428
429 fn connection_id(&self) -> Option<u64> {
430 Some(self.component.drt().connection_id())
431 }
432}
433
434impl Endpoint {
435 pub fn id(&self) -> EndpointId {
436 EndpointId {
437 namespace: self.component.namespace().name().to_string(),
438 component: self.component.name().to_string(),
439 name: self.name().to_string(),
440 }
441 }
442
443 pub fn name(&self) -> &str {
444 &self.name
445 }
446
447 pub fn component(&self) -> &Component {
448 &self.component
449 }
450
451 pub fn set_lifecycle_operation_role(
453 &self,
454 role: crate::telemetry::LifecycleOperationRole,
455 ) -> anyhow::Result<()> {
456 let existing = *self.lifecycle_operation_role.get_or_init(|| role);
457 anyhow::ensure!(
458 existing == role,
459 "endpoint {} lifecycle role is already {existing:?}, cannot set it to {role:?}",
460 self.id()
461 );
462 Ok(())
463 }
464
465 pub(crate) fn lifecycle_operation_role(
466 &self,
467 ) -> Arc<OnceLock<crate::telemetry::LifecycleOperationRole>> {
468 self.lifecycle_operation_role.clone()
469 }
470
471 pub async fn client(&self) -> anyhow::Result<client::Client> {
472 client::Client::new(self.clone()).await
473 }
474
475 pub async fn client_with_cancellation(
483 &self,
484 cancel_token: tokio_util::sync::CancellationToken,
485 ) -> anyhow::Result<client::Client> {
486 client::Client::with_cancellation(self.clone(), cancel_token).await
487 }
488
489 pub fn endpoint_builder(&self) -> endpoint::EndpointConfigBuilder {
490 endpoint::EndpointConfigBuilder::from_endpoint(self.clone())
491 }
492}
493
494#[derive(Builder, Clone, Validate)]
495#[builder(pattern = "owned")]
496pub struct Namespace {
497 #[builder(private)]
498 runtime: Arc<DistributedRuntime>,
499
500 #[validate(custom(function = "validate_allowed_chars"))]
501 name: String,
502
503 #[builder(default = "None")]
504 parent: Option<Arc<Namespace>>,
505
506 #[builder(default = "Vec::new()")]
508 labels: Vec<(String, String)>,
509
510 #[builder(default = "crate::MetricsRegistry::new()")]
512 metrics_registry: crate::MetricsRegistry,
513
514 #[builder(default = "Arc::new(DashMap::new())")]
519 component_cache: Arc<DashMap<String, Component>>,
520}
521
522impl DistributedRuntimeProvider for Namespace {
523 fn drt(&self) -> &DistributedRuntime {
524 &self.runtime
525 }
526}
527
528impl std::fmt::Debug for Namespace {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 write!(
531 f,
532 "Namespace {{ name: {}; parent: {:?} }}",
533 self.name, self.parent
534 )
535 }
536}
537
538impl RuntimeProvider for Namespace {
539 fn rt(&self) -> &Runtime {
540 self.runtime.rt()
541 }
542}
543
544impl std::fmt::Display for Namespace {
545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546 write!(f, "{}", self.name)
547 }
548}
549
550impl Namespace {
551 pub(crate) fn new(runtime: DistributedRuntime, name: String) -> anyhow::Result<Self> {
552 let ns = NamespaceBuilder::default()
553 .runtime(Arc::new(runtime))
554 .name(name)
555 .build()?;
556 ns.drt()
558 .get_metrics_registry()
559 .add_child_registry(ns.get_metrics_registry());
560 Ok(ns)
561 }
562
563 pub fn component(&self, name: impl Into<String>) -> anyhow::Result<Component> {
569 let name = name.into();
570
571 if let Some(cached) = self.component_cache.get(&name) {
574 return Ok(cached.value().clone());
575 }
576
577 let component = ComponentBuilder::from_runtime(self.runtime.clone())
579 .name(&name)
580 .namespace(self.clone())
581 .build()?;
582
583 self.get_metrics_registry()
585 .add_child_registry(component.get_metrics_registry());
586
587 self.component_cache.insert(name, component.clone());
591
592 Ok(component)
593 }
594
595 pub fn namespace(&self, name: impl Into<String>) -> anyhow::Result<Namespace> {
597 let child = NamespaceBuilder::default()
598 .runtime(self.runtime.clone())
599 .name(name.into())
600 .parent(Some(Arc::new(self.clone())))
601 .build()?;
602 self.get_metrics_registry()
604 .add_child_registry(child.get_metrics_registry());
605 Ok(child)
606 }
607
608 pub fn name(&self) -> String {
609 match &self.parent {
610 Some(parent) => format!("{}.{}", parent.name(), self.name),
611 None => self.name.clone(),
612 }
613 }
614}
615
616fn validate_allowed_chars(input: &str) -> Result<(), ValidationError> {
618 let regex = regex::Regex::new(r"^[a-z0-9-_]+$").unwrap();
620
621 if regex.is_match(input) {
622 Ok(())
623 } else {
624 Err(ValidationError::new("invalid_characters"))
625 }
626}