Skip to main content

dynamo_runtime/
system_status_server.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4// TODO: (DEP-635) this file should be renamed to system_http_server.rs
5//  it is being used not just for status, health, but others like loras management.
6
7#[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/// Selects probe semantics for the runtime-owned HTTP server.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum SystemProbePolicy {
43    /// Preserve the configured worker health handler for both probes.
44    #[default]
45    Worker,
46    /// Static liveness and readiness based on runtime dependencies, not the engine.
47    RuntimeOnly,
48}
49
50/// System status server information containing socket address and handle
51#[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    /// Return an address that is served by the bound socket.
78    ///
79    /// A wildcard bind is replaced with a usable address from the same address
80    /// family. If interface enumeration fails, the same-family loopback is
81    /// used so IPv4 and IPv6 are never combined.
82    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
130/// System status server state containing the distributed runtime reference
131pub struct SystemStatusState {
132    // global drt registry is for printing out the entire Prometheus format output
133    root_drt: Arc<crate::DistributedRuntime>,
134    // Discovery metadata (only for Kubernetes backend)
135    discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
136}
137
138impl SystemStatusState {
139    /// Create new system status server state with the provided distributed runtime
140    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    /// Get a reference to the distributed runtime
151    pub fn drt(&self) -> &crate::DistributedRuntime {
152        &self.root_drt
153    }
154
155    /// Get a reference to the discovery metadata if available
156    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/// Request body for POST /v1/loras
164#[derive(Debug, Clone, Deserialize, Serialize)]
165pub struct LoadLoraRequest {
166    pub lora_name: String,
167    pub source: LoraSource,
168}
169
170/// Source information for loading a LoRA
171#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct LoraSource {
173    pub uri: String,
174}
175
176/// Response body for LoRA operations
177#[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
194/// Start the complete system HTTP server after runtime initialization.
195pub 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    // Create system status server state with the provided distributed runtime
204    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    // Reuse the concrete address so an ephemeral port remains stable across rebinds.
218    let listener =
219        RebindingTcpListener::new(listener, actual_address, SYSTEM_STATUS_REBIND_BACKOFF);
220    let observer = cancel_token.child_token();
221
222    // Spawn the server in the background and return the handle
223    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    // Check if LoRA feature is enabled
274    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    // Add LoRA routes only if DYN_LORA_ENABLED is set to true
301    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    // Self-hosted MDC files. Always mounted; empty registry → 404.
324    // The endpoint triple disambiguates multi-LocalModel-per-DRT; the
325    // suffix segment (LoRA slug or `_base`) scopes per-registration so
326    // detaching one doesn't wipe another's entries.
327    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
353/// Axum's [`Listener::accept`] cannot return an error, so recovery must happen here.
354struct RebindingTcpListener {
355    /// Concrete address returned by the initial bind, including an assigned ephemeral port.
356    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                // Connection errors do not invalidate the listener.
402                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                // Resource exhaustion does not invalidate the listener.
410                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                    // Drop before rebinding or the address remains in use.
426                    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    // Linux accept(2) also reports pending network errors for the queued connection.
448    #[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
466/// EMFILE, ENFILE, and ENOBUFS require raw errno checks because Rust has no distinct kinds.
467fn 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/// Health handler with optional active health checking
483#[tracing::instrument(skip_all, level = "trace")]
484async fn health_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
485    // Get basic health status
486    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
510/// Sidecar readiness is independent of engine health and model registration.
511async 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/// Metrics handler with DistributedRuntime uptime
525#[tracing::instrument(skip_all, level = "trace")]
526async fn metrics_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
527    // Get all metrics from the DistributedRuntime.
528    // The uptime gauge is updated automatically via a PrometheusUpdateCallback
529    // registered in DistributedRuntime::new(), so it is always fresh before scrape.
530    //
531    // NOTE: We use a multi-registry model (e.g. one registry per endpoint) and merge at scrape time,
532    // so /metrics traverses registered child registries and produces a single combined output.
533    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/// Metadata handler
548#[tracing::instrument(skip_all, level = "trace")]
549async fn metadata_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
550    // Check if discovery metadata is available
551    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    // Read the metadata
564    let metadata_guard = metadata.read().await;
565
566    // Serialize to JSON
567    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/// Handler for POST /v1/loras - Load a LoRA adapter
584#[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    // Call the load_lora endpoint for each available backend
592    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/// Handler for DELETE /v1/loras/*lora_name - Unload a LoRA adapter
635#[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    // Strip the leading slash from the wildcard capture
641    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    // Call the unload_lora endpoint for each available backend
648    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/// Handler for GET /v1/loras - List all LoRA adapters
688#[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    // Call the list_loras endpoint for each available backend
693    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
715/// `GET /v1/metadata/{namespace}/{component}/{endpoint}/{slug}/{suffix}/{filename}`
716/// — 404 on miss, 500 on read error, raw bytes on hit. Consumer blake3-verifies.
717async 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
770/// Helper function to call a LoRA management endpoint for the local worker.
771///
772/// Resolution order (both are in-process, never network discovery):
773/// 1. The legacy local endpoint registry, populated by non-unified workers via
774///    `.register_local_engine()`.
775/// 2. The generic engine-route registry (`/engine/*`). Unified-backend workers
776///    advertise LoRA lifecycle ops (`load_lora`/`unload_lora`/`list_loras`) as
777///    engine *updates*, registered under `update/<name>`; this fallback maps the
778///    bare LoRA name onto that key so the legacy `/v1/loras` surface forwards to
779///    them (the `/v1/loras` compatibility shim).
780///
781/// Because legacy workers populate the local registry, they never reach the
782/// fallback — their `/v1/loras` behavior is unchanged. If neither registry
783/// holds the name, returns an explicit "LoRA management not available" error
784/// rather than an opaque "endpoint not found".
785async 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    // 1. Legacy local registry (in-process call only).
795    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    // 2. Unified-backend engine-update registry fallback. The unified Worker
815    //    registers LoRA ops as engine updates under `update/<name>`, so map the
816    //    bare LoRA endpoint name onto that namespaced key.
817    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
838/// Helper to parse response data into LoraResponse
839fn 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/// Engine route handler for /engine/* routes
864///
865/// This handler looks up registered callbacks in the engine routes registry
866/// and invokes them with the request body, returning the response as JSON.
867#[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    // Parse body as JSON (empty object for GET/empty body)
876    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    // Look up callback
897    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    // Call callback (it's async, so await it)
914    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// Regular tests: cargo test system_status_server --lib
935#[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    // This is a basic test to verify the HTTP server is working before testing other more complicated tests
956    #[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        // Test basic HTTP server lifecycle without DistributedRuntime
962        let app = Router::new().route("/test", get(|| async { (StatusCode::OK, "test") }));
963
964        // start HTTP server
965        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        // server starts immediately, no need to wait
973
974        // cancel token
975        cancel_token.cancel();
976
977        // wait for the server to shut down
978        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            // Descriptor limits are process-wide; isolate this test from sibling tests.
1147            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        // SAFETY: limit is writable and correctly aligned. Only this child is affected.
1182        assert_eq!(
1183            unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) },
1184            0
1185        );
1186        limit.rlim_cur = limit.rlim_cur.min(96);
1187        // SAFETY: limit is initialized; the soft limit is lowered without changing the hard limit.
1188        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        // A fresh bind rules out lingering descriptor pressure as the cause of AddrInUse.
1217        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// Integration tests: cargo test system_status_server --lib --features integration
1235#[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        // Test that uptime is available from SystemHealth
1250        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1251            let drt = create_test_drt_async().await;
1252
1253            // Get uptime from SystemHealth
1254            let uptime = drt.system_health().lock().uptime();
1255            // Uptime should exist (even if close to zero)
1256            assert!(uptime.as_nanos() > 0 || uptime.is_zero());
1257
1258            // Sleep briefly and check uptime increases
1259            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        // Test that metrics have correct namespace
1269        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1270            let drt = create_test_drt_async().await;
1271            // SystemStatusState is already created in distributed.rs
1272            // so we don't need to create it again here
1273
1274            // The uptime_seconds metric should already be registered and available
1275            let response = drt.metrics().prometheus_expfmt().unwrap();
1276            println!("Full metrics response:\n{}", response);
1277
1278            // Check that uptime_seconds metric is present with correct namespace
1279            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        // Test that the uptime gauge is properly updated and increases over time
1298        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1299            let drt = create_test_drt_async().await;
1300
1301            // Get initial uptime
1302            let initial_uptime = drt.system_health().lock().uptime();
1303
1304            // Update the gauge with initial value
1305            drt.system_health().lock().update_uptime_gauge();
1306
1307            // Sleep for 100ms
1308            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1309
1310            // Get uptime after sleep
1311            let uptime_after_sleep = drt.system_health().lock().uptime();
1312
1313            // Update the gauge again
1314            drt.system_health().lock().update_uptime_gauge();
1315
1316            // Verify uptime increased by at least 100ms
1317            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        // Test that system status server is not running when disabled
1330        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1331            let drt = create_test_drt_async().await;
1332
1333            // Verify that system status server info is None when disabled
1334            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    /// This test verifies the health and liveness endpoints of the system status server.
1346    /// It checks that the endpoints respond with the correct HTTP status codes and bodies
1347    /// based on the initial health status and any custom endpoint paths provided via environment variables.
1348    /// The test is parameterized using multiple #[case] attributes to cover various scenarios,
1349    /// including different initial health states ("ready" and "notready"), default and custom endpoint paths,
1350    /// and expected response codes and bodies.
1351    #[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        // use tokio::io::{AsyncReadExt, AsyncWriteExt};
1375        // use reqwest for HTTP requests
1376
1377        // Closure call is needed here to satisfy async_with_vars
1378
1379        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                // Get system status server info from DRT (instead of manually spawning)
1396                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                // Prepare test cases
1404                let mut test_cases = vec![];
1405                match custom_health_path {
1406                    None => {
1407                        // When using default paths, test the default paths
1408                        test_cases.push(("/health", expected_status, expected_body));
1409                    }
1410                    Some(chp) => {
1411                        // When using custom paths, default paths should not exist
1412                        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                        // When using default paths, test the default paths
1419                        test_cases.push(("/live", expected_status, expected_body));
1420                    }
1421                    Some(clp) => {
1422                        // When using custom paths, default paths should not exist
1423                        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        // Closure call is needed here to satisfy async_with_vars
1462
1463        #[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                // TODO Add proper testing for
1473                // trace id and parent id
1474
1475                crate::logging::init();
1476
1477                let drt = Arc::new(create_test_drt_async().await);
1478
1479                // Get system status server info from DRT (instead of manually spawning)
1480                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        // Test health endpoint starts in not ready status, then becomes ready
1515        // when endpoints are created (DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS=generate)
1516        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                // Check if system status server was started
1528                let system_info_opt = drt.system_status_server_info();
1529
1530                // Ensure system status server was spawned by DRT
1531                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                // Get the system status server info from DRT - this should never fail now due to above check
1538                let system_info = system_info_opt.unwrap();
1539                let addr = system_info.socket_addr;
1540
1541                // Initially check health - should be not ready
1542                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                // Health should be not ready (503) initially
1550                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                // Now create a namespace, component, and endpoint to make the system healthy
1554                let namespace = drt.namespace("ns1234").unwrap();
1555                let component = namespace.component("comp1234").unwrap();
1556
1557                // Create a simple test handler
1558                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                // Create the ingress and start the endpoint service
1576                let ingress = Ingress::for_engine(std::sync::Arc::new(TestHandler)).unwrap();
1577
1578                // Start the service and endpoint with a health check payload
1579                // This will automatically register the endpoint for health monitoring
1580                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                // Hit health endpoint 200 times to verify consistency
1592                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 {  // Only log first 5 failures
1605                            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                // Expect at least 150 out of 200 requests to be successful
1616                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        // use reqwest for HTTP requests
1625        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                // Get system status server info from DRT (instead of manually spawning)
1634                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                // DRT handles server cleanup automatically
1666            },
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                    // Unified decode workers deliberately follow this path: the
1726                    // endpoint is registered without a canary payload.
1727                    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        // Test the complete health check flow with the new canary-based system:
1766        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                // Enable health check with short intervals for testing
1780                ("DYN_HEALTH_CHECK_ENABLED", Some("true")),
1781                (env_canary::DYN_CANARY_WAIT_TIME, Some("1")), // Send canary after 1 second of inactivity
1782                ("DYN_HEALTH_CHECK_REQUEST_TIMEOUT", Some("1")), // Immediately timeout to mimic unresponsiveness
1783                ("RUST_LOG", Some("info")),                      // Enable logging for test
1784            ],
1785            async {
1786                let drt = Arc::new(create_test_drt_async().await);
1787
1788                // Get system status server info
1789                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                // Register an endpoint with health check payload
1798                let endpoint = "test.endpoint";
1799                let health_check_payload = serde_json::json!({
1800                    "prompt": "health check test",
1801                    "_health_check": true
1802                });
1803
1804                // Register the endpoint and its health check payload
1805                {
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                // Check initial health - should be ready (default state)
1824                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                // Set endpoint to healthy state
1834                drt.system_health()
1835                    .lock()
1836                    .set_endpoint_health_status(endpoint, HealthStatus::Ready);
1837
1838                // Check health again - should now be healthy
1839                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                // Verify the endpoint status in SystemHealth directly
1850                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    /// `/v1/loras` compat shim: with the legacy local registry empty, a LoRA
1865    /// update registered in `engine_routes()` under `update/<name>` resolves via
1866    /// the fallback and its JSON response is parsed into a `LoraResponse`. This
1867    /// is the path unified-backend workers take for `/v1/loras`.
1868    #[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            // Unified Worker registers LoRA ops under the `update/` namespace.
1883            drt.engine_routes().register("update/load_lora", callback);
1884
1885            // Local registry is empty, so resolution must fall through to
1886            // engine_routes.
1887            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    /// When neither the local registry nor `engine_routes()` holds the name,
1905    /// the caller gets an explicit "LoRA management not available" error
1906    /// rather than an opaque "endpoint not found".
1907    #[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    /// An update callback that returns `{"status":"error",...}` (rather than
1925    /// raising) surfaces as a `LoraResponse{status:"error"}`. The `/v1/loras`
1926    /// load/unload handlers map this to HTTP 500, preserving legacy semantics;
1927    /// the direct `/engine/*` route would instead return HTTP 200 + this JSON.
1928    #[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            // Unified Worker registers LoRA ops under the `update/` namespace.
1942            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}