1#[cfg(test)]
8mod probe_tests;
9
10use crate::config::HealthStatus;
11use crate::config::environment_names::logging as env_logging;
12use crate::config::environment_names::runtime::canary as env_canary;
13use crate::config::environment_names::runtime::system as env_system;
14use crate::metrics::MetricsHierarchy;
15use crate::traits::DistributedRuntimeProvider;
16use crate::utils::ip_resolver::{
17 DefaultIpResolver, IpResolutionError, IpResolver, resolve_advertise_ip_for_bind,
18};
19use axum::{
20 Router,
21 body::Bytes,
22 extract::{Json, Path, State},
23 http::StatusCode,
24 response::IntoResponse,
25 routing::{any, delete, get, post},
26 serve::Listener,
27};
28use futures::StreamExt;
29use serde::{Deserialize, Serialize};
30use serde_json::json;
31use std::collections::HashMap;
32use std::io;
33use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
34use std::sync::{Arc, OnceLock};
35use std::time::{Duration, Instant};
36use tokio::{net::TcpListener, task::JoinHandle};
37use tokio_util::sync::CancellationToken;
38use tower_http::trace::TraceLayer;
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum SystemProbePolicy {
43 #[default]
45 Worker,
46 RuntimeOnly,
48}
49
50#[derive(Debug)]
52pub struct SystemStatusServerInfo {
53 pub socket_addr: SocketAddr,
54 pub handle: Option<Arc<JoinHandle<()>>>,
55}
56
57impl SystemStatusServerInfo {
58 pub fn new(socket_addr: SocketAddr, handle: Option<JoinHandle<()>>) -> Self {
59 Self {
60 socket_addr,
61 handle: handle.map(Arc::new),
62 }
63 }
64
65 pub fn address(&self) -> String {
66 self.socket_addr.to_string()
67 }
68
69 pub fn hostname(&self) -> String {
70 self.socket_addr.ip().to_string()
71 }
72
73 pub fn port(&self) -> u16 {
74 self.socket_addr.port()
75 }
76
77 pub fn advertised_socket_addr(&self) -> SocketAddr {
83 match advertised_socket_addr(self.socket_addr, &DefaultIpResolver) {
84 Ok(address) => {
85 if self.socket_addr.ip().is_unspecified() && address.ip().is_loopback() {
86 static WARNED: std::sync::Once = std::sync::Once::new();
87 WARNED.call_once(|| {
88 tracing::warn!(
89 bind = %self.socket_addr,
90 advertised = %address,
91 "System status server found no non-loopback address in the bound family, so remote callers cannot reach the advertised address; on IPv6-only hosts, set DYN_SYSTEM_HOST=::"
92 );
93 });
94 }
95 address
96 }
97 Err(error) => {
98 let fallback = match self.socket_addr.ip() {
99 IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST),
100 IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST),
101 };
102 tracing::warn!(
103 %error,
104 %fallback,
105 "Failed to resolve the system status advertisement address; using loopback"
106 );
107 SocketAddr::new(fallback, self.socket_addr.port())
108 }
109 }
110 }
111}
112
113fn advertised_socket_addr<R: IpResolver>(
114 bound: SocketAddr,
115 resolver: &R,
116) -> Result<SocketAddr, IpResolutionError> {
117 let advertise_ip = resolve_advertise_ip_for_bind(bound.ip(), resolver)?;
118 Ok(SocketAddr::new(advertise_ip, bound.port()))
119}
120
121impl Clone for SystemStatusServerInfo {
122 fn clone(&self) -> Self {
123 Self {
124 socket_addr: self.socket_addr,
125 handle: self.handle.clone(),
126 }
127 }
128}
129
130pub struct SystemStatusState {
132 root_drt: Arc<crate::DistributedRuntime>,
134 discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
136}
137
138impl SystemStatusState {
139 pub fn new(
141 drt: Arc<crate::DistributedRuntime>,
142 discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
143 ) -> anyhow::Result<Self> {
144 Ok(Self {
145 root_drt: drt,
146 discovery_metadata,
147 })
148 }
149
150 pub fn drt(&self) -> &crate::DistributedRuntime {
152 &self.root_drt
153 }
154
155 pub fn discovery_metadata(
157 &self,
158 ) -> Option<&Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>> {
159 self.discovery_metadata.as_ref()
160 }
161}
162
163#[derive(Debug, Clone, Deserialize, Serialize)]
165pub struct LoadLoraRequest {
166 pub lora_name: String,
167 pub source: LoraSource,
168}
169
170#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct LoraSource {
173 pub uri: String,
174}
175
176#[derive(Debug, Clone, Deserialize, Serialize)]
178pub struct LoraResponse {
179 pub status: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub message: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub lora_name: Option<String>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub lora_id: Option<u64>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub loras: Option<serde_json::Value>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub count: Option<usize>,
190}
191
192const SYSTEM_STATUS_REBIND_BACKOFF: Duration = Duration::from_secs(1);
193
194pub async fn spawn_system_status_server(
196 host: &str,
197 port: u16,
198 cancel_token: CancellationToken,
199 drt: Arc<crate::DistributedRuntime>,
200 discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
201 policy: SystemProbePolicy,
202) -> anyhow::Result<(SocketAddr, JoinHandle<()>)> {
203 let server_state = Arc::new(SystemStatusState::new(drt, discovery_metadata)?);
205 let app = build_system_status_router(server_state, policy);
206
207 let initial_bind_address = format!("{}:{}", host, port);
208 tracing::info!("[spawn_system_status_server] binding to: {initial_bind_address}");
209 let (listener, actual_address) = bind_system_status_listener(&initial_bind_address)
210 .await
211 .map_err(|error| {
212 tracing::error!("Failed to bind to address {initial_bind_address}: {error}");
213 error
214 })?;
215 tracing::info!("[spawn_system_status_server] system status server bound to: {actual_address}");
216
217 let listener =
219 RebindingTcpListener::new(listener, actual_address, SYSTEM_STATUS_REBIND_BACKOFF);
220 let observer = cancel_token.child_token();
221
222 let handle = tokio::spawn(async move {
224 if let Err(e) = axum::serve(listener, app)
225 .with_graceful_shutdown(observer.cancelled_owned())
226 .await
227 {
228 tracing::error!("System status server error: {e}");
229 }
230 });
231
232 Ok((actual_address, handle))
233}
234
235fn build_system_status_router(
236 server_state: Arc<SystemStatusState>,
237 policy: SystemProbePolicy,
238) -> Router {
239 let system_health = server_state.drt().system_health();
240 let (health_path, live_path) = {
241 let health = system_health.lock();
242 (
243 health.health_path().to_string(),
244 health.live_path().to_string(),
245 )
246 };
247 let mut app = match policy {
248 SystemProbePolicy::Worker => Router::new()
249 .route(
250 &health_path,
251 get({
252 let state = Arc::clone(&server_state);
253 move || health_handler(state)
254 }),
255 )
256 .route(
257 &live_path,
258 get({
259 let state = Arc::clone(&server_state);
260 move || health_handler(state)
261 }),
262 ),
263 SystemProbePolicy::RuntimeOnly => Router::new()
264 .route(&live_path, get(|| async { StatusCode::OK }))
265 .route(
266 &health_path,
267 get({
268 let state = Arc::clone(&server_state);
269 move || runtime_health_handler(state)
270 }),
271 ),
272 };
273 let lora_enabled =
275 crate::config::env_is_truthy(crate::config::environment_names::llm::DYN_LORA_ENABLED);
276
277 app = app
278 .route(
279 "/metrics",
280 get({
281 let state = Arc::clone(&server_state);
282 move || metrics_handler(state)
283 }),
284 )
285 .route(
286 "/metadata",
287 get({
288 let state = Arc::clone(&server_state);
289 move || metadata_handler(state)
290 }),
291 )
292 .route(
293 "/engine/{*path}",
294 any({
295 let state = Arc::clone(&server_state);
296 move |path, body| engine_route_handler(state, path, body)
297 }),
298 );
299
300 if lora_enabled {
302 app = app
303 .route(
304 "/v1/loras",
305 get({
306 let state = Arc::clone(&server_state);
307 move || list_loras_handler(State(state))
308 })
309 .post({
310 let state = Arc::clone(&server_state);
311 move |body| load_lora_handler(State(state), body)
312 }),
313 )
314 .route(
315 "/v1/loras/{*lora_name}",
316 delete({
317 let state = Arc::clone(&server_state);
318 move |path| unload_lora_handler(State(state), path)
319 }),
320 );
321 }
322
323 app.route(
328 "/v1/metadata/{namespace}/{component}/{endpoint}/{model_slug}/{model_suffix}/{*filename}",
329 get({
330 let state = Arc::clone(&server_state);
331 move |path| metadata_file_handler(State(state), path)
332 }),
333 )
334 .fallback(|uri: axum::http::Uri| async move {
335 tracing::debug!(%uri, "system status server has no route for this request");
336 (StatusCode::NOT_FOUND, "Route not found").into_response()
337 })
338 .layer(TraceLayer::new_for_http().make_span_with(crate::logging::make_system_request_span))
339}
340
341async fn bind_system_status_listener(
342 address: &str,
343) -> anyhow::Result<(TcpListener, std::net::SocketAddr)> {
344 let listener = TcpListener::bind(address)
345 .await
346 .map_err(|error| anyhow::anyhow!("Failed to bind to address: {error}"))?;
347
348 let actual_address = listener.local_addr()?;
349
350 Ok((listener, actual_address))
351}
352
353struct RebindingTcpListener {
355 address: std::net::SocketAddr,
357 listener: Option<TcpListener>,
358 rebind_backoff: Duration,
359}
360
361impl RebindingTcpListener {
362 fn new(listener: TcpListener, address: std::net::SocketAddr, rebind_backoff: Duration) -> Self {
363 Self {
364 address,
365 listener: Some(listener),
366 rebind_backoff,
367 }
368 }
369}
370
371impl Listener for RebindingTcpListener {
372 type Io = tokio::net::TcpStream;
373 type Addr = std::net::SocketAddr;
374
375 async fn accept(&mut self) -> (Self::Io, Self::Addr) {
376 let mut next_rebind = tokio::time::Instant::now();
377 loop {
378 let Some(listener) = self.listener.as_ref() else {
379 tokio::time::sleep_until(next_rebind).await;
380 next_rebind = tokio::time::Instant::now() + self.rebind_backoff;
381 tracing::debug!(address = %self.address, "System status server rebinding");
382 match TcpListener::bind(self.address).await {
383 Ok(listener) => {
384 tracing::info!(address = %self.address, "System status server rebound");
385 self.listener = Some(listener);
386 }
387 Err(error) => {
388 tracing::error!(
389 address = %self.address,
390 %error,
391 retry_after = ?self.rebind_backoff,
392 "System status server failed to rebind"
393 );
394 }
395 }
396 continue;
397 };
398
399 match listener.accept().await {
400 Ok(accepted) => return accepted,
401 Err(error) if is_connection_error(&error) => {
403 tracing::trace!(
404 address = %self.address,
405 %error,
406 "System status connection failed before accept"
407 );
408 }
409 Err(error) if is_resource_exhaustion_error(&error) => {
411 tracing::error!(
412 address = %self.address,
413 %error,
414 retry_after = ?self.rebind_backoff,
415 "System status listener lacks resources; retrying the same socket"
416 );
417 tokio::time::sleep(self.rebind_backoff).await;
418 }
419 Err(error) => {
420 tracing::error!(
421 address = %self.address,
422 %error,
423 "System status listener stopped accepting; rebinding"
424 );
425 self.listener = None;
427 }
428 }
429 }
430 }
431
432 fn local_addr(&self) -> io::Result<Self::Addr> {
433 Ok(self.address)
434 }
435}
436
437fn is_connection_error(error: &io::Error) -> bool {
438 if matches!(
439 error.kind(),
440 io::ErrorKind::ConnectionRefused
441 | io::ErrorKind::ConnectionAborted
442 | io::ErrorKind::ConnectionReset
443 ) {
444 return true;
445 }
446
447 #[cfg(target_os = "linux")]
449 if let Some(code) = error.raw_os_error() {
450 return matches!(
451 code,
452 libc::ENETDOWN
453 | libc::EPROTO
454 | libc::ENOPROTOOPT
455 | libc::EHOSTDOWN
456 | libc::ENONET
457 | libc::EHOSTUNREACH
458 | libc::EOPNOTSUPP
459 | libc::ENETUNREACH
460 );
461 }
462
463 false
464}
465
466fn is_resource_exhaustion_error(error: &io::Error) -> bool {
468 if error.kind() == io::ErrorKind::OutOfMemory {
469 return true;
470 }
471
472 #[cfg(unix)]
473 {
474 if let Some(code) = error.raw_os_error() {
475 return matches!(code, libc::ENFILE | libc::EMFILE | libc::ENOBUFS);
476 }
477 }
478
479 false
480}
481
482#[tracing::instrument(skip_all, level = "trace")]
484async fn health_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
485 let system_health = state.drt().system_health();
487 let system_health_lock = system_health.lock();
488 let (healthy, endpoints) = system_health_lock.get_health_status();
489 let uptime = Some(system_health_lock.uptime());
490 drop(system_health_lock);
491
492 let healthy_string = if healthy { "ready" } else { "notready" };
493 let status_code = if healthy {
494 StatusCode::OK
495 } else {
496 StatusCode::SERVICE_UNAVAILABLE
497 };
498
499 let response = json!({
500 "status": healthy_string,
501 "uptime": uptime,
502 "endpoints": endpoints,
503 });
504
505 tracing::trace!("Response {}", response.to_string());
506
507 (status_code, response.to_string())
508}
509
510async fn runtime_health_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
512 let ready = matches!(
513 tokio::time::timeout(Duration::from_secs(1), state.drt().check_dependencies()).await,
514 Ok(Ok(()))
515 );
516 let (code, status) = if ready {
517 (StatusCode::OK, "ready")
518 } else {
519 (StatusCode::SERVICE_UNAVAILABLE, "notready")
520 };
521 (code, Json(json!({ "status": status })))
522}
523
524#[tracing::instrument(skip_all, level = "trace")]
526async fn metrics_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
527 let response = match state.drt().metrics().prometheus_expfmt() {
534 Ok(r) => r,
535 Err(e) => {
536 tracing::error!("Failed to get metrics from registry: {e}");
537 return (
538 StatusCode::INTERNAL_SERVER_ERROR,
539 "Failed to get metrics".to_string(),
540 );
541 }
542 };
543
544 (StatusCode::OK, response)
545}
546
547#[tracing::instrument(skip_all, level = "trace")]
549async fn metadata_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
550 let metadata = match state.discovery_metadata() {
552 Some(metadata) => metadata,
553 None => {
554 tracing::debug!("Metadata endpoint called but no discovery metadata available");
555 return (
556 StatusCode::NOT_FOUND,
557 "Discovery metadata not available".to_string(),
558 )
559 .into_response();
560 }
561 };
562
563 let metadata_guard = metadata.read().await;
565
566 match serde_json::to_string(&*metadata_guard) {
568 Ok(json) => {
569 tracing::trace!("Returning metadata: {} bytes", json.len());
570 (StatusCode::OK, json).into_response()
571 }
572 Err(e) => {
573 tracing::error!("Failed to serialize metadata: {e}");
574 (
575 StatusCode::INTERNAL_SERVER_ERROR,
576 "Failed to serialize metadata".to_string(),
577 )
578 .into_response()
579 }
580 }
581}
582
583#[tracing::instrument(skip_all, level = "debug")]
585async fn load_lora_handler(
586 State(state): State<Arc<SystemStatusState>>,
587 Json(request): Json<LoadLoraRequest>,
588) -> impl IntoResponse {
589 tracing::info!("Loading LoRA: {}", request.lora_name);
590
591 match call_lora_endpoint(
593 state.drt(),
594 "load_lora",
595 json!({
596 "lora_name": request.lora_name,
597 "source": {
598 "uri": request.source.uri
599 },
600 }),
601 )
602 .await
603 {
604 Ok(response) => {
605 if response.status == "error" {
606 tracing::error!(
607 "Failed to load LoRA {}: {}",
608 request.lora_name,
609 response.message.as_deref().unwrap_or("Unknown error")
610 );
611 (StatusCode::INTERNAL_SERVER_ERROR, Json(response))
612 } else {
613 tracing::info!("LoRA loaded successfully: {}", request.lora_name);
614 (StatusCode::OK, Json(response))
615 }
616 }
617 Err(e) => {
618 tracing::error!("Failed to load LoRA {}: {}", request.lora_name, e);
619 (
620 StatusCode::INTERNAL_SERVER_ERROR,
621 Json(LoraResponse {
622 status: "error".to_string(),
623 message: Some(e.to_string()),
624 lora_name: Some(request.lora_name),
625 lora_id: None,
626 loras: None,
627 count: None,
628 }),
629 )
630 }
631 }
632}
633
634#[tracing::instrument(skip_all, level = "debug")]
636async fn unload_lora_handler(
637 State(state): State<Arc<SystemStatusState>>,
638 Path(lora_name): Path<String>,
639) -> impl IntoResponse {
640 let lora_name = lora_name
642 .strip_prefix('/')
643 .unwrap_or(&lora_name)
644 .to_string();
645 tracing::info!("Unloading LoRA: {lora_name}");
646
647 match call_lora_endpoint(
649 state.drt(),
650 "unload_lora",
651 json!({
652 "lora_name": lora_name.clone(),
653 }),
654 )
655 .await
656 {
657 Ok(response) => {
658 if response.status == "error" {
659 tracing::error!(
660 "Failed to unload LoRA {}: {}",
661 lora_name,
662 response.message.as_deref().unwrap_or("Unknown error")
663 );
664 (StatusCode::INTERNAL_SERVER_ERROR, Json(response))
665 } else {
666 tracing::info!("LoRA unloaded successfully: {lora_name}");
667 (StatusCode::OK, Json(response))
668 }
669 }
670 Err(e) => {
671 tracing::error!("Failed to unload LoRA {}: {}", lora_name, e);
672 (
673 StatusCode::INTERNAL_SERVER_ERROR,
674 Json(LoraResponse {
675 status: "error".to_string(),
676 message: Some(e.to_string()),
677 lora_name: Some(lora_name),
678 lora_id: None,
679 loras: None,
680 count: None,
681 }),
682 )
683 }
684 }
685}
686
687#[tracing::instrument(skip_all, level = "debug")]
689async fn list_loras_handler(State(state): State<Arc<SystemStatusState>>) -> impl IntoResponse {
690 tracing::info!("Listing all LoRAs");
691
692 match call_lora_endpoint(state.drt(), "list_loras", json!({})).await {
694 Ok(response) => {
695 tracing::info!("Successfully retrieved LoRA list");
696 (StatusCode::OK, Json(response))
697 }
698 Err(e) => {
699 tracing::error!("Failed to list LoRAs: {e}");
700 (
701 StatusCode::INTERNAL_SERVER_ERROR,
702 Json(LoraResponse {
703 status: "error".to_string(),
704 message: Some(e.to_string()),
705 lora_name: None,
706 lora_id: None,
707 loras: None,
708 count: None,
709 }),
710 )
711 }
712 }
713}
714
715async fn metadata_file_handler(
718 State(state): State<Arc<SystemStatusState>>,
719 Path((namespace, component, endpoint, model_slug, model_suffix, filename)): Path<(
720 String,
721 String,
722 String,
723 String,
724 String,
725 String,
726 )>,
727) -> impl IntoResponse {
728 let path = match state.drt().metadata_artifacts().get(
729 &namespace,
730 &component,
731 &endpoint,
732 &model_slug,
733 &model_suffix,
734 &filename,
735 ) {
736 Some(p) => p,
737 None => {
738 tracing::debug!(
739 namespace,
740 component,
741 endpoint,
742 model_slug,
743 model_suffix,
744 filename,
745 "metadata artifact not registered for self-host"
746 );
747 return (StatusCode::NOT_FOUND, "Not found").into_response();
748 }
749 };
750
751 match tokio::fs::read(&path).await {
752 Ok(bytes) => (StatusCode::OK, bytes).into_response(),
753 Err(err) => {
754 tracing::error!(
755 namespace,
756 component,
757 endpoint,
758 model_slug,
759 model_suffix,
760 filename,
761 path = %path.display(),
762 %err,
763 "failed to read self-hosted metadata file"
764 );
765 (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file").into_response()
766 }
767 }
768}
769
770async fn call_lora_endpoint(
786 drt: &crate::DistributedRuntime,
787 endpoint_name: &str,
788 request_body: serde_json::Value,
789) -> anyhow::Result<LoraResponse> {
790 use crate::engine::AsyncEngine;
791
792 tracing::debug!("Calling LoRA endpoint: '{endpoint_name}'");
793
794 if let Some(engine) = drt.local_endpoint_registry().get(endpoint_name) {
796 tracing::debug!(
797 "Found endpoint '{}' in local registry, calling directly",
798 endpoint_name
799 );
800
801 let request = crate::pipeline::SingleIn::new(request_body);
802 let mut stream = engine.generate(request).await?;
803
804 if let Some(response) = stream.next().await {
805 let response_data = response.data.unwrap_or_default();
806 let lora_response = serde_json::from_value::<LoraResponse>(response_data.clone())
807 .unwrap_or_else(|_| parse_lora_response(&response_data));
808 return Ok(lora_response);
809 }
810
811 anyhow::bail!("No response received from endpoint '{}'", endpoint_name)
812 }
813
814 let update_key = format!("update/{endpoint_name}");
818 if let Some(callback) = drt.engine_routes().get(&update_key) {
819 tracing::debug!(
820 "Found '{}' in engine routes registry, invoking update callback",
821 update_key
822 );
823 let response_data = callback(request_body).await?;
824 let lora_response = serde_json::from_value::<LoraResponse>(response_data.clone())
825 .unwrap_or_else(|_| parse_lora_response(&response_data));
826 return Ok(lora_response);
827 }
828
829 anyhow::bail!(
830 "LoRA management not available: no '{}' handler is registered \
831 (neither a local LoRA endpoint nor an engine update). This worker \
832 either has LoRA disabled or its backend does not support LoRA \
833 management.",
834 endpoint_name
835 )
836}
837
838fn parse_lora_response(response_data: &serde_json::Value) -> LoraResponse {
840 LoraResponse {
841 status: response_data
842 .get("status")
843 .and_then(|s| s.as_str())
844 .unwrap_or("success")
845 .to_string(),
846 message: response_data
847 .get("message")
848 .and_then(|m| m.as_str())
849 .map(|s| s.to_string()),
850 lora_name: response_data
851 .get("lora_name")
852 .and_then(|n| n.as_str())
853 .map(|s| s.to_string()),
854 lora_id: response_data.get("lora_id").and_then(|id| id.as_u64()),
855 loras: response_data.get("loras").cloned(),
856 count: response_data
857 .get("count")
858 .and_then(|c| c.as_u64())
859 .map(|c| c as usize),
860 }
861}
862
863#[tracing::instrument(skip_all, level = "trace", fields(path = %path))]
868async fn engine_route_handler(
869 state: Arc<SystemStatusState>,
870 Path(path): Path<String>,
871 body: Bytes,
872) -> impl IntoResponse {
873 tracing::trace!("Engine route request to /engine/{path}");
874
875 let body_json: serde_json::Value = if body.is_empty() {
877 serde_json::json!({})
878 } else {
879 match serde_json::from_slice(&body) {
880 Ok(json) => json,
881 Err(e) => {
882 tracing::warn!("Invalid JSON in request body: {e}");
883 return (
884 StatusCode::BAD_REQUEST,
885 json!({
886 "error": "Invalid JSON",
887 "message": format!("{}", e)
888 })
889 .to_string(),
890 )
891 .into_response();
892 }
893 }
894 };
895
896 let callback = match state.drt().engine_routes().get(&path) {
898 Some(cb) => cb,
899 None => {
900 tracing::debug!("Route /engine/{path} not found");
901 return (
902 StatusCode::NOT_FOUND,
903 json!({
904 "error": "Route not found",
905 "message": format!("Route /engine/{} not found", path)
906 })
907 .to_string(),
908 )
909 .into_response();
910 }
911 };
912
913 match callback(body_json).await {
915 Ok(response) => {
916 tracing::trace!("Engine route handler succeeded for /engine/{path}");
917 (StatusCode::OK, response.to_string()).into_response()
918 }
919 Err(e) => {
920 tracing::error!("Engine route handler error for /engine/{}: {}", path, e);
921 (
922 StatusCode::INTERNAL_SERVER_ERROR,
923 json!({
924 "error": "Handler error",
925 "message": format!("{}", e)
926 })
927 .to_string(),
928 )
929 .into_response()
930 }
931 }
932}
933
934#[cfg(test)]
936mod tests {
937 use super::*;
938 use crate::utils::ip_resolver::test_support::StubResolver;
939 use tokio::time::Duration;
940
941 #[test]
942 fn advertised_address_stays_in_the_bound_family() {
943 let mut resolver = StubResolver::not_found();
944 resolver.interfaces = vec![
945 ("lo", "127.0.0.1".parse().unwrap()),
946 ("eth0", "2001:db8::20".parse().unwrap()),
947 ];
948
949 let advertised = advertised_socket_addr("0.0.0.0:8080".parse().unwrap(), &resolver)
950 .expect("wildcard advertisement should resolve");
951
952 assert_eq!(advertised, "127.0.0.1:8080".parse().unwrap());
953 }
954
955 #[tokio::test]
957 async fn test_http_server_lifecycle() {
958 let cancel_token = CancellationToken::new();
959 let cancel_token_for_server = cancel_token.clone();
960
961 let app = Router::new().route("/test", get(|| async { (StatusCode::OK, "test") }));
963
964 let server_handle = tokio::spawn(async move {
966 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
967 let _ = axum::serve(listener, app)
968 .with_graceful_shutdown(cancel_token_for_server.cancelled_owned())
969 .await;
970 });
971
972 cancel_token.cancel();
976
977 let result = tokio::time::timeout(Duration::from_secs(5), server_handle).await;
979 assert!(
980 result.is_ok(),
981 "HTTP server should shut down when cancel token is cancelled"
982 );
983 }
984
985 #[cfg(unix)]
986 #[tokio::test]
987 async fn test_rebinding_listener_serves_again_after_listener_shutdown() {
988 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
989 let address = listener.local_addr().unwrap();
990
991 let shutdown_result = socket2::SockRef::from(&listener).shutdown(std::net::Shutdown::Both);
992
993 let mut rebinding = RebindingTcpListener::new(listener, address, Duration::from_millis(10));
994
995 if let Err(error) = shutdown_result {
996 eprintln!("skipping: this platform refused shutdown on a listening socket: {error}");
997 return;
998 }
999
1000 let accepted = tokio::spawn(async move { rebinding.accept().await });
1001
1002 let mut accepted = accepted;
1003 let mut clients = Vec::new();
1004 let peer = tokio::time::timeout(Duration::from_secs(5), async {
1005 loop {
1006 let Ok(stream) = tokio::net::TcpStream::connect(address).await else {
1007 tokio::time::sleep(Duration::from_millis(10)).await;
1008 continue;
1009 };
1010 let client_addr = stream.local_addr().unwrap();
1011 clients.push((stream, client_addr));
1012 tokio::select! {
1013 result = &mut accepted => {
1014 let (_io, peer) = result.expect("accept task should not panic");
1015 return peer;
1016 }
1017 _ = tokio::time::sleep(Duration::from_millis(50)) => {}
1018 }
1019 }
1020 })
1021 .await
1022 .expect("listener should rebind and accept connections again");
1023
1024 assert!(
1025 clients.iter().any(|(_, client_addr)| *client_addr == peer),
1026 "accepted connection should be one we opened"
1027 );
1028 }
1029
1030 #[cfg(unix)]
1031 #[tokio::test]
1032 async fn test_rebinding_listener_keeps_trying_after_a_rebind_fails() {
1033 let holder = TcpListener::bind("127.0.0.1:0").await.unwrap();
1034 let contested_address = holder.local_addr().unwrap();
1035
1036 let broken = TcpListener::bind("127.0.0.1:0").await.unwrap();
1037 let shutdown_result = socket2::SockRef::from(&broken).shutdown(std::net::Shutdown::Both);
1038
1039 let mut rebinding =
1040 RebindingTcpListener::new(broken, contested_address, Duration::from_millis(10));
1041
1042 if let Err(error) = shutdown_result {
1043 eprintln!("skipping: this platform refused shutdown on a listening socket: {error}");
1044 return;
1045 }
1046
1047 let accepted = tokio::spawn(async move { rebinding.accept().await });
1048
1049 tokio::time::sleep(Duration::from_millis(100)).await;
1050 drop(holder);
1051
1052 let client = tokio::time::timeout(Duration::from_secs(5), async {
1053 loop {
1054 if let Ok(stream) = tokio::net::TcpStream::connect(contested_address).await {
1055 return stream;
1056 }
1057 tokio::time::sleep(Duration::from_millis(10)).await;
1058 }
1059 })
1060 .await
1061 .expect("listener should rebind once the address is free again");
1062
1063 let (_io, peer) = tokio::time::timeout(Duration::from_secs(5), accepted)
1064 .await
1065 .expect("accept should return once a later rebind succeeds")
1066 .expect("accept task should not panic");
1067
1068 assert_eq!(
1069 peer,
1070 client.local_addr().unwrap(),
1071 "accepted connection should be the one we opened"
1072 );
1073 }
1074
1075 #[cfg(unix)]
1076 #[test]
1077 fn test_resource_exhaustion_does_not_look_like_a_dead_listener() {
1078 for code in [libc::ENFILE, libc::ENOBUFS] {
1079 let error = io::Error::from_raw_os_error(code);
1080 assert!(
1081 is_resource_exhaustion_error(&error),
1082 "errno {code} should be treated as a process resource shortage, got {:?}",
1083 error.kind()
1084 );
1085 assert!(
1086 !is_connection_error(&error),
1087 "errno {code} is not a dead connection"
1088 );
1089 }
1090 }
1091
1092 #[cfg(target_os = "linux")]
1093 #[test]
1094 fn test_pending_network_errors_leave_the_listener_usable() {
1095 for code in [
1096 libc::ENETDOWN,
1097 libc::EPROTO,
1098 libc::ENOPROTOOPT,
1099 libc::EHOSTDOWN,
1100 libc::ENONET,
1101 libc::EHOSTUNREACH,
1102 libc::EOPNOTSUPP,
1103 libc::ENETUNREACH,
1104 ] {
1105 let error = io::Error::from_raw_os_error(code);
1106 assert!(
1107 is_connection_error(&error),
1108 "errno {code} should retry accept"
1109 );
1110 }
1111 }
1112
1113 #[tokio::test]
1114 async fn test_rebinding_listener_shutdown_interrupts_failed_rebinds() {
1115 let holder = TcpListener::bind("127.0.0.1:0").await.unwrap();
1116 let listener = RebindingTcpListener {
1117 address: holder.local_addr().unwrap(),
1118 listener: None,
1119 rebind_backoff: Duration::from_secs(60),
1120 };
1121 let cancel_token = CancellationToken::new();
1122 let observer = cancel_token.clone();
1123 let server = tokio::spawn(async move {
1124 axum::serve(listener, Router::new())
1125 .with_graceful_shutdown(observer.cancelled_owned())
1126 .await
1127 .unwrap();
1128 });
1129 tokio::time::sleep(Duration::from_millis(50)).await;
1130 assert!(
1131 !server.is_finished(),
1132 "server should keep retrying the held address"
1133 );
1134 cancel_token.cancel();
1135 tokio::time::timeout(Duration::from_secs(1), server)
1136 .await
1137 .expect("shutdown should interrupt the 60-second rebind backoff")
1138 .expect("server task should not panic");
1139 }
1140
1141 #[cfg(target_os = "linux")]
1142 #[tokio::test]
1143 async fn test_rebinding_listener_preserves_backlog_during_resource_exhaustion() {
1144 const CHILD_ENV: &str = "DYNAMO_TEST_LISTENER_EMFILE_CHILD";
1145 if std::env::var_os(CHILD_ENV).is_none() {
1146 let output = tokio::time::timeout(
1148 Duration::from_secs(15),
1149 tokio::process::Command::new(std::env::current_exe().unwrap())
1150 .args([
1151 "--exact",
1152 "system_status_server::tests::test_rebinding_listener_preserves_backlog_during_resource_exhaustion",
1153 "--nocapture",
1154 ])
1155 .env(CHILD_ENV, "1")
1156 .kill_on_drop(true)
1157 .output(),
1158 )
1159 .await
1160 .expect("descriptor-exhaustion child should finish promptly")
1161 .expect("test child should start");
1162 assert!(
1163 output.status.success(),
1164 "descriptor-exhaustion child failed:\n{}\n{}",
1165 String::from_utf8_lossy(&output.stdout),
1166 String::from_utf8_lossy(&output.stderr),
1167 );
1168 assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
1169 return;
1170 }
1171
1172 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1173 let address = listener.local_addr().unwrap();
1174 let client = tokio::net::TcpStream::connect(address).await.unwrap();
1175 let peer = client.local_addr().unwrap();
1176
1177 let mut limit = libc::rlimit {
1178 rlim_cur: 0,
1179 rlim_max: 0,
1180 };
1181 assert_eq!(
1183 unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) },
1184 0
1185 );
1186 limit.rlim_cur = limit.rlim_cur.min(96);
1187 assert_eq!(unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) }, 0);
1189
1190 let mut descriptors = Vec::new();
1191 loop {
1192 match std::fs::File::open("/dev/null") {
1193 Ok(file) => descriptors.push(file),
1194 Err(error) => {
1195 assert_eq!(error.raw_os_error(), Some(libc::EMFILE));
1196 break;
1197 }
1198 }
1199 }
1200 let error = listener.accept().await.unwrap_err();
1201 assert_eq!(error.raw_os_error(), Some(libc::EMFILE));
1202
1203 let backoff = Duration::from_millis(200);
1204 let mut rebinding = RebindingTcpListener::new(listener, address, backoff);
1205 let started = tokio::time::Instant::now();
1206 let accepted = rebinding.accept();
1207 tokio::pin!(accepted);
1208 assert!(
1209 tokio::time::timeout(Duration::from_millis(50), accepted.as_mut())
1210 .await
1211 .is_err(),
1212 "accept should wait during descriptor exhaustion"
1213 );
1214 drop(descriptors);
1215
1216 let _control = TcpListener::bind("127.0.0.1:0").await.unwrap();
1218 assert_eq!(
1219 TcpListener::bind(address).await.unwrap_err().kind(),
1220 io::ErrorKind::AddrInUse,
1221 "the original listening address should remain held"
1222 );
1223 let (_stream, accepted_peer) = tokio::time::timeout(Duration::from_secs(2), accepted)
1224 .await
1225 .expect("the queued connection should survive the shortage");
1226 assert!(
1227 started.elapsed() >= backoff,
1228 "accept should finish its resource-shortage backoff before retrying"
1229 );
1230 assert_eq!(accepted_peer, peer);
1231 }
1232}
1233
1234#[cfg(all(test, feature = "integration"))]
1236mod integration_tests {
1237 use super::*;
1238 use crate::config::environment_names::logging as env_logging;
1239 use crate::config::environment_names::runtime::canary as env_canary;
1240 use crate::distributed::distributed_test_utils::create_test_drt_async;
1241 use crate::metrics::MetricsHierarchy;
1242 use anyhow::Result;
1243 use rstest::rstest;
1244 use std::sync::Arc;
1245 use tokio::time::Duration;
1246
1247 #[tokio::test]
1248 async fn test_uptime_from_system_health() {
1249 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1251 let drt = create_test_drt_async().await;
1252
1253 let uptime = drt.system_health().lock().uptime();
1255 assert!(uptime.as_nanos() > 0 || uptime.is_zero());
1257
1258 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1260 let uptime_after = drt.system_health().lock().uptime();
1261 assert!(uptime_after > uptime);
1262 })
1263 .await;
1264 }
1265
1266 #[tokio::test]
1267 async fn test_runtime_metrics_initialization_and_namespace() {
1268 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1270 let drt = create_test_drt_async().await;
1271 let response = drt.metrics().prometheus_expfmt().unwrap();
1276 println!("Full metrics response:\n{}", response);
1277
1278 assert!(
1280 response.contains("# HELP dynamo_component_uptime_seconds"),
1281 "Should contain uptime_seconds help text"
1282 );
1283 assert!(
1284 response.contains("# TYPE dynamo_component_uptime_seconds gauge"),
1285 "Should contain uptime_seconds type"
1286 );
1287 assert!(
1288 response.contains("dynamo_component_uptime_seconds"),
1289 "Should contain uptime_seconds metric with correct namespace"
1290 );
1291 })
1292 .await;
1293 }
1294
1295 #[tokio::test]
1296 async fn test_uptime_gauge_updates() {
1297 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1299 let drt = create_test_drt_async().await;
1300
1301 let initial_uptime = drt.system_health().lock().uptime();
1303
1304 drt.system_health().lock().update_uptime_gauge();
1306
1307 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1309
1310 let uptime_after_sleep = drt.system_health().lock().uptime();
1312
1313 drt.system_health().lock().update_uptime_gauge();
1315
1316 let elapsed = uptime_after_sleep - initial_uptime;
1318 assert!(
1319 elapsed >= std::time::Duration::from_millis(100),
1320 "Uptime should have increased by at least 100ms after sleep, but only increased by {:?}",
1321 elapsed
1322 );
1323 })
1324 .await;
1325 }
1326
1327 #[tokio::test]
1328 async fn test_http_requests_fail_when_system_disabled() {
1329 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1331 let drt = create_test_drt_async().await;
1332
1333 let system_info = drt.system_status_server_info();
1335 assert!(
1336 system_info.is_none(),
1337 "System status server should not be running when disabled"
1338 );
1339
1340 println!("✓ System status server correctly disabled when not enabled");
1341 })
1342 .await;
1343 }
1344
1345 #[rstest]
1352 #[case("ready", 200, "ready", None, None, 3)]
1353 #[case("notready", 503, "notready", None, None, 3)]
1354 #[case("ready", 200, "ready", Some("/custom/health"), Some("/custom/live"), 5)]
1355 #[case(
1356 "notready",
1357 503,
1358 "notready",
1359 Some("/custom/health"),
1360 Some("/custom/live"),
1361 5
1362 )]
1363 #[tokio::test]
1364 #[cfg(feature = "integration")]
1365 async fn test_health_endpoints(
1366 #[case] starting_health_status: &'static str,
1367 #[case] expected_status: u16,
1368 #[case] expected_body: &'static str,
1369 #[case] custom_health_path: Option<&'static str>,
1370 #[case] custom_live_path: Option<&'static str>,
1371 #[case] expected_num_tests: usize,
1372 ) {
1373 use std::sync::Arc;
1374 crate::logging::init();
1380
1381 #[allow(clippy::redundant_closure_call)]
1382 temp_env::async_with_vars(
1383 [
1384 (env_system::DYN_SYSTEM_PORT, Some("0")),
1385 (
1386 env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS,
1387 Some(starting_health_status),
1388 ),
1389 (env_system::DYN_SYSTEM_HEALTH_PATH, custom_health_path),
1390 (env_system::DYN_SYSTEM_LIVE_PATH, custom_live_path),
1391 ],
1392 (async || {
1393 let drt = Arc::new(create_test_drt_async().await);
1394
1395 let system_info = drt
1397 .system_status_server_info()
1398 .expect("System status server should be started by DRT");
1399 let addr = system_info.socket_addr;
1400
1401 let client = reqwest::Client::new();
1402
1403 let mut test_cases = vec![];
1405 match custom_health_path {
1406 None => {
1407 test_cases.push(("/health", expected_status, expected_body));
1409 }
1410 Some(chp) => {
1411 test_cases.push(("/health", 404, "Route not found"));
1413 test_cases.push((chp, expected_status, expected_body));
1414 }
1415 }
1416 match custom_live_path {
1417 None => {
1418 test_cases.push(("/live", expected_status, expected_body));
1420 }
1421 Some(clp) => {
1422 test_cases.push(("/live", 404, "Route not found"));
1424 test_cases.push((clp, expected_status, expected_body));
1425 }
1426 }
1427 test_cases.push(("/someRandomPathNotFoundHere", 404, "Route not found"));
1428 assert_eq!(test_cases.len(), expected_num_tests);
1429
1430 for (path, expect_status, expect_body) in test_cases {
1431 println!("[test] Sending request to {}", path);
1432 let url = format!("http://{}{}", addr, path);
1433 let response = client.get(&url).send().await.unwrap();
1434 let status = response.status();
1435 let body = response.text().await.unwrap();
1436 println!(
1437 "[test] Response for {}: status={}, body={:?}",
1438 path, status, body
1439 );
1440 assert_eq!(
1441 status, expect_status,
1442 "Response: status={}, body={:?}",
1443 status, body
1444 );
1445 assert!(
1446 body.contains(expect_body),
1447 "Response: status={}, body={:?}",
1448 status,
1449 body
1450 );
1451 }
1452 })(),
1453 )
1454 .await;
1455 }
1456
1457 #[tokio::test]
1458 async fn test_health_endpoint_tracing() -> Result<()> {
1459 use std::sync::Arc;
1460
1461 #[allow(clippy::redundant_closure_call)]
1464 let _ = temp_env::async_with_vars(
1465 [
1466 (env_system::DYN_SYSTEM_PORT, Some("0")),
1467 (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("ready")),
1468 (env_logging::DYN_LOGGING_JSONL, Some("1")),
1469 (env_logging::DYN_LOG, Some("trace")),
1470 ],
1471 (async || {
1472 crate::logging::init();
1476
1477 let drt = Arc::new(create_test_drt_async().await);
1478
1479 let system_info = drt
1481 .system_status_server_info()
1482 .expect("System status server should be started by DRT");
1483 let addr = system_info.socket_addr;
1484 let client = reqwest::Client::new();
1485 for path in [("/health"), ("/live"), ("/someRandomPathNotFoundHere")] {
1486 let traceparent_value =
1487 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
1488 let tracestate_value = "vendor1=opaqueValue1,vendor2=opaqueValue2";
1489 let mut headers = reqwest::header::HeaderMap::new();
1490 headers.insert(
1491 reqwest::header::HeaderName::from_static("traceparent"),
1492 reqwest::header::HeaderValue::from_str(traceparent_value)?,
1493 );
1494 headers.insert(
1495 reqwest::header::HeaderName::from_static("tracestate"),
1496 reqwest::header::HeaderValue::from_str(tracestate_value)?,
1497 );
1498 let url = format!("http://{}{}", addr, path);
1499 let response = client.get(&url).headers(headers).send().await.unwrap();
1500 let status = response.status();
1501 let body = response.text().await.unwrap();
1502 tracing::info!(body = body, status = status.to_string());
1503 }
1504
1505 Ok::<(), anyhow::Error>(())
1506 })(),
1507 )
1508 .await;
1509 Ok(())
1510 }
1511
1512 #[tokio::test]
1513 async fn test_health_endpoint_with_changing_health_status() {
1514 const ENDPOINT_NAME: &str = "generate";
1517 const ENDPOINT_HEALTH_CONFIG: &str = "[\"generate\"]";
1518 temp_env::async_with_vars(
1519 [
1520 (env_system::DYN_SYSTEM_PORT, Some("0")),
1521 (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("notready")),
1522 (env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS, Some(ENDPOINT_HEALTH_CONFIG)),
1523 ],
1524 async {
1525 let drt = Arc::new(create_test_drt_async().await);
1526
1527 let system_info_opt = drt.system_status_server_info();
1529
1530 assert!(
1532 system_info_opt.is_some(),
1533 "System status server was not spawned by DRT. Expected DRT to spawn server when DYN_SYSTEM_PORT is set to a positive value, but system_status_server_info() returned None. Environment: DYN_SYSTEM_PORT={:?}",
1534 std::env::var(env_system::DYN_SYSTEM_PORT)
1535 );
1536
1537 let system_info = system_info_opt.unwrap();
1539 let addr = system_info.socket_addr;
1540
1541 let client = reqwest::Client::new();
1543 let health_url = format!("http://{}/health", addr);
1544
1545 let response = client.get(&health_url).send().await.unwrap();
1546 let status = response.status();
1547 let body = response.text().await.unwrap();
1548
1549 assert_eq!(status, 503, "Health should be 503 (not ready) initially, got: {}", status);
1551 assert!(body.contains("\"status\":\"notready\""), "Health should contain status notready");
1552
1553 let namespace = drt.namespace("ns1234").unwrap();
1555 let component = namespace.component("comp1234").unwrap();
1556
1557 use crate::pipeline::{async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, SingleIn};
1559 use crate::protocols::annotated::Annotated;
1560
1561 struct TestHandler;
1562
1563 #[async_trait]
1564 impl AsyncEngine<SingleIn<String>, ManyOut<Annotated<String>>, anyhow::Error> for TestHandler {
1565 async fn generate(&self, input: SingleIn<String>) -> anyhow::Result<ManyOut<Annotated<String>>> {
1566 let (data, ctx) = input.into_parts();
1567 let response = Annotated::from_data(format!("You responded: {}", data));
1568 Ok(crate::pipeline::ResponseStream::new(
1569 Box::pin(crate::stream::iter(vec![response])),
1570 ctx.context()
1571 ))
1572 }
1573 }
1574
1575 let ingress = Ingress::for_engine(std::sync::Arc::new(TestHandler)).unwrap();
1577
1578 tokio::spawn(async move {
1581 let _ = component.endpoint(ENDPOINT_NAME)
1582 .endpoint_builder()
1583 .handler(ingress)
1584 .health_check_payload(serde_json::json!({
1585 "test": "health_check"
1586 }))
1587 .start()
1588 .await;
1589 });
1590
1591 let mut success_count = 0;
1593 let mut failures = Vec::new();
1594
1595 for i in 1..=200 {
1596 let response = client.get(&health_url).send().await.unwrap();
1597 let status = response.status();
1598 let body = response.text().await.unwrap();
1599
1600 if status == 200 && body.contains("\"status\":\"ready\"") {
1601 success_count += 1;
1602 } else {
1603 failures.push((i, status.as_u16(), body.clone()));
1604 if failures.len() <= 5 { tracing::warn!("Request {}: status={}, body={}", i, status, body);
1606 }
1607 }
1608 }
1609
1610 tracing::info!("Health endpoint test results: {success_count}/200 requests succeeded");
1611 if !failures.is_empty() {
1612 tracing::warn!("Failed requests: {}", failures.len());
1613 }
1614
1615 assert!(success_count >= 150, "Expected at least 150 out of 200 requests to succeed, but only {} succeeded", success_count);
1617 },
1618 )
1619 .await;
1620 }
1621
1622 #[tokio::test]
1623 async fn test_spawn_system_status_server_endpoints() {
1624 temp_env::async_with_vars(
1626 [
1627 (env_system::DYN_SYSTEM_PORT, Some("0")),
1628 (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("ready")),
1629 ],
1630 async {
1631 let drt = Arc::new(create_test_drt_async().await);
1632
1633 let system_info = drt
1635 .system_status_server_info()
1636 .expect("System status server should be started by DRT");
1637 let addr = system_info.socket_addr;
1638 let client = reqwest::Client::new();
1639 for (path, expect_200, expect_body) in [
1640 ("/health", true, "ready"),
1641 ("/live", true, "ready"),
1642 ("/someRandomPathNotFoundHere", false, "Route not found"),
1643 ] {
1644 println!("[test] Sending request to {}", path);
1645 let url = format!("http://{}{}", addr, path);
1646 let response = client.get(&url).send().await.unwrap();
1647 let status = response.status();
1648 let body = response.text().await.unwrap();
1649 println!(
1650 "[test] Response for {}: status={}, body={:?}",
1651 path, status, body
1652 );
1653 if expect_200 {
1654 assert_eq!(status, 200, "Response: status={}, body={:?}", status, body);
1655 } else {
1656 assert_eq!(status, 404, "Response: status={}, body={:?}", status, body);
1657 }
1658 assert!(
1659 body.contains(expect_body),
1660 "Response: status={}, body={:?}",
1661 status,
1662 body
1663 );
1664 }
1665 },
1667 )
1668 .await;
1669 }
1670
1671 #[tokio::test]
1672 async fn test_payloadless_endpoint_is_healthy_with_canary_enabled() {
1673 temp_env::async_with_vars(
1674 [
1675 (env_system::DYN_SYSTEM_PORT, Some("0")),
1676 (
1677 env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS,
1678 Some("notready"),
1679 ),
1680 ("DYN_HEALTH_CHECK_ENABLED", Some("true")),
1681 ],
1682 async {
1683 let runtime = crate::Runtime::from_current().unwrap();
1684 let drt = Arc::new(
1685 crate::DistributedRuntime::new(
1686 runtime,
1687 crate::distributed::DistributedConfig::process_local(),
1688 )
1689 .await
1690 .unwrap(),
1691 );
1692 let addr = drt
1693 .system_status_server_info()
1694 .expect("System status server should be started")
1695 .socket_addr;
1696
1697 use crate::pipeline::{
1698 AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, SingleIn, async_trait,
1699 network::Ingress,
1700 };
1701 use crate::protocols::annotated::Annotated;
1702
1703 struct PayloadlessHandler;
1704
1705 #[async_trait]
1706 impl AsyncEngine<SingleIn<String>, ManyOut<Annotated<String>>, anyhow::Error>
1707 for PayloadlessHandler
1708 {
1709 async fn generate(
1710 &self,
1711 input: SingleIn<String>,
1712 ) -> anyhow::Result<ManyOut<Annotated<String>>> {
1713 let (data, ctx) = input.into_parts();
1714 Ok(crate::pipeline::ResponseStream::new(
1715 Box::pin(crate::stream::iter(vec![Annotated::from_data(data)])),
1716 ctx.context(),
1717 ))
1718 }
1719 }
1720
1721 let namespace = drt.namespace("test").unwrap();
1722 let component = namespace.component("backend").unwrap();
1723 let ingress = Ingress::for_engine(Arc::new(PayloadlessHandler)).unwrap();
1724 tokio::spawn(async move {
1725 let _ = component
1728 .endpoint("generate")
1729 .endpoint_builder()
1730 .handler(ingress)
1731 .start()
1732 .await;
1733 });
1734
1735 let client = reqwest::Client::new();
1736 for path in ["/health", "/live"] {
1737 let url = format!("http://{addr}{path}");
1738 let mut last_response = None;
1739 for _ in 0..50 {
1740 let response = client.get(&url).send().await.unwrap();
1741 let status = response.status();
1742 let body = response.text().await.unwrap();
1743 if status == 200 && body.contains("\"status\":\"ready\"") {
1744 last_response = Some((status, body));
1745 break;
1746 }
1747 last_response = Some((status, body));
1748 tokio::time::sleep(Duration::from_millis(20)).await;
1749 }
1750 let (status, body) = last_response.unwrap();
1751 assert_eq!(status, 200, "{path} response: {body}");
1752 assert!(
1753 body.contains("\"status\":\"ready\""),
1754 "{path} response: {body}"
1755 );
1756 }
1757 },
1758 )
1759 .await;
1760 }
1761
1762 #[cfg(feature = "integration")]
1763 #[tokio::test]
1764 async fn test_health_check_with_payload_and_timeout() {
1765 crate::logging::init();
1767
1768 temp_env::async_with_vars(
1769 [
1770 (env_system::DYN_SYSTEM_PORT, Some("0")),
1771 (
1772 env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS,
1773 Some("notready"),
1774 ),
1775 (
1776 env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS,
1777 Some("[\"test.endpoint\"]"),
1778 ),
1779 ("DYN_HEALTH_CHECK_ENABLED", Some("true")),
1781 (env_canary::DYN_CANARY_WAIT_TIME, Some("1")), ("DYN_HEALTH_CHECK_REQUEST_TIMEOUT", Some("1")), ("RUST_LOG", Some("info")), ],
1785 async {
1786 let drt = Arc::new(create_test_drt_async().await);
1787
1788 let system_info = drt
1790 .system_status_server_info()
1791 .expect("System status server should be started");
1792 let addr = system_info.socket_addr;
1793
1794 let client = reqwest::Client::new();
1795 let health_url = format!("http://{}/health", addr);
1796
1797 let endpoint = "test.endpoint";
1799 let health_check_payload = serde_json::json!({
1800 "prompt": "health check test",
1801 "_health_check": true
1802 });
1803
1804 {
1806 let system_health = drt.system_health();
1807 let system_health_lock = system_health.lock();
1808 system_health_lock.register_health_check_target(
1809 endpoint,
1810 crate::component::Instance {
1811 component: "test_component".to_string(),
1812 endpoint: "health".to_string(),
1813 namespace: "test_namespace".to_string(),
1814 instance_id: 1,
1815 transport: crate::component::TransportType::Nats(endpoint.to_string()),
1816 device_type: None,
1817 request_plane_codec: None,
1818 },
1819 health_check_payload.clone(),
1820 );
1821 }
1822
1823 let response = client.get(&health_url).send().await.unwrap();
1825 let status = response.status();
1826 let body = response.text().await.unwrap();
1827 assert_eq!(status, 503, "Should be unhealthy initially (default state)");
1828 assert!(
1829 body.contains("\"status\":\"notready\""),
1830 "Should show notready status initially"
1831 );
1832
1833 drt.system_health()
1835 .lock()
1836 .set_endpoint_health_status(endpoint, HealthStatus::Ready);
1837
1838 let response = client.get(&health_url).send().await.unwrap();
1840 let status = response.status();
1841 let body = response.text().await.unwrap();
1842
1843 assert_eq!(status, 200, "Should be healthy due to recent response");
1844 assert!(
1845 body.contains("\"status\":\"ready\""),
1846 "Should show ready status after response"
1847 );
1848
1849 let endpoint_status = drt
1851 .system_health()
1852 .lock()
1853 .get_endpoint_health_status(endpoint);
1854 assert_eq!(
1855 endpoint_status,
1856 Some(HealthStatus::Ready),
1857 "SystemHealth should show endpoint as Ready after response"
1858 );
1859 },
1860 )
1861 .await;
1862 }
1863
1864 #[tokio::test]
1869 async fn test_call_lora_endpoint_resolves_via_engine_routes() {
1870 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1871 let drt = create_test_drt_async().await;
1872
1873 let callback: crate::engine_routes::EngineRouteCallback = Arc::new(|_body| {
1874 Box::pin(async move {
1875 Ok(serde_json::json!({
1876 "status": "success",
1877 "lora_name": "adapterA",
1878 "lora_id": 42,
1879 }))
1880 })
1881 });
1882 drt.engine_routes().register("update/load_lora", callback);
1884
1885 assert!(drt.local_endpoint_registry().get("load_lora").is_none());
1888
1889 let response = call_lora_endpoint(
1890 &drt,
1891 "load_lora",
1892 serde_json::json!({"lora_name": "adapterA"}),
1893 )
1894 .await
1895 .expect("engine_routes fallback should resolve the control");
1896
1897 assert_eq!(response.status, "success");
1898 assert_eq!(response.lora_name.as_deref(), Some("adapterA"));
1899 assert_eq!(response.lora_id, Some(42));
1900 })
1901 .await;
1902 }
1903
1904 #[tokio::test]
1908 async fn test_call_lora_endpoint_missing_returns_clean_error() {
1909 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1910 let drt = create_test_drt_async().await;
1911
1912 let err = call_lora_endpoint(&drt, "load_lora", serde_json::json!({}))
1913 .await
1914 .expect_err("missing handler must error");
1915
1916 assert!(
1917 err.to_string().contains("LoRA management not available"),
1918 "expected explicit unavailable message, got: {err}"
1919 );
1920 })
1921 .await;
1922 }
1923
1924 #[tokio::test]
1929 async fn test_call_lora_endpoint_propagates_error_status() {
1930 temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1931 let drt = create_test_drt_async().await;
1932
1933 let callback: crate::engine_routes::EngineRouteCallback = Arc::new(|_body| {
1934 Box::pin(async move {
1935 Ok(serde_json::json!({
1936 "status": "error",
1937 "message": "adapter not found",
1938 }))
1939 })
1940 });
1941 drt.engine_routes().register("update/unload_lora", callback);
1943
1944 let response = call_lora_endpoint(&drt, "unload_lora", serde_json::json!({}))
1945 .await
1946 .expect("a non-raising callback returns Ok even on logical error");
1947
1948 assert_eq!(response.status, "error");
1949 assert_eq!(response.message.as_deref(), Some("adapter not found"));
1950 })
1951 .await;
1952 }
1953}