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
7use crate::config::HealthStatus;
8use crate::config::environment_names::logging as env_logging;
9use crate::config::environment_names::runtime::canary as env_canary;
10use crate::config::environment_names::runtime::system as env_system;
11use crate::logging::make_system_request_span;
12use crate::metrics::MetricsHierarchy;
13use crate::traits::DistributedRuntimeProvider;
14use axum::{
15    Router,
16    body::Bytes,
17    extract::{Json, Path, State},
18    http::StatusCode,
19    response::IntoResponse,
20    routing::{any, delete, get, post},
21};
22use futures::StreamExt;
23use serde::{Deserialize, Serialize};
24use serde_json::json;
25use std::collections::HashMap;
26use std::sync::{Arc, OnceLock};
27use std::time::Instant;
28use tokio::{net::TcpListener, task::JoinHandle};
29use tokio_util::sync::CancellationToken;
30use tower_http::trace::TraceLayer;
31
32/// System status server information containing socket address and handle
33#[derive(Debug)]
34pub struct SystemStatusServerInfo {
35    pub socket_addr: std::net::SocketAddr,
36    pub handle: Option<Arc<JoinHandle<()>>>,
37}
38
39impl SystemStatusServerInfo {
40    pub fn new(socket_addr: std::net::SocketAddr, handle: Option<JoinHandle<()>>) -> Self {
41        Self {
42            socket_addr,
43            handle: handle.map(Arc::new),
44        }
45    }
46
47    pub fn address(&self) -> String {
48        self.socket_addr.to_string()
49    }
50
51    pub fn hostname(&self) -> String {
52        self.socket_addr.ip().to_string()
53    }
54
55    pub fn port(&self) -> u16 {
56        self.socket_addr.port()
57    }
58}
59
60impl Clone for SystemStatusServerInfo {
61    fn clone(&self) -> Self {
62        Self {
63            socket_addr: self.socket_addr,
64            handle: self.handle.clone(),
65        }
66    }
67}
68
69/// System status server state containing the distributed runtime reference
70pub struct SystemStatusState {
71    // global drt registry is for printing out the entire Prometheus format output
72    root_drt: Arc<crate::DistributedRuntime>,
73    // Discovery metadata (only for Kubernetes backend)
74    discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
75}
76
77impl SystemStatusState {
78    /// Create new system status server state with the provided distributed runtime
79    pub fn new(
80        drt: Arc<crate::DistributedRuntime>,
81        discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
82    ) -> anyhow::Result<Self> {
83        Ok(Self {
84            root_drt: drt,
85            discovery_metadata,
86        })
87    }
88
89    /// Get a reference to the distributed runtime
90    pub fn drt(&self) -> &crate::DistributedRuntime {
91        &self.root_drt
92    }
93
94    /// Get a reference to the discovery metadata if available
95    pub fn discovery_metadata(
96        &self,
97    ) -> Option<&Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>> {
98        self.discovery_metadata.as_ref()
99    }
100}
101
102/// Request body for POST /v1/loras
103#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct LoadLoraRequest {
105    pub lora_name: String,
106    pub source: LoraSource,
107}
108
109/// Source information for loading a LoRA
110#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct LoraSource {
112    pub uri: String,
113}
114
115/// Response body for LoRA operations
116#[derive(Debug, Clone, Deserialize, Serialize)]
117pub struct LoraResponse {
118    pub status: String,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub message: Option<String>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub lora_name: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub lora_id: Option<u64>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub loras: Option<serde_json::Value>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub count: Option<usize>,
129}
130
131/// Start system status server with metrics support
132pub async fn spawn_system_status_server(
133    host: &str,
134    port: u16,
135    cancel_token: CancellationToken,
136    drt: Arc<crate::DistributedRuntime>,
137    discovery_metadata: Option<Arc<tokio::sync::RwLock<crate::discovery::DiscoveryMetadata>>>,
138) -> anyhow::Result<(std::net::SocketAddr, tokio::task::JoinHandle<()>)> {
139    // Create system status server state with the provided distributed runtime
140    let server_state = Arc::new(SystemStatusState::new(drt, discovery_metadata)?);
141    let health_path = server_state
142        .drt()
143        .system_health()
144        .lock()
145        .health_path()
146        .to_string();
147    let live_path = server_state
148        .drt()
149        .system_health()
150        .lock()
151        .live_path()
152        .to_string();
153
154    // Check if LoRA feature is enabled
155    let lora_enabled = std::env::var(crate::config::environment_names::llm::DYN_LORA_ENABLED)
156        .map(|v| v.to_lowercase() == "true")
157        .unwrap_or(false);
158
159    let mut app = Router::new()
160        .route(
161            &health_path,
162            get({
163                let state = Arc::clone(&server_state);
164                move || health_handler(state)
165            }),
166        )
167        .route(
168            &live_path,
169            get({
170                let state = Arc::clone(&server_state);
171                move || health_handler(state)
172            }),
173        )
174        .route(
175            "/metrics",
176            get({
177                let state = Arc::clone(&server_state);
178                move || metrics_handler(state)
179            }),
180        )
181        .route(
182            "/metadata",
183            get({
184                let state = Arc::clone(&server_state);
185                move || metadata_handler(state)
186            }),
187        )
188        .route(
189            "/engine/{*path}",
190            any({
191                let state = Arc::clone(&server_state);
192                move |path, body| engine_route_handler(state, path, body)
193            }),
194        );
195
196    // Add LoRA routes only if DYN_LORA_ENABLED is set to true
197    if lora_enabled {
198        app = app
199            .route(
200                "/v1/loras",
201                get({
202                    let state = Arc::clone(&server_state);
203                    move || list_loras_handler(State(state))
204                })
205                .post({
206                    let state = Arc::clone(&server_state);
207                    move |body| load_lora_handler(State(state), body)
208                }),
209            )
210            .route(
211                "/v1/loras/{*lora_name}",
212                delete({
213                    let state = Arc::clone(&server_state);
214                    move |path| unload_lora_handler(State(state), path)
215                }),
216            );
217    }
218
219    // Self-hosted MDC files. Always mounted; empty registry → 404.
220    // The endpoint triple disambiguates multi-LocalModel-per-DRT; the
221    // suffix segment (LoRA slug or `_base`) scopes per-registration so
222    // detaching one doesn't wipe another's entries.
223    app = app.route(
224        "/v1/metadata/{namespace}/{component}/{endpoint}/{model_slug}/{model_suffix}/{*filename}",
225        get({
226            let state = Arc::clone(&server_state);
227            move |path| metadata_file_handler(State(state), path)
228        }),
229    );
230
231    let app = app
232        .fallback(|| async {
233            tracing::info!("[fallback handler] called");
234            (StatusCode::NOT_FOUND, "Route not found").into_response()
235        })
236        .layer(TraceLayer::new_for_http().make_span_with(make_system_request_span));
237
238    let address = format!("{}:{}", host, port);
239    tracing::info!("[spawn_system_status_server] binding to: {address}");
240
241    let listener = match TcpListener::bind(&address).await {
242        Ok(listener) => {
243            // get the actual address and port, print in debug level
244            let actual_address = listener.local_addr()?;
245            tracing::info!(
246                "[spawn_system_status_server] system status server bound to: {}",
247                actual_address
248            );
249            (listener, actual_address)
250        }
251        Err(e) => {
252            tracing::error!("Failed to bind to address {}: {}", address, e);
253            return Err(anyhow::anyhow!("Failed to bind to address: {}", e));
254        }
255    };
256    let (listener, actual_address) = listener;
257
258    let observer = cancel_token.child_token();
259    // Spawn the server in the background and return the handle
260    let handle = tokio::spawn(async move {
261        if let Err(e) = axum::serve(listener, app)
262            .with_graceful_shutdown(observer.cancelled_owned())
263            .await
264        {
265            tracing::error!("System status server error: {e}");
266        }
267    });
268
269    Ok((actual_address, handle))
270}
271
272/// Health handler with optional active health checking
273#[tracing::instrument(skip_all, level = "trace")]
274async fn health_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
275    // Get basic health status
276    let system_health = state.drt().system_health();
277    let system_health_lock = system_health.lock();
278    let (healthy, endpoints) = system_health_lock.get_health_status();
279    let uptime = Some(system_health_lock.uptime());
280    drop(system_health_lock);
281
282    let healthy_string = if healthy { "ready" } else { "notready" };
283    let status_code = if healthy {
284        StatusCode::OK
285    } else {
286        StatusCode::SERVICE_UNAVAILABLE
287    };
288
289    let response = json!({
290        "status": healthy_string,
291        "uptime": uptime,
292        "endpoints": endpoints,
293    });
294
295    tracing::trace!("Response {}", response.to_string());
296
297    (status_code, response.to_string())
298}
299
300/// Metrics handler with DistributedRuntime uptime
301#[tracing::instrument(skip_all, level = "trace")]
302async fn metrics_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
303    // Get all metrics from the DistributedRuntime.
304    // The uptime gauge is updated automatically via a PrometheusUpdateCallback
305    // registered in DistributedRuntime::new(), so it is always fresh before scrape.
306    //
307    // NOTE: We use a multi-registry model (e.g. one registry per endpoint) and merge at scrape time,
308    // so /metrics traverses registered child registries and produces a single combined output.
309    let response = match state.drt().metrics().prometheus_expfmt() {
310        Ok(r) => r,
311        Err(e) => {
312            tracing::error!("Failed to get metrics from registry: {e}");
313            return (
314                StatusCode::INTERNAL_SERVER_ERROR,
315                "Failed to get metrics".to_string(),
316            );
317        }
318    };
319
320    (StatusCode::OK, response)
321}
322
323/// Metadata handler
324#[tracing::instrument(skip_all, level = "trace")]
325async fn metadata_handler(state: Arc<SystemStatusState>) -> impl IntoResponse {
326    // Check if discovery metadata is available
327    let metadata = match state.discovery_metadata() {
328        Some(metadata) => metadata,
329        None => {
330            tracing::debug!("Metadata endpoint called but no discovery metadata available");
331            return (
332                StatusCode::NOT_FOUND,
333                "Discovery metadata not available".to_string(),
334            )
335                .into_response();
336        }
337    };
338
339    // Read the metadata
340    let metadata_guard = metadata.read().await;
341
342    // Serialize to JSON
343    match serde_json::to_string(&*metadata_guard) {
344        Ok(json) => {
345            tracing::trace!("Returning metadata: {} bytes", json.len());
346            (StatusCode::OK, json).into_response()
347        }
348        Err(e) => {
349            tracing::error!("Failed to serialize metadata: {e}");
350            (
351                StatusCode::INTERNAL_SERVER_ERROR,
352                "Failed to serialize metadata".to_string(),
353            )
354                .into_response()
355        }
356    }
357}
358
359/// Handler for POST /v1/loras - Load a LoRA adapter
360#[tracing::instrument(skip_all, level = "debug")]
361async fn load_lora_handler(
362    State(state): State<Arc<SystemStatusState>>,
363    Json(request): Json<LoadLoraRequest>,
364) -> impl IntoResponse {
365    tracing::info!("Loading LoRA: {}", request.lora_name);
366
367    // Call the load_lora endpoint for each available backend
368    match call_lora_endpoint(
369        state.drt(),
370        "load_lora",
371        json!({
372            "lora_name": request.lora_name,
373            "source": {
374                "uri": request.source.uri
375            },
376        }),
377    )
378    .await
379    {
380        Ok(response) => {
381            if response.status == "error" {
382                tracing::error!(
383                    "Failed to load LoRA {}: {}",
384                    request.lora_name,
385                    response.message.as_deref().unwrap_or("Unknown error")
386                );
387                (StatusCode::INTERNAL_SERVER_ERROR, Json(response))
388            } else {
389                tracing::info!("LoRA loaded successfully: {}", request.lora_name);
390                (StatusCode::OK, Json(response))
391            }
392        }
393        Err(e) => {
394            tracing::error!("Failed to load LoRA {}: {}", request.lora_name, e);
395            (
396                StatusCode::INTERNAL_SERVER_ERROR,
397                Json(LoraResponse {
398                    status: "error".to_string(),
399                    message: Some(e.to_string()),
400                    lora_name: Some(request.lora_name),
401                    lora_id: None,
402                    loras: None,
403                    count: None,
404                }),
405            )
406        }
407    }
408}
409
410/// Handler for DELETE /v1/loras/*lora_name - Unload a LoRA adapter
411#[tracing::instrument(skip_all, level = "debug")]
412async fn unload_lora_handler(
413    State(state): State<Arc<SystemStatusState>>,
414    Path(lora_name): Path<String>,
415) -> impl IntoResponse {
416    // Strip the leading slash from the wildcard capture
417    let lora_name = lora_name
418        .strip_prefix('/')
419        .unwrap_or(&lora_name)
420        .to_string();
421    tracing::info!("Unloading LoRA: {lora_name}");
422
423    // Call the unload_lora endpoint for each available backend
424    match call_lora_endpoint(
425        state.drt(),
426        "unload_lora",
427        json!({
428            "lora_name": lora_name.clone(),
429        }),
430    )
431    .await
432    {
433        Ok(response) => {
434            if response.status == "error" {
435                tracing::error!(
436                    "Failed to unload LoRA {}: {}",
437                    lora_name,
438                    response.message.as_deref().unwrap_or("Unknown error")
439                );
440                (StatusCode::INTERNAL_SERVER_ERROR, Json(response))
441            } else {
442                tracing::info!("LoRA unloaded successfully: {lora_name}");
443                (StatusCode::OK, Json(response))
444            }
445        }
446        Err(e) => {
447            tracing::error!("Failed to unload LoRA {}: {}", lora_name, e);
448            (
449                StatusCode::INTERNAL_SERVER_ERROR,
450                Json(LoraResponse {
451                    status: "error".to_string(),
452                    message: Some(e.to_string()),
453                    lora_name: Some(lora_name),
454                    lora_id: None,
455                    loras: None,
456                    count: None,
457                }),
458            )
459        }
460    }
461}
462
463/// Handler for GET /v1/loras - List all LoRA adapters
464#[tracing::instrument(skip_all, level = "debug")]
465async fn list_loras_handler(State(state): State<Arc<SystemStatusState>>) -> impl IntoResponse {
466    tracing::info!("Listing all LoRAs");
467
468    // Call the list_loras endpoint for each available backend
469    match call_lora_endpoint(state.drt(), "list_loras", json!({})).await {
470        Ok(response) => {
471            tracing::info!("Successfully retrieved LoRA list");
472            (StatusCode::OK, Json(response))
473        }
474        Err(e) => {
475            tracing::error!("Failed to list LoRAs: {e}");
476            (
477                StatusCode::INTERNAL_SERVER_ERROR,
478                Json(LoraResponse {
479                    status: "error".to_string(),
480                    message: Some(e.to_string()),
481                    lora_name: None,
482                    lora_id: None,
483                    loras: None,
484                    count: None,
485                }),
486            )
487        }
488    }
489}
490
491/// `GET /v1/metadata/{namespace}/{component}/{endpoint}/{slug}/{suffix}/{filename}`
492/// — 404 on miss, 500 on read error, raw bytes on hit. Consumer blake3-verifies.
493async fn metadata_file_handler(
494    State(state): State<Arc<SystemStatusState>>,
495    Path((namespace, component, endpoint, model_slug, model_suffix, filename)): Path<(
496        String,
497        String,
498        String,
499        String,
500        String,
501        String,
502    )>,
503) -> impl IntoResponse {
504    let path = match state.drt().metadata_artifacts().get(
505        &namespace,
506        &component,
507        &endpoint,
508        &model_slug,
509        &model_suffix,
510        &filename,
511    ) {
512        Some(p) => p,
513        None => {
514            tracing::debug!(
515                namespace,
516                component,
517                endpoint,
518                model_slug,
519                model_suffix,
520                filename,
521                "metadata artifact not registered for self-host"
522            );
523            return (StatusCode::NOT_FOUND, "Not found").into_response();
524        }
525    };
526
527    match tokio::fs::read(&path).await {
528        Ok(bytes) => (StatusCode::OK, bytes).into_response(),
529        Err(err) => {
530            tracing::error!(
531                namespace,
532                component,
533                endpoint,
534                model_slug,
535                model_suffix,
536                filename,
537                path = %path.display(),
538                %err,
539                "failed to read self-hosted metadata file"
540            );
541            (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file").into_response()
542        }
543    }
544}
545
546/// Helper function to call a LoRA management endpoint for the local worker.
547///
548/// Resolution order (both are in-process, never network discovery):
549/// 1. The legacy local endpoint registry, populated by non-unified workers via
550///    `.register_local_engine()`.
551/// 2. The generic engine-route registry (`/engine/*`). Unified-backend workers
552///    advertise LoRA lifecycle ops (`load_lora`/`unload_lora`/`list_loras`) as
553///    engine *updates*, registered under `update/<name>`; this fallback maps the
554///    bare LoRA name onto that key so the legacy `/v1/loras` surface forwards to
555///    them (the `/v1/loras` compatibility shim).
556///
557/// Because legacy workers populate the local registry, they never reach the
558/// fallback — their `/v1/loras` behavior is unchanged. If neither registry
559/// holds the name, returns an explicit "LoRA management not available" error
560/// rather than an opaque "endpoint not found".
561async fn call_lora_endpoint(
562    drt: &crate::DistributedRuntime,
563    endpoint_name: &str,
564    request_body: serde_json::Value,
565) -> anyhow::Result<LoraResponse> {
566    use crate::engine::AsyncEngine;
567
568    tracing::debug!("Calling LoRA endpoint: '{endpoint_name}'");
569
570    // 1. Legacy local registry (in-process call only).
571    if let Some(engine) = drt.local_endpoint_registry().get(endpoint_name) {
572        tracing::debug!(
573            "Found endpoint '{}' in local registry, calling directly",
574            endpoint_name
575        );
576
577        let request = crate::pipeline::SingleIn::new(request_body);
578        let mut stream = engine.generate(request).await?;
579
580        if let Some(response) = stream.next().await {
581            let response_data = response.data.unwrap_or_default();
582            let lora_response = serde_json::from_value::<LoraResponse>(response_data.clone())
583                .unwrap_or_else(|_| parse_lora_response(&response_data));
584            return Ok(lora_response);
585        }
586
587        anyhow::bail!("No response received from endpoint '{}'", endpoint_name)
588    }
589
590    // 2. Unified-backend engine-update registry fallback. The unified Worker
591    //    registers LoRA ops as engine updates under `update/<name>`, so map the
592    //    bare LoRA endpoint name onto that namespaced key.
593    let update_key = format!("update/{endpoint_name}");
594    if let Some(callback) = drt.engine_routes().get(&update_key) {
595        tracing::debug!(
596            "Found '{}' in engine routes registry, invoking update callback",
597            update_key
598        );
599        let response_data = callback(request_body).await?;
600        let lora_response = serde_json::from_value::<LoraResponse>(response_data.clone())
601            .unwrap_or_else(|_| parse_lora_response(&response_data));
602        return Ok(lora_response);
603    }
604
605    anyhow::bail!(
606        "LoRA management not available: no '{}' handler is registered \
607         (neither a local LoRA endpoint nor an engine update). This worker \
608         either has LoRA disabled or its backend does not support LoRA \
609         management.",
610        endpoint_name
611    )
612}
613
614/// Helper to parse response data into LoraResponse
615fn parse_lora_response(response_data: &serde_json::Value) -> LoraResponse {
616    LoraResponse {
617        status: response_data
618            .get("status")
619            .and_then(|s| s.as_str())
620            .unwrap_or("success")
621            .to_string(),
622        message: response_data
623            .get("message")
624            .and_then(|m| m.as_str())
625            .map(|s| s.to_string()),
626        lora_name: response_data
627            .get("lora_name")
628            .and_then(|n| n.as_str())
629            .map(|s| s.to_string()),
630        lora_id: response_data.get("lora_id").and_then(|id| id.as_u64()),
631        loras: response_data.get("loras").cloned(),
632        count: response_data
633            .get("count")
634            .and_then(|c| c.as_u64())
635            .map(|c| c as usize),
636    }
637}
638
639/// Engine route handler for /engine/* routes
640///
641/// This handler looks up registered callbacks in the engine routes registry
642/// and invokes them with the request body, returning the response as JSON.
643#[tracing::instrument(skip_all, level = "trace", fields(path = %path))]
644async fn engine_route_handler(
645    state: Arc<SystemStatusState>,
646    Path(path): Path<String>,
647    body: Bytes,
648) -> impl IntoResponse {
649    tracing::trace!("Engine route request to /engine/{path}");
650
651    // Parse body as JSON (empty object for GET/empty body)
652    let body_json: serde_json::Value = if body.is_empty() {
653        serde_json::json!({})
654    } else {
655        match serde_json::from_slice(&body) {
656            Ok(json) => json,
657            Err(e) => {
658                tracing::warn!("Invalid JSON in request body: {e}");
659                return (
660                    StatusCode::BAD_REQUEST,
661                    json!({
662                        "error": "Invalid JSON",
663                        "message": format!("{}", e)
664                    })
665                    .to_string(),
666                )
667                    .into_response();
668            }
669        }
670    };
671
672    // Look up callback
673    let callback = match state.drt().engine_routes().get(&path) {
674        Some(cb) => cb,
675        None => {
676            tracing::debug!("Route /engine/{path} not found");
677            return (
678                StatusCode::NOT_FOUND,
679                json!({
680                    "error": "Route not found",
681                    "message": format!("Route /engine/{} not found", path)
682                })
683                .to_string(),
684            )
685                .into_response();
686        }
687    };
688
689    // Call callback (it's async, so await it)
690    match callback(body_json).await {
691        Ok(response) => {
692            tracing::trace!("Engine route handler succeeded for /engine/{path}");
693            (StatusCode::OK, response.to_string()).into_response()
694        }
695        Err(e) => {
696            tracing::error!("Engine route handler error for /engine/{}: {}", path, e);
697            (
698                StatusCode::INTERNAL_SERVER_ERROR,
699                json!({
700                    "error": "Handler error",
701                    "message": format!("{}", e)
702                })
703                .to_string(),
704            )
705                .into_response()
706        }
707    }
708}
709
710// Regular tests: cargo test system_status_server --lib
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use tokio::time::Duration;
715
716    // This is a basic test to verify the HTTP server is working before testing other more complicated tests
717    #[tokio::test]
718    async fn test_http_server_lifecycle() {
719        let cancel_token = CancellationToken::new();
720        let cancel_token_for_server = cancel_token.clone();
721
722        // Test basic HTTP server lifecycle without DistributedRuntime
723        let app = Router::new().route("/test", get(|| async { (StatusCode::OK, "test") }));
724
725        // start HTTP server
726        let server_handle = tokio::spawn(async move {
727            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
728            let _ = axum::serve(listener, app)
729                .with_graceful_shutdown(cancel_token_for_server.cancelled_owned())
730                .await;
731        });
732
733        // server starts immediately, no need to wait
734
735        // cancel token
736        cancel_token.cancel();
737
738        // wait for the server to shut down
739        let result = tokio::time::timeout(Duration::from_secs(5), server_handle).await;
740        assert!(
741            result.is_ok(),
742            "HTTP server should shut down when cancel token is cancelled"
743        );
744    }
745}
746
747// Integration tests: cargo test system_status_server --lib --features integration
748#[cfg(all(test, feature = "integration"))]
749mod integration_tests {
750    use super::*;
751    use crate::config::environment_names::logging as env_logging;
752    use crate::config::environment_names::runtime::canary as env_canary;
753    use crate::distributed::distributed_test_utils::create_test_drt_async;
754    use crate::metrics::MetricsHierarchy;
755    use anyhow::Result;
756    use rstest::rstest;
757    use std::sync::Arc;
758    use tokio::time::Duration;
759
760    #[tokio::test]
761    async fn test_uptime_from_system_health() {
762        // Test that uptime is available from SystemHealth
763        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
764            let drt = create_test_drt_async().await;
765
766            // Get uptime from SystemHealth
767            let uptime = drt.system_health().lock().uptime();
768            // Uptime should exist (even if close to zero)
769            assert!(uptime.as_nanos() > 0 || uptime.is_zero());
770
771            // Sleep briefly and check uptime increases
772            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
773            let uptime_after = drt.system_health().lock().uptime();
774            assert!(uptime_after > uptime);
775        })
776        .await;
777    }
778
779    #[tokio::test]
780    async fn test_runtime_metrics_initialization_and_namespace() {
781        // Test that metrics have correct namespace
782        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
783            let drt = create_test_drt_async().await;
784            // SystemStatusState is already created in distributed.rs
785            // so we don't need to create it again here
786
787            // The uptime_seconds metric should already be registered and available
788            let response = drt.metrics().prometheus_expfmt().unwrap();
789            println!("Full metrics response:\n{}", response);
790
791            // Check that uptime_seconds metric is present with correct namespace
792            assert!(
793                response.contains("# HELP dynamo_component_uptime_seconds"),
794                "Should contain uptime_seconds help text"
795            );
796            assert!(
797                response.contains("# TYPE dynamo_component_uptime_seconds gauge"),
798                "Should contain uptime_seconds type"
799            );
800            assert!(
801                response.contains("dynamo_component_uptime_seconds"),
802                "Should contain uptime_seconds metric with correct namespace"
803            );
804        })
805        .await;
806    }
807
808    #[tokio::test]
809    async fn test_uptime_gauge_updates() {
810        // Test that the uptime gauge is properly updated and increases over time
811        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
812            let drt = create_test_drt_async().await;
813
814            // Get initial uptime
815            let initial_uptime = drt.system_health().lock().uptime();
816
817            // Update the gauge with initial value
818            drt.system_health().lock().update_uptime_gauge();
819
820            // Sleep for 100ms
821            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
822
823            // Get uptime after sleep
824            let uptime_after_sleep = drt.system_health().lock().uptime();
825
826            // Update the gauge again
827            drt.system_health().lock().update_uptime_gauge();
828
829            // Verify uptime increased by at least 100ms
830            let elapsed = uptime_after_sleep - initial_uptime;
831            assert!(
832                elapsed >= std::time::Duration::from_millis(100),
833                "Uptime should have increased by at least 100ms after sleep, but only increased by {:?}",
834                elapsed
835            );
836        })
837        .await;
838    }
839
840    #[tokio::test]
841    async fn test_http_requests_fail_when_system_disabled() {
842        // Test that system status server is not running when disabled
843        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
844            let drt = create_test_drt_async().await;
845
846            // Verify that system status server info is None when disabled
847            let system_info = drt.system_status_server_info();
848            assert!(
849                system_info.is_none(),
850                "System status server should not be running when disabled"
851            );
852
853            println!("✓ System status server correctly disabled when not enabled");
854        })
855        .await;
856    }
857
858    /// This test verifies the health and liveness endpoints of the system status server.
859    /// It checks that the endpoints respond with the correct HTTP status codes and bodies
860    /// based on the initial health status and any custom endpoint paths provided via environment variables.
861    /// The test is parameterized using multiple #[case] attributes to cover various scenarios,
862    /// including different initial health states ("ready" and "notready"), default and custom endpoint paths,
863    /// and expected response codes and bodies.
864    #[rstest]
865    #[case("ready", 200, "ready", None, None, 3)]
866    #[case("notready", 503, "notready", None, None, 3)]
867    #[case("ready", 200, "ready", Some("/custom/health"), Some("/custom/live"), 5)]
868    #[case(
869        "notready",
870        503,
871        "notready",
872        Some("/custom/health"),
873        Some("/custom/live"),
874        5
875    )]
876    #[tokio::test]
877    #[cfg(feature = "integration")]
878    async fn test_health_endpoints(
879        #[case] starting_health_status: &'static str,
880        #[case] expected_status: u16,
881        #[case] expected_body: &'static str,
882        #[case] custom_health_path: Option<&'static str>,
883        #[case] custom_live_path: Option<&'static str>,
884        #[case] expected_num_tests: usize,
885    ) {
886        use std::sync::Arc;
887        // use tokio::io::{AsyncReadExt, AsyncWriteExt};
888        // use reqwest for HTTP requests
889
890        // Closure call is needed here to satisfy async_with_vars
891
892        crate::logging::init();
893
894        #[allow(clippy::redundant_closure_call)]
895        temp_env::async_with_vars(
896            [
897                (env_system::DYN_SYSTEM_PORT, Some("0")),
898                (
899                    env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS,
900                    Some(starting_health_status),
901                ),
902                (env_system::DYN_SYSTEM_HEALTH_PATH, custom_health_path),
903                (env_system::DYN_SYSTEM_LIVE_PATH, custom_live_path),
904            ],
905            (async || {
906                let drt = Arc::new(create_test_drt_async().await);
907
908                // Get system status server info from DRT (instead of manually spawning)
909                let system_info = drt
910                    .system_status_server_info()
911                    .expect("System status server should be started by DRT");
912                let addr = system_info.socket_addr;
913
914                let client = reqwest::Client::new();
915
916                // Prepare test cases
917                let mut test_cases = vec![];
918                match custom_health_path {
919                    None => {
920                        // When using default paths, test the default paths
921                        test_cases.push(("/health", expected_status, expected_body));
922                    }
923                    Some(chp) => {
924                        // When using custom paths, default paths should not exist
925                        test_cases.push(("/health", 404, "Route not found"));
926                        test_cases.push((chp, expected_status, expected_body));
927                    }
928                }
929                match custom_live_path {
930                    None => {
931                        // When using default paths, test the default paths
932                        test_cases.push(("/live", expected_status, expected_body));
933                    }
934                    Some(clp) => {
935                        // When using custom paths, default paths should not exist
936                        test_cases.push(("/live", 404, "Route not found"));
937                        test_cases.push((clp, expected_status, expected_body));
938                    }
939                }
940                test_cases.push(("/someRandomPathNotFoundHere", 404, "Route not found"));
941                assert_eq!(test_cases.len(), expected_num_tests);
942
943                for (path, expect_status, expect_body) in test_cases {
944                    println!("[test] Sending request to {}", path);
945                    let url = format!("http://{}{}", addr, path);
946                    let response = client.get(&url).send().await.unwrap();
947                    let status = response.status();
948                    let body = response.text().await.unwrap();
949                    println!(
950                        "[test] Response for {}: status={}, body={:?}",
951                        path, status, body
952                    );
953                    assert_eq!(
954                        status, expect_status,
955                        "Response: status={}, body={:?}",
956                        status, body
957                    );
958                    assert!(
959                        body.contains(expect_body),
960                        "Response: status={}, body={:?}",
961                        status,
962                        body
963                    );
964                }
965            })(),
966        )
967        .await;
968    }
969
970    #[tokio::test]
971    async fn test_health_endpoint_tracing() -> Result<()> {
972        use std::sync::Arc;
973
974        // Closure call is needed here to satisfy async_with_vars
975
976        #[allow(clippy::redundant_closure_call)]
977        let _ = temp_env::async_with_vars(
978            [
979                (env_system::DYN_SYSTEM_PORT, Some("0")),
980                (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("ready")),
981                (env_logging::DYN_LOGGING_JSONL, Some("1")),
982                (env_logging::DYN_LOG, Some("trace")),
983            ],
984            (async || {
985                // TODO Add proper testing for
986                // trace id and parent id
987
988                crate::logging::init();
989
990                let drt = Arc::new(create_test_drt_async().await);
991
992                // Get system status server info from DRT (instead of manually spawning)
993                let system_info = drt
994                    .system_status_server_info()
995                    .expect("System status server should be started by DRT");
996                let addr = system_info.socket_addr;
997                let client = reqwest::Client::new();
998                for path in [("/health"), ("/live"), ("/someRandomPathNotFoundHere")] {
999                    let traceparent_value =
1000                        "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
1001                    let tracestate_value = "vendor1=opaqueValue1,vendor2=opaqueValue2";
1002                    let mut headers = reqwest::header::HeaderMap::new();
1003                    headers.insert(
1004                        reqwest::header::HeaderName::from_static("traceparent"),
1005                        reqwest::header::HeaderValue::from_str(traceparent_value)?,
1006                    );
1007                    headers.insert(
1008                        reqwest::header::HeaderName::from_static("tracestate"),
1009                        reqwest::header::HeaderValue::from_str(tracestate_value)?,
1010                    );
1011                    let url = format!("http://{}{}", addr, path);
1012                    let response = client.get(&url).headers(headers).send().await.unwrap();
1013                    let status = response.status();
1014                    let body = response.text().await.unwrap();
1015                    tracing::info!(body = body, status = status.to_string());
1016                }
1017
1018                Ok::<(), anyhow::Error>(())
1019            })(),
1020        )
1021        .await;
1022        Ok(())
1023    }
1024
1025    #[tokio::test]
1026    async fn test_health_endpoint_with_changing_health_status() {
1027        // Test health endpoint starts in not ready status, then becomes ready
1028        // when endpoints are created (DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS=generate)
1029        const ENDPOINT_NAME: &str = "generate";
1030        const ENDPOINT_HEALTH_CONFIG: &str = "[\"generate\"]";
1031        temp_env::async_with_vars(
1032            [
1033                (env_system::DYN_SYSTEM_PORT, Some("0")),
1034                (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("notready")),
1035                (env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS, Some(ENDPOINT_HEALTH_CONFIG)),
1036            ],
1037            async {
1038                let drt = Arc::new(create_test_drt_async().await);
1039
1040                // Check if system status server was started
1041                let system_info_opt = drt.system_status_server_info();
1042
1043                // Ensure system status server was spawned by DRT
1044                assert!(
1045                    system_info_opt.is_some(),
1046                    "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={:?}",
1047                    std::env::var(env_system::DYN_SYSTEM_PORT)
1048                );
1049
1050                // Get the system status server info from DRT - this should never fail now due to above check
1051                let system_info = system_info_opt.unwrap();
1052                let addr = system_info.socket_addr;
1053
1054                // Initially check health - should be not ready
1055                let client = reqwest::Client::new();
1056                let health_url = format!("http://{}/health", addr);
1057
1058                let response = client.get(&health_url).send().await.unwrap();
1059                let status = response.status();
1060                let body = response.text().await.unwrap();
1061
1062                // Health should be not ready (503) initially
1063                assert_eq!(status, 503, "Health should be 503 (not ready) initially, got: {}", status);
1064                assert!(body.contains("\"status\":\"notready\""), "Health should contain status notready");
1065
1066                // Now create a namespace, component, and endpoint to make the system healthy
1067                let namespace = drt.namespace("ns1234").unwrap();
1068                let component = namespace.component("comp1234").unwrap();
1069
1070                // Create a simple test handler
1071                use crate::pipeline::{async_trait, network::Ingress, AsyncEngine, AsyncEngineContextProvider, Error, ManyOut, SingleIn};
1072                use crate::protocols::annotated::Annotated;
1073
1074                struct TestHandler;
1075
1076                #[async_trait]
1077                impl AsyncEngine<SingleIn<String>, ManyOut<Annotated<String>>, anyhow::Error> for TestHandler {
1078                    async fn generate(&self, input: SingleIn<String>) -> anyhow::Result<ManyOut<Annotated<String>>> {
1079                        let (data, ctx) = input.into_parts();
1080                        let response = Annotated::from_data(format!("You responded: {}", data));
1081                        Ok(crate::pipeline::ResponseStream::new(
1082                            Box::pin(crate::stream::iter(vec![response])),
1083                            ctx.context()
1084                        ))
1085                    }
1086                }
1087
1088                // Create the ingress and start the endpoint service
1089                let ingress = Ingress::for_engine(std::sync::Arc::new(TestHandler)).unwrap();
1090
1091                // Start the service and endpoint with a health check payload
1092                // This will automatically register the endpoint for health monitoring
1093                tokio::spawn(async move {
1094                    let _ = component.endpoint(ENDPOINT_NAME)
1095                        .endpoint_builder()
1096                        .handler(ingress)
1097                        .health_check_payload(serde_json::json!({
1098                            "test": "health_check"
1099                        }))
1100                        .start()
1101                        .await;
1102                });
1103
1104                // Hit health endpoint 200 times to verify consistency
1105                let mut success_count = 0;
1106                let mut failures = Vec::new();
1107
1108                for i in 1..=200 {
1109                    let response = client.get(&health_url).send().await.unwrap();
1110                    let status = response.status();
1111                    let body = response.text().await.unwrap();
1112
1113                    if status == 200 && body.contains("\"status\":\"ready\"") {
1114                        success_count += 1;
1115                    } else {
1116                        failures.push((i, status.as_u16(), body.clone()));
1117                        if failures.len() <= 5 {  // Only log first 5 failures
1118                            tracing::warn!("Request {}: status={}, body={}", i, status, body);
1119                        }
1120                    }
1121                }
1122
1123                tracing::info!("Health endpoint test results: {success_count}/200 requests succeeded");
1124                if !failures.is_empty() {
1125                    tracing::warn!("Failed requests: {}", failures.len());
1126                }
1127
1128                // Expect at least 150 out of 200 requests to be successful
1129                assert!(success_count >= 150, "Expected at least 150 out of 200 requests to succeed, but only {} succeeded", success_count);
1130            },
1131        )
1132        .await;
1133    }
1134
1135    #[tokio::test]
1136    async fn test_spawn_system_status_server_endpoints() {
1137        // use reqwest for HTTP requests
1138        temp_env::async_with_vars(
1139            [
1140                (env_system::DYN_SYSTEM_PORT, Some("0")),
1141                (env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("ready")),
1142            ],
1143            async {
1144                let drt = Arc::new(create_test_drt_async().await);
1145
1146                // Get system status server info from DRT (instead of manually spawning)
1147                let system_info = drt
1148                    .system_status_server_info()
1149                    .expect("System status server should be started by DRT");
1150                let addr = system_info.socket_addr;
1151                let client = reqwest::Client::new();
1152                for (path, expect_200, expect_body) in [
1153                    ("/health", true, "ready"),
1154                    ("/live", true, "ready"),
1155                    ("/someRandomPathNotFoundHere", false, "Route not found"),
1156                ] {
1157                    println!("[test] Sending request to {}", path);
1158                    let url = format!("http://{}{}", addr, path);
1159                    let response = client.get(&url).send().await.unwrap();
1160                    let status = response.status();
1161                    let body = response.text().await.unwrap();
1162                    println!(
1163                        "[test] Response for {}: status={}, body={:?}",
1164                        path, status, body
1165                    );
1166                    if expect_200 {
1167                        assert_eq!(status, 200, "Response: status={}, body={:?}", status, body);
1168                    } else {
1169                        assert_eq!(status, 404, "Response: status={}, body={:?}", status, body);
1170                    }
1171                    assert!(
1172                        body.contains(expect_body),
1173                        "Response: status={}, body={:?}",
1174                        status,
1175                        body
1176                    );
1177                }
1178                // DRT handles server cleanup automatically
1179            },
1180        )
1181        .await;
1182    }
1183
1184    #[cfg(feature = "integration")]
1185    #[tokio::test]
1186    async fn test_health_check_with_payload_and_timeout() {
1187        // Test the complete health check flow with the new canary-based system:
1188        crate::logging::init();
1189
1190        temp_env::async_with_vars(
1191            [
1192                (env_system::DYN_SYSTEM_PORT, Some("0")),
1193                (
1194                    env_system::DYN_SYSTEM_STARTING_HEALTH_STATUS,
1195                    Some("notready"),
1196                ),
1197                (
1198                    env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS,
1199                    Some("[\"test.endpoint\"]"),
1200                ),
1201                // Enable health check with short intervals for testing
1202                ("DYN_HEALTH_CHECK_ENABLED", Some("true")),
1203                (env_canary::DYN_CANARY_WAIT_TIME, Some("1")), // Send canary after 1 second of inactivity
1204                ("DYN_HEALTH_CHECK_REQUEST_TIMEOUT", Some("1")), // Immediately timeout to mimic unresponsiveness
1205                ("RUST_LOG", Some("info")),                      // Enable logging for test
1206            ],
1207            async {
1208                let drt = Arc::new(create_test_drt_async().await);
1209
1210                // Get system status server info
1211                let system_info = drt
1212                    .system_status_server_info()
1213                    .expect("System status server should be started");
1214                let addr = system_info.socket_addr;
1215
1216                let client = reqwest::Client::new();
1217                let health_url = format!("http://{}/health", addr);
1218
1219                // Register an endpoint with health check payload
1220                let endpoint = "test.endpoint";
1221                let health_check_payload = serde_json::json!({
1222                    "prompt": "health check test",
1223                    "_health_check": true
1224                });
1225
1226                // Register the endpoint and its health check payload
1227                {
1228                    let system_health = drt.system_health();
1229                    let system_health_lock = system_health.lock();
1230                    system_health_lock.register_health_check_target(
1231                        endpoint,
1232                        crate::component::Instance {
1233                            component: "test_component".to_string(),
1234                            endpoint: "health".to_string(),
1235                            namespace: "test_namespace".to_string(),
1236                            instance_id: 1,
1237                            transport: crate::component::TransportType::Nats(endpoint.to_string()),
1238                            device_type: None,
1239                        },
1240                        health_check_payload.clone(),
1241                    );
1242                }
1243
1244                // Check initial health - should be ready (default state)
1245                let response = client.get(&health_url).send().await.unwrap();
1246                let status = response.status();
1247                let body = response.text().await.unwrap();
1248                assert_eq!(status, 503, "Should be unhealthy initially (default state)");
1249                assert!(
1250                    body.contains("\"status\":\"notready\""),
1251                    "Should show notready status initially"
1252                );
1253
1254                // Set endpoint to healthy state
1255                drt.system_health()
1256                    .lock()
1257                    .set_endpoint_health_status(endpoint, HealthStatus::Ready);
1258
1259                // Check health again - should now be healthy
1260                let response = client.get(&health_url).send().await.unwrap();
1261                let status = response.status();
1262                let body = response.text().await.unwrap();
1263
1264                assert_eq!(status, 200, "Should be healthy due to recent response");
1265                assert!(
1266                    body.contains("\"status\":\"ready\""),
1267                    "Should show ready status after response"
1268                );
1269
1270                // Verify the endpoint status in SystemHealth directly
1271                let endpoint_status = drt
1272                    .system_health()
1273                    .lock()
1274                    .get_endpoint_health_status(endpoint);
1275                assert_eq!(
1276                    endpoint_status,
1277                    Some(HealthStatus::Ready),
1278                    "SystemHealth should show endpoint as Ready after response"
1279                );
1280            },
1281        )
1282        .await;
1283    }
1284
1285    /// `/v1/loras` compat shim: with the legacy local registry empty, a LoRA
1286    /// update registered in `engine_routes()` under `update/<name>` resolves via
1287    /// the fallback and its JSON response is parsed into a `LoraResponse`. This
1288    /// is the path unified-backend workers take for `/v1/loras`.
1289    #[tokio::test]
1290    async fn test_call_lora_endpoint_resolves_via_engine_routes() {
1291        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1292            let drt = create_test_drt_async().await;
1293
1294            let callback: crate::engine_routes::EngineRouteCallback = Arc::new(|_body| {
1295                Box::pin(async move {
1296                    Ok(serde_json::json!({
1297                        "status": "success",
1298                        "lora_name": "adapterA",
1299                        "lora_id": 42,
1300                    }))
1301                })
1302            });
1303            // Unified Worker registers LoRA ops under the `update/` namespace.
1304            drt.engine_routes().register("update/load_lora", callback);
1305
1306            // Local registry is empty, so resolution must fall through to
1307            // engine_routes.
1308            assert!(drt.local_endpoint_registry().get("load_lora").is_none());
1309
1310            let response = call_lora_endpoint(
1311                &drt,
1312                "load_lora",
1313                serde_json::json!({"lora_name": "adapterA"}),
1314            )
1315            .await
1316            .expect("engine_routes fallback should resolve the control");
1317
1318            assert_eq!(response.status, "success");
1319            assert_eq!(response.lora_name.as_deref(), Some("adapterA"));
1320            assert_eq!(response.lora_id, Some(42));
1321        })
1322        .await;
1323    }
1324
1325    /// When neither the local registry nor `engine_routes()` holds the name,
1326    /// the caller gets an explicit "LoRA management not available" error
1327    /// rather than an opaque "endpoint not found".
1328    #[tokio::test]
1329    async fn test_call_lora_endpoint_missing_returns_clean_error() {
1330        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1331            let drt = create_test_drt_async().await;
1332
1333            let err = call_lora_endpoint(&drt, "load_lora", serde_json::json!({}))
1334                .await
1335                .expect_err("missing handler must error");
1336
1337            assert!(
1338                err.to_string().contains("LoRA management not available"),
1339                "expected explicit unavailable message, got: {err}"
1340            );
1341        })
1342        .await;
1343    }
1344
1345    /// An update callback that returns `{"status":"error",...}` (rather than
1346    /// raising) surfaces as a `LoraResponse{status:"error"}`. The `/v1/loras`
1347    /// load/unload handlers map this to HTTP 500, preserving legacy semantics;
1348    /// the direct `/engine/*` route would instead return HTTP 200 + this JSON.
1349    #[tokio::test]
1350    async fn test_call_lora_endpoint_propagates_error_status() {
1351        temp_env::async_with_vars([(env_system::DYN_SYSTEM_PORT, None::<&str>)], async {
1352            let drt = create_test_drt_async().await;
1353
1354            let callback: crate::engine_routes::EngineRouteCallback = Arc::new(|_body| {
1355                Box::pin(async move {
1356                    Ok(serde_json::json!({
1357                        "status": "error",
1358                        "message": "adapter not found",
1359                    }))
1360                })
1361            });
1362            // Unified Worker registers LoRA ops under the `update/` namespace.
1363            drt.engine_routes().register("update/unload_lora", callback);
1364
1365            let response = call_lora_endpoint(&drt, "unload_lora", serde_json::json!({}))
1366                .await
1367                .expect("a non-raising callback returns Ok even on logical error");
1368
1369            assert_eq!(response.status, "error");
1370            assert_eq!(response.message.as_deref(), Some("adapter not found"));
1371        })
1372        .await;
1373    }
1374}