Skip to main content

dynamo_runtime/
component.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The [Component] module defines the top-level API for building distributed applications.
5//!
6//! A distributed application consists of a set of [Component] that can host one
7//! or more [Endpoint]. Each [Endpoint] is a network-accessible service
8//! that can be accessed by other [Component] in the distributed application.
9//!
10//! A [Component] is made discoverable by registering it with the distributed runtime under
11//! a [`Namespace`].
12//!
13//! A [`Namespace`] is a logical grouping of [Component] that are grouped together.
14//!
15//! We might extend namespace to include grouping behavior, which would define groups of
16//! components that are tightly coupled.
17//!
18//! A [Component] is the core building block of a distributed application. It is a logical
19//! unit of work such as a `Preprocessor` or `SmartRouter` that has a well-defined role in the
20//! distributed application.
21//!
22//! A [Component] can present to the distributed application one or more configuration files
23//! which define how that component was constructed/configured and what capabilities it can
24//! provide.
25//!
26//! Other [Component] can write to watching locations within a [Component] etcd
27//! path. This allows the [Component] to take dynamic actions depending on the watch
28//! triggers.
29//!
30//! TODO: Top-level Overview of Endpoints/Functions
31
32use 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::{collections::HashMap, hash::Hash, sync::Arc};
58use validator::{Validate, ValidationError};
59
60mod client;
61#[allow(clippy::module_inception)]
62mod component;
63mod endpoint;
64mod namespace;
65mod registry;
66pub mod service;
67
68pub(crate) use client::EndpointDiscoverySource;
69pub(crate) use client::RoutingInstances;
70pub(crate) use client::RoutingOccupancyState;
71pub(crate) use client::get_or_create_routing_occupancy_state;
72pub use client::{Client, RoutingInstanceCounts};
73pub use endpoint::{StartedEndpoint, build_transport_type};
74
75#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
76#[serde(rename_all = "snake_case")]
77pub enum TransportType {
78    #[serde(rename = "nats_tcp")]
79    Nats(String),
80    Tcp(String),
81}
82
83impl TransportType {
84    pub fn address(&self) -> &str {
85        match self {
86            TransportType::Nats(address) | TransportType::Tcp(address) => address,
87        }
88    }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
92#[serde(rename_all = "snake_case")]
93pub enum DeviceType {
94    Cpu,
95    Cuda,
96}
97
98#[derive(Default)]
99pub struct RegistryInner {
100    pub(crate) services: HashMap<String, Service>,
101}
102
103#[derive(Clone)]
104pub struct Registry {
105    pub(crate) inner: Arc<tokio::sync::Mutex<RegistryInner>>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109pub struct Instance {
110    pub component: String,
111    pub endpoint: String,
112    pub namespace: String,
113    pub instance_id: u64,
114    pub transport: TransportType,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub device_type: Option<DeviceType>,
117    /// Payload codec accepted by this worker's request-plane endpoint.
118    /// Missing metadata identifies a legacy JSON-only worker.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub request_plane_codec: Option<RequestPlanePayloadCodec>,
121}
122
123impl Instance {
124    pub fn id(&self) -> u64 {
125        self.instance_id
126    }
127
128    pub fn endpoint_id(&self) -> EndpointId {
129        EndpointId {
130            namespace: self.namespace.clone(),
131            component: self.component.clone(),
132            name: self.endpoint.clone(),
133        }
134    }
135
136    pub fn endpoint_instance_id(&self) -> crate::discovery::EndpointInstanceId {
137        crate::discovery::EndpointInstanceId {
138            namespace: self.namespace.clone(),
139            component: self.component.clone(),
140            endpoint: self.endpoint.clone(),
141            instance_id: self.instance_id,
142        }
143    }
144}
145
146impl fmt::Display for Instance {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        write!(
149            f,
150            "{}/{}/{}/{}",
151            self.namespace, self.component, self.endpoint, self.instance_id
152        )
153    }
154}
155
156/// Sort by string name
157impl std::cmp::Ord for Instance {
158    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
159        self.to_string().cmp(&other.to_string())
160    }
161}
162
163impl PartialOrd for Instance {
164    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
165        // Since Ord is fully implemented, the comparison is always total.
166        Some(self.cmp(other))
167    }
168}
169
170/// A [Component] a discoverable entity in the distributed runtime.
171/// You can host [Endpoint] on a [Component] by first creating
172/// a [Service] then adding one or more [Endpoint] to the [Service].
173///
174/// You can also issue a request to a [Component]'s [Endpoint] by creating a [Client].
175#[derive(Educe, Builder, Clone, Validate)]
176#[educe(Debug)]
177#[builder(pattern = "owned", build_fn(private, name = "build_internal"))]
178pub struct Component {
179    #[builder(private)]
180    #[educe(Debug(ignore))]
181    drt: Arc<DistributedRuntime>,
182
183    /// Name of the component
184    #[builder(setter(into))]
185    #[validate(custom(function = "validate_allowed_chars"))]
186    name: String,
187
188    /// Additional labels for metrics
189    #[builder(default = "Vec::new()")]
190    labels: Vec<(String, String)>,
191
192    // todo - restrict the namespace to a-z0-9-_A-Z
193    /// Namespace
194    #[builder(setter(into))]
195    namespace: Namespace,
196
197    /// This hierarchy's own metrics registry
198    #[builder(default = "crate::MetricsRegistry::new()")]
199    metrics_registry: crate::MetricsRegistry,
200}
201
202impl Hash for Component {
203    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
204        self.namespace.name().hash(state);
205        self.name.hash(state);
206    }
207}
208
209impl PartialEq for Component {
210    fn eq(&self, other: &Self) -> bool {
211        self.namespace.name() == other.namespace.name() && self.name == other.name
212    }
213}
214
215impl Eq for Component {}
216
217impl std::fmt::Display for Component {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        write!(f, "{}.{}", self.namespace.name(), self.name)
220    }
221}
222
223impl DistributedRuntimeProvider for Component {
224    fn drt(&self) -> &DistributedRuntime {
225        &self.drt
226    }
227}
228
229impl RuntimeProvider for Component {
230    fn rt(&self) -> &Runtime {
231        self.drt.rt()
232    }
233}
234
235impl MetricsHierarchy for Component {
236    fn basename(&self) -> String {
237        self.name.clone()
238    }
239
240    fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
241        let mut parents = vec![];
242
243        // Get all ancestors of namespace (DRT, parent namespaces, etc.)
244        parents.extend(self.namespace.parent_hierarchies());
245
246        // Add namespace itself
247        parents.push(&self.namespace as &dyn MetricsHierarchy);
248
249        parents
250    }
251
252    fn get_metrics_registry(&self) -> &MetricsRegistry {
253        &self.metrics_registry
254    }
255
256    fn connection_id(&self) -> Option<u64> {
257        Some(self.drt.connection_id())
258    }
259}
260
261impl Component {
262    pub fn service_name(&self) -> String {
263        let service_name = format!("{}_{}", self.namespace.name(), self.name);
264        Slug::slugify(&service_name).to_string()
265    }
266
267    pub fn namespace(&self) -> &Namespace {
268        &self.namespace
269    }
270
271    pub fn name(&self) -> &str {
272        &self.name
273    }
274
275    pub fn labels(&self) -> &[(String, String)] {
276        &self.labels
277    }
278
279    pub fn endpoint(&self, endpoint: impl Into<String>) -> Endpoint {
280        let endpoint = Endpoint {
281            component: self.clone(),
282            name: endpoint.into(),
283            labels: Vec::new(),
284            metrics_registry: crate::MetricsRegistry::new(),
285        };
286        // Attach endpoint registry so scrapes traverse separate registries (avoids collisions).
287        self.get_metrics_registry()
288            .add_child_registry(endpoint.get_metrics_registry());
289        endpoint
290    }
291
292    pub async fn list_instances(&self) -> anyhow::Result<Vec<Instance>> {
293        let discovery = self.drt.discovery();
294
295        let discovery_query = crate::discovery::DiscoveryQuery::ComponentEndpoints {
296            namespace: self.namespace.name(),
297            component: self.name.clone(),
298        };
299
300        let discovery_instances = discovery.list(discovery_query).await?;
301
302        // Extract Instance from DiscoveryInstance::Endpoint wrapper
303        let mut instances: Vec<Instance> = discovery_instances
304            .into_iter()
305            .filter_map(|di| match di {
306                crate::discovery::DiscoveryInstance::Endpoint(instance) => Some(instance),
307                _ => None, // Ignore all other variants (ModelCard, etc.)
308            })
309            .collect();
310
311        instances.sort();
312        Ok(instances)
313    }
314}
315
316impl ComponentBuilder {
317    pub fn from_runtime(drt: Arc<DistributedRuntime>) -> Self {
318        Self::default().drt(drt)
319    }
320
321    pub fn build(self) -> Result<Component, anyhow::Error> {
322        let component = self.build_internal()?;
323        // If this component is using NATS, register the NATS service and wait for completion.
324        // This prevents a race condition where serve_endpoint() tries to look up the service
325        // before it's registered in the component registry.
326        let drt = component.drt();
327        if drt.request_plane().is_nats() {
328            let mut rx = drt.register_nats_service(component.clone());
329            // Wait synchronously for the NATS service registration to complete.
330            // Uses block_in_place() to safely call blocking_recv() from async contexts.
331            // This temporarily moves the current task off the runtime thread to allow
332            // blocking without deadlocking the runtime.
333            let result = tokio::task::block_in_place(|| rx.blocking_recv());
334            match result {
335                Some(Ok(())) => {
336                    tracing::debug!(
337                        component = component.service_name(),
338                        "NATS service registration completed"
339                    );
340                }
341                Some(Err(e)) => {
342                    return Err(anyhow::anyhow!(
343                        "NATS service registration failed for component '{}': {}",
344                        component.service_name(),
345                        e
346                    ));
347                }
348                None => {
349                    return Err(anyhow::anyhow!(
350                        "NATS service registration channel closed unexpectedly for component '{}'",
351                        component.service_name()
352                    ));
353                }
354            }
355        }
356        Ok(component)
357    }
358}
359
360#[derive(Debug, Clone)]
361pub struct Endpoint {
362    component: Component,
363
364    // todo - restrict alphabet
365    /// Endpoint name
366    name: String,
367
368    /// Additional labels for metrics
369    labels: Vec<(String, String)>,
370
371    /// This hierarchy's own metrics registry
372    metrics_registry: crate::MetricsRegistry,
373}
374
375impl Hash for Endpoint {
376    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
377        self.component.hash(state);
378        self.name.hash(state);
379    }
380}
381
382impl PartialEq for Endpoint {
383    fn eq(&self, other: &Self) -> bool {
384        self.component == other.component && self.name == other.name
385    }
386}
387
388impl Eq for Endpoint {}
389
390impl DistributedRuntimeProvider for Endpoint {
391    fn drt(&self) -> &DistributedRuntime {
392        self.component.drt()
393    }
394}
395
396impl RuntimeProvider for Endpoint {
397    fn rt(&self) -> &Runtime {
398        self.component.rt()
399    }
400}
401
402impl MetricsHierarchy for Endpoint {
403    fn basename(&self) -> String {
404        self.name.clone()
405    }
406
407    fn parent_hierarchies(&self) -> Vec<&dyn MetricsHierarchy> {
408        let mut parents = vec![];
409
410        // Get all ancestors of component (DRT, Namespace, etc.)
411        parents.extend(self.component.parent_hierarchies());
412
413        // Add component itself
414        parents.push(&self.component as &dyn MetricsHierarchy);
415
416        parents
417    }
418
419    fn get_metrics_registry(&self) -> &MetricsRegistry {
420        &self.metrics_registry
421    }
422
423    fn connection_id(&self) -> Option<u64> {
424        Some(self.component.drt().connection_id())
425    }
426}
427
428impl Endpoint {
429    pub fn id(&self) -> EndpointId {
430        EndpointId {
431            namespace: self.component.namespace().name().to_string(),
432            component: self.component.name().to_string(),
433            name: self.name().to_string(),
434        }
435    }
436
437    pub fn name(&self) -> &str {
438        &self.name
439    }
440
441    pub fn component(&self) -> &Component {
442        &self.component
443    }
444
445    pub async fn client(&self) -> anyhow::Result<client::Client> {
446        client::Client::new(self.clone()).await
447    }
448
449    pub fn endpoint_builder(&self) -> endpoint::EndpointConfigBuilder {
450        endpoint::EndpointConfigBuilder::from_endpoint(self.clone())
451    }
452}
453
454#[derive(Builder, Clone, Validate)]
455#[builder(pattern = "owned")]
456pub struct Namespace {
457    #[builder(private)]
458    runtime: Arc<DistributedRuntime>,
459
460    #[validate(custom(function = "validate_allowed_chars"))]
461    name: String,
462
463    #[builder(default = "None")]
464    parent: Option<Arc<Namespace>>,
465
466    /// Additional labels for metrics
467    #[builder(default = "Vec::new()")]
468    labels: Vec<(String, String)>,
469
470    /// This hierarchy's own metrics registry
471    #[builder(default = "crate::MetricsRegistry::new()")]
472    metrics_registry: crate::MetricsRegistry,
473
474    /// Cache for components to avoid duplicate registrations and metrics collisions.
475    /// When the same component is requested multiple times, we return the cached instance
476    /// to ensure all endpoints share the same Component and MetricsRegistry.
477    /// Uses DashMap for lock-free reads and automatic handling of concurrent inserts.
478    #[builder(default = "Arc::new(DashMap::new())")]
479    component_cache: Arc<DashMap<String, Component>>,
480}
481
482impl DistributedRuntimeProvider for Namespace {
483    fn drt(&self) -> &DistributedRuntime {
484        &self.runtime
485    }
486}
487
488impl std::fmt::Debug for Namespace {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        write!(
491            f,
492            "Namespace {{ name: {}; parent: {:?} }}",
493            self.name, self.parent
494        )
495    }
496}
497
498impl RuntimeProvider for Namespace {
499    fn rt(&self) -> &Runtime {
500        self.runtime.rt()
501    }
502}
503
504impl std::fmt::Display for Namespace {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        write!(f, "{}", self.name)
507    }
508}
509
510impl Namespace {
511    pub(crate) fn new(runtime: DistributedRuntime, name: String) -> anyhow::Result<Self> {
512        let ns = NamespaceBuilder::default()
513            .runtime(Arc::new(runtime))
514            .name(name)
515            .build()?;
516        // Attach namespace registry so scrapes traverse separate registries (avoids collisions).
517        ns.drt()
518            .get_metrics_registry()
519            .add_child_registry(ns.get_metrics_registry());
520        Ok(ns)
521    }
522
523    /// Create a [`Component`] in the namespace who's endpoints can be discovered with etcd
524    ///
525    /// Components are cached by name to ensure that multiple calls with the same name
526    /// return the same Component instance. This prevents duplicate metrics registrations
527    /// and ensures all endpoints share the same Component's MetricsRegistry.
528    pub fn component(&self, name: impl Into<String>) -> anyhow::Result<Component> {
529        let name = name.into();
530
531        // Fast path: Check if component exists in cache
532        // DashMap provides lock-free reads via internal sharding
533        if let Some(cached) = self.component_cache.get(&name) {
534            return Ok(cached.value().clone());
535        }
536
537        // Slow path: Create new component
538        let component = ComponentBuilder::from_runtime(self.runtime.clone())
539            .name(&name)
540            .namespace(self.clone())
541            .build()?;
542
543        // Attach component registry so scrapes traverse separate registries (avoids collisions).
544        self.get_metrics_registry()
545            .add_child_registry(component.get_metrics_registry());
546
547        // Cache the component for future calls
548        // DashMap handles race conditions internally - if another thread
549        // inserted the same key concurrently, we just use our created component
550        self.component_cache.insert(name, component.clone());
551
552        Ok(component)
553    }
554
555    /// Create a [`Namespace`] in the parent namespace
556    pub fn namespace(&self, name: impl Into<String>) -> anyhow::Result<Namespace> {
557        let child = NamespaceBuilder::default()
558            .runtime(self.runtime.clone())
559            .name(name.into())
560            .parent(Some(Arc::new(self.clone())))
561            .build()?;
562        // Attach child namespace registry so scrapes traverse separate registries (avoids collisions).
563        self.get_metrics_registry()
564            .add_child_registry(child.get_metrics_registry());
565        Ok(child)
566    }
567
568    pub fn name(&self) -> String {
569        match &self.parent {
570            Some(parent) => format!("{}.{}", parent.name(), self.name),
571            None => self.name.clone(),
572        }
573    }
574}
575
576// Custom validator function
577fn validate_allowed_chars(input: &str) -> Result<(), ValidationError> {
578    // Define the allowed character set using a regex
579    let regex = regex::Regex::new(r"^[a-z0-9-_]+$").unwrap();
580
581    if regex.is_match(input) {
582        Ok(())
583    } else {
584        Err(ValidationError::new("invalid_characters"))
585    }
586}