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