dynamo_runtime/component/
endpoint.rs1use std::net::SocketAddr;
5use std::sync::Arc;
6
7use anyhow::Result;
8use derive_builder::Builder;
9use derive_getters::Dissolve;
10use educe::Educe;
11use tokio_util::sync::CancellationToken;
12
13use crate::{
14 component::{DeviceType, Endpoint, Instance, TransportType},
15 distributed::RequestPlaneMode,
16 pipeline::network::{
17 PushWorkHandler, RequestPlanePayloadCodec, ingress::push_endpoint::PushEndpoint,
18 },
19 protocols::EndpointId,
20 traits::DistributedRuntimeProvider,
21 transports::{nats, tcp},
22};
23
24fn endpoint_device_type() -> Option<DeviceType> {
25 if std::env::var("CUDA_VISIBLE_DEVICES")
27 .ok()
28 .map(|v| {
29 let l = v.trim().to_ascii_lowercase();
30 l.is_empty() || l == "-1" || l == "none" || l == "void"
31 })
32 .unwrap_or(false)
33 {
34 return Some(DeviceType::Cpu);
35 }
36
37 if std::env::var("NVIDIA_VISIBLE_DEVICES")
39 .ok()
40 .map(|v| {
41 let l = v.trim().to_ascii_lowercase();
42 l == "none" || l == "void"
43 })
44 .unwrap_or(false)
45 {
46 return Some(DeviceType::Cpu);
47 }
48
49 Some(DeviceType::Cuda)
51}
52
53pub struct StartedEndpoint {
59 instance: Instance,
60 shutdown_token: CancellationToken,
61 task: tokio::task::JoinHandle<anyhow::Result<()>>,
62}
63
64impl StartedEndpoint {
65 pub fn instance(&self) -> &Instance {
66 &self.instance
67 }
68
69 pub async fn shutdown(self) -> Result<()> {
70 self.shutdown_token.cancel();
71 self.task.await??;
72 Ok(())
73 }
74
75 pub async fn wait(self) -> Result<()> {
76 self.task.await??;
77 Ok(())
78 }
79}
80
81#[derive(Educe, Builder, Dissolve)]
82#[educe(Debug)]
83#[builder(pattern = "owned", build_fn(private, name = "build_internal"))]
84pub struct EndpointConfig {
85 #[builder(private)]
86 endpoint: Endpoint,
87
88 #[educe(Debug(ignore))]
90 handler: Arc<dyn PushWorkHandler>,
91
92 #[builder(default, setter(into))]
94 metrics_labels: Option<Vec<(String, String)>>,
95
96 #[builder(default = "true")]
98 graceful_shutdown: bool,
99
100 #[educe(Debug(ignore))]
104 #[builder(default, setter(into, strip_option))]
105 health_check_payload: Option<serde_json::Value>,
106}
107
108impl EndpointConfigBuilder {
109 pub(crate) fn from_endpoint(endpoint: Endpoint) -> Self {
110 Self::default().endpoint(endpoint)
111 }
112
113 pub fn register_local_engine(
115 self,
116 engine: crate::local_endpoint_registry::LocalAsyncEngine,
117 ) -> Result<Self> {
118 if let Some(endpoint) = &self.endpoint {
119 let registry = endpoint.drt().local_endpoint_registry();
120 registry.register(endpoint.name.clone(), engine);
121 tracing::debug!(
122 "Registered engine for endpoint '{}' in local registry",
123 endpoint.name
124 );
125 }
126 Ok(self)
127 }
128
129 pub async fn start(self) -> Result<()> {
130 self.start_with_registration().await?.wait().await
131 }
132
133 pub async fn start_with_registration(self) -> Result<StartedEndpoint> {
135 let (endpoint, handler, metrics_labels, graceful_shutdown, health_check_payload) =
136 self.build_internal()?.dissolve();
137 let connection_id = endpoint.drt().connection_id();
138 let endpoint_id = endpoint.id();
139
140 tracing::debug!("Starting endpoint: {endpoint_id}");
141
142 let metrics_labels: Option<Vec<(&str, &str)>> = metrics_labels
143 .as_ref()
144 .map(|v| v.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect());
145 handler.bind_endpoint(&endpoint);
146 handler.add_metrics(&endpoint, metrics_labels.as_deref())?;
148
149 let endpoint_shutdown_token = endpoint.drt().child_token();
152
153 let system_health = endpoint.drt().system_health();
154
155 let namespace_name_for_task = endpoint_id.namespace.clone();
157 let component_name_for_task = endpoint_id.component.clone();
158 let endpoint_name_for_task = endpoint_id.name.clone();
159
160 let server = endpoint.drt().request_plane_server().await?;
162 let transport = build_transport_type(&endpoint, &endpoint_id, connection_id).await?;
163
164 if let Some(health_check_payload) = &health_check_payload {
166 if system_health.lock().health_check_enabled()
167 && endpoint
168 .drt()
169 .local_endpoint_registry()
170 .get(&endpoint.name)
171 .is_none()
172 {
173 anyhow::bail!(
174 "Endpoint '{}' has a health_check_payload and canary is enabled, \
175 but no local engine is registered. Call .register_local_engine() \
176 before .start() so the canary health check can function.",
177 endpoint.name
178 );
179 }
180
181 let instance = Instance {
182 component: endpoint_id.component.clone(),
183 endpoint: endpoint_id.name.clone(),
184 namespace: endpoint_id.namespace.clone(),
185 instance_id: connection_id,
186 transport: transport.clone(),
187 device_type: endpoint_device_type(),
188 request_plane_codec: Some(RequestPlanePayloadCodec::configured()),
189 };
190 tracing::debug!(endpoint_name = %endpoint.name, "Registering endpoint health check target");
191 let guard = system_health.lock();
192 guard.register_health_check_target(
193 &endpoint.name,
194 instance,
195 health_check_payload.clone(),
196 );
197 if let Some(notifier) = guard.get_endpoint_health_check_notifier(&endpoint.name) {
198 handler.set_endpoint_health_check_notifier(notifier)?;
199 }
200 }
201
202 tracing::debug!(
203 endpoint = %endpoint_name_for_task,
204 transport = server.transport_name(),
205 "Registering endpoint with request plane server"
206 );
207
208 server
210 .register_endpoint(
211 endpoint_name_for_task.clone(),
212 handler,
213 connection_id,
214 namespace_name_for_task.clone(),
215 component_name_for_task.clone(),
216 system_health.clone(),
217 )
218 .await?;
219
220 let tracker_clone = if graceful_shutdown {
221 tracing::debug!(
222 "Registering endpoint '{}' with graceful shutdown tracker",
223 endpoint.name
224 );
225 let tracker = endpoint.drt().graceful_shutdown_tracker();
226 tracker.register_endpoint();
227 Some(tracker)
228 } else {
229 tracing::debug!("Endpoint '{}' has graceful_shutdown=false", endpoint.name);
230 None
231 };
232
233 let discovery = endpoint.drt().discovery();
237
238 let discovery_spec = crate::discovery::DiscoverySpec::Endpoint {
239 namespace: endpoint_id.namespace.clone(),
240 component: endpoint_id.component.clone(),
241 endpoint: endpoint_id.name.clone(),
242 transport,
243 device_type: endpoint_device_type(),
244 request_plane_codec: Some(RequestPlanePayloadCodec::configured()),
245 };
246
247 let discovery_instance = match discovery.register(discovery_spec).await {
248 Ok(instance) => instance,
249 Err(e) => {
250 tracing::error!(
251 %endpoint_id,
252 error = %e,
253 "Unable to register service for discovery"
254 );
255 let _ = server
256 .unregister_endpoint_instance(&endpoint_id, connection_id)
257 .await;
258 if let Some(tracker) = tracker_clone {
259 tracker.unregister_endpoint();
260 }
261 anyhow::bail!(
262 "Unable to register service for discovery. Check discovery service status"
263 );
264 }
265 };
266 let instance = match &discovery_instance {
267 crate::discovery::DiscoveryInstance::Endpoint(instance) => instance.clone(),
268 _ => unreachable!("endpoint discovery spec returned a non-endpoint instance"),
269 };
270
271 let endpoint_name_for_cleanup = endpoint_name_for_task;
273 let server_for_cleanup = server;
274 let cancel_token_for_cleanup = endpoint_shutdown_token.clone();
275 let discovery_for_cleanup = discovery;
276
277 let task: tokio::task::JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
278 cancel_token_for_cleanup.cancelled().await;
279
280 if let Err(error) = discovery_for_cleanup.unregister(discovery_instance).await {
281 tracing::warn!(%error, "Failed to unregister endpoint from discovery");
282 }
283
284 tracing::debug!(
285 endpoint = %endpoint_name_for_cleanup,
286 "Unregistering endpoint from request plane server"
287 );
288
289 if let Err(e) = server_for_cleanup
290 .unregister_endpoint_instance(&endpoint_id, connection_id)
291 .await
292 {
293 tracing::warn!(
294 endpoint = %endpoint_name_for_cleanup,
295 error = %e,
296 "Failed to unregister endpoint"
297 );
298 }
299
300 if let Some(tracker) = tracker_clone {
301 tracing::debug!("Unregister endpoint from graceful shutdown tracker");
302 tracker.unregister_endpoint();
303 }
304
305 anyhow::Ok(())
306 });
307
308 Ok(StartedEndpoint {
309 instance,
310 shutdown_token: endpoint_shutdown_token,
311 task,
312 })
313 }
314}
315
316fn build_transport_type_inner(
326 mode: RequestPlaneMode,
327 endpoint_id: &EndpointId,
328 connection_id: u64,
329) -> Result<TransportType> {
330 match mode {
331 RequestPlaneMode::Tcp => {
332 let address = crate::pipeline::network::manager::get_actual_tcp_rpc_address()?;
333 Ok(tcp_transport_type(address, endpoint_id, connection_id))
334 }
335 RequestPlaneMode::Nats => Ok(TransportType::Nats(nats::instance_subject(
336 endpoint_id,
337 connection_id,
338 ))),
339 }
340}
341
342fn tcp_transport_type(
343 address: SocketAddr,
344 endpoint_id: &EndpointId,
345 connection_id: u64,
346) -> TransportType {
347 TransportType::Tcp(format!(
349 "{address}/{}",
350 tcp::instance_path(endpoint_id, connection_id)
351 ))
352}
353
354pub async fn build_transport_type(
360 endpoint: &Endpoint,
361 endpoint_id: &EndpointId,
362 connection_id: u64,
363) -> Result<TransportType> {
364 let mode = endpoint.drt().request_plane();
365
366 if mode == RequestPlaneMode::Tcp {
367 let _ = endpoint.drt().request_plane_server().await?;
368 }
369
370 build_transport_type_inner(mode, endpoint_id, connection_id)
371}
372
373impl Endpoint {
374 pub async fn unregister_endpoint_instance(&self) -> anyhow::Result<()> {
380 let drt = self.drt();
381 let instance_id = drt.connection_id();
382 let endpoint_id = self.id();
383
384 let transport = build_transport_type(self, &endpoint_id, instance_id).await?;
386
387 let instance = crate::discovery::DiscoveryInstance::Endpoint(Instance {
388 namespace: endpoint_id.namespace,
389 component: endpoint_id.component,
390 endpoint: endpoint_id.name,
391 instance_id,
392 transport,
393 device_type: endpoint_device_type(),
394 request_plane_codec: Some(RequestPlanePayloadCodec::configured()),
395 });
396
397 let discovery = drt.discovery();
398 if let Err(e) = discovery.unregister(instance).await {
399 let endpoint_id = self.id();
400 tracing::error!(
401 %endpoint_id,
402 error = %e,
403 "Unable to unregister endpoint instance from discovery"
404 );
405 anyhow::bail!(
406 "Unable to unregister endpoint instance from discovery. Check discovery service status"
407 );
408 }
409
410 tracing::info!(
411 instance_id = instance_id,
412 "Successfully unregistered endpoint instance from discovery - worker removed from routing pool"
413 );
414
415 Ok(())
416 }
417
418 pub async fn register_endpoint_instance(&self) -> anyhow::Result<()> {
424 let drt = self.drt();
425 let instance_id = drt.connection_id();
426 let endpoint_id = self.id();
427
428 let transport = build_transport_type(self, &endpoint_id, instance_id).await?;
430
431 let spec = crate::discovery::DiscoverySpec::Endpoint {
432 namespace: endpoint_id.namespace,
433 component: endpoint_id.component,
434 endpoint: endpoint_id.name,
435 transport,
436 device_type: endpoint_device_type(),
437 request_plane_codec: Some(RequestPlanePayloadCodec::configured()),
438 };
439
440 let discovery = drt.discovery();
441 if let Err(e) = discovery.register(spec).await {
442 let endpoint_id = self.id();
443 tracing::error!(
444 %endpoint_id,
445 error = %e,
446 "Unable to re-register endpoint instance to discovery"
447 );
448 anyhow::bail!(
449 "Unable to re-register endpoint instance to discovery. Check discovery service status"
450 );
451 }
452
453 tracing::info!(
454 instance_id = instance_id,
455 "Successfully re-registered endpoint instance to discovery - worker added back to routing pool"
456 );
457
458 Ok(())
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn tcp_transport_uses_concrete_ipv4_and_bracketed_ipv6_addresses() {
468 let endpoint_id = EndpointId {
469 namespace: "ns".to_string(),
470 component: "worker".to_string(),
471 name: "generate".to_string(),
472 };
473
474 for (address, expected) in [
475 ("192.0.2.10:1234", "192.0.2.10:1234/2a/ns/worker/generate"),
476 (
477 "[2001:db8::10]:1234",
478 "[2001:db8::10]:1234/2a/ns/worker/generate",
479 ),
480 ] {
481 let transport = tcp_transport_type(address.parse().unwrap(), &endpoint_id, 0x2a);
482 assert_eq!(transport.address(), expected);
483 assert!(!transport.address().starts_with("0.0.0.0"));
484 assert!(!transport.address().starts_with("[::]"));
485 }
486 }
487}