//! Comprehensive health check tests for ML Training Service //! //! Tests cover: //! - HTTP /health and /ready endpoints //! - Service startup state transitions //! - GPU availability checks //! - Model checkpoint accessibility //! - Database connectivity //! - Training job status //! - Dependency cascade failures //! - Health check latency //! - Concurrent health checks //! - Health during model training //! - Resource exhaustion (GPU memory, disk space) use axum::{ body::Body, http::{Request, StatusCode}, }; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; use tower::ServiceExt; /// Mock health state for ML training service #[derive(Clone)] struct MockMlHealthState { healthy: Arc>, ready: Arc>, gpu_available: Arc>, checkpoints_accessible: Arc>, database_connected: Arc>, training_active: Arc>, gpu_memory_available: Arc>, } impl MockMlHealthState { fn new() -> Self { Self { healthy: Arc::new(RwLock::new(true)), ready: Arc::new(RwLock::new(true)), gpu_available: Arc::new(RwLock::new(true)), checkpoints_accessible: Arc::new(RwLock::new(true)), database_connected: Arc::new(RwLock::new(true)), training_active: Arc::new(RwLock::new(false)), gpu_memory_available: Arc::new(RwLock::new(true)), } } async fn set_healthy(&self, healthy: bool) { *self.healthy.write().await = healthy; } async fn set_ready(&self, ready: bool) { *self.ready.write().await = ready; } async fn set_gpu_available(&self, available: bool) { *self.gpu_available.write().await = available; } async fn set_checkpoints_accessible(&self, accessible: bool) { *self.checkpoints_accessible.write().await = accessible; } async fn set_database_connected(&self, connected: bool) { *self.database_connected.write().await = connected; } async fn set_training_active(&self, active: bool) { *self.training_active.write().await = active; } async fn set_gpu_memory_available(&self, available: bool) { *self.gpu_memory_available.write().await = available; } async fn is_healthy(&self) -> bool { *self.healthy.read().await } async fn is_ready(&self) -> bool { *self.ready.read().await } async fn is_gpu_available(&self) -> bool { *self.gpu_available.read().await } async fn is_checkpoints_accessible(&self) -> bool { *self.checkpoints_accessible.read().await } async fn is_database_connected(&self) -> bool { *self.database_connected.read().await } async fn is_training_active(&self) -> bool { *self.training_active.read().await } async fn is_gpu_memory_available(&self) -> bool { *self.gpu_memory_available.read().await } } /// Create health router for ML training service fn create_ml_health_router(state: MockMlHealthState) -> axum::Router { use axum::{extract::State, routing::get, Json, Router}; use serde_json::json; async fn health_handler( State(state): State, ) -> Result, StatusCode> { if state.is_healthy().await { Ok(Json(json!({ "status": "healthy", "service": "ml_training", "version": env!("CARGO_PKG_VERSION") }))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) } } async fn ready_handler( State(state): State, ) -> Result, StatusCode> { if state.is_ready().await { Ok(Json(json!({ "status": "ready", "service": "ml_training", "version": env!("CARGO_PKG_VERSION") }))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) } } async fn deep_health_handler( State(state): State, ) -> Result, StatusCode> { let gpu_ok = state.is_gpu_available().await; let checkpoints_ok = state.is_checkpoints_accessible().await; let db_ok = state.is_database_connected().await; let gpu_mem_ok = state.is_gpu_memory_available().await; let healthy = state.is_healthy().await; let training_active = state.is_training_active().await; if healthy && gpu_ok && checkpoints_ok && db_ok && gpu_mem_ok { Ok(Json(json!({ "status": "healthy", "service": "ml_training", "version": env!("CARGO_PKG_VERSION"), "dependencies": { "gpu": "available", "checkpoints": "accessible", "database": "connected", "gpu_memory": "available" }, "training_active": training_active }))) } else { Err(StatusCode::SERVICE_UNAVAILABLE) } } Router::new() .route("/health", get(health_handler)) .route("/ready", get(ready_handler)) .route("/health/deep", get(deep_health_handler)) .with_state(state) } #[tokio::test] async fn test_ml_health_basic_healthy() { let state = MockMlHealthState::new(); let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_ml_health_unhealthy() { let state = MockMlHealthState::new(); state.set_healthy(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_readiness_check() { let state = MockMlHealthState::new(); let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/ready") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_ml_service_startup() { let state = MockMlHealthState::new(); state.set_ready(false).await; // Initially not ready assert!(!state.is_ready().await); assert!(state.is_healthy().await); // Simulate GPU initialization tokio::time::sleep(Duration::from_millis(100)).await; state.set_ready(true).await; assert!(state.is_ready().await); } #[tokio::test] async fn test_ml_gpu_unavailable() { let state = MockMlHealthState::new(); state.set_gpu_available(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_checkpoints_inaccessible() { let state = MockMlHealthState::new(); state.set_checkpoints_accessible(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_database_disconnection() { let state = MockMlHealthState::new(); state.set_database_connected(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_gpu_memory_exhausted() { let state = MockMlHealthState::new(); state.set_gpu_memory_available(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_health_during_training() { let state = MockMlHealthState::new(); state.set_training_active(true).await; let app = create_ml_health_router(state.clone()); // Service should still be healthy during training let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_ml_dependency_cascade_failure() { let state = MockMlHealthState::new(); // All dependencies fail state.set_gpu_available(false).await; state.set_checkpoints_accessible(false).await; state.set_database_connected(false).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_health_check_latency() { let state = MockMlHealthState::new(); let app = create_ml_health_router(state.clone()); let start = Instant::now(); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let latency = start.elapsed(); assert_eq!(response.status(), StatusCode::OK); assert!( latency < Duration::from_millis(100), "Health check latency: {:?}", latency ); } #[tokio::test] async fn test_ml_concurrent_health_checks() { let state = MockMlHealthState::new(); let mut handles = vec![]; for _ in 0..100 { let state_clone = state.clone(); let handle = tokio::spawn(async move { let app = create_ml_health_router(state_clone); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); }); handles.push(handle); } for handle in handles { handle.await.unwrap(); } } #[tokio::test] async fn test_ml_health_during_shutdown() { let state = MockMlHealthState::new(); // Graceful shutdown state.set_ready(false).await; state.set_healthy(true).await; let app = create_ml_health_router(state.clone()); // Ready check fails let response = app .clone() .oneshot( Request::builder() .uri("/ready") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); // Health check passes let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_ml_rapid_health_checks() { let state = MockMlHealthState::new(); let start = Instant::now(); for _ in 0..500 { let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } let duration = start.elapsed(); assert!( duration < Duration::from_secs(1), "500 health checks took: {:?}", duration ); } #[tokio::test] async fn test_ml_deep_vs_shallow_health() { let state = MockMlHealthState::new(); let app = create_ml_health_router(state.clone()); // Shallow health let start = Instant::now(); let response = app .clone() .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let shallow_latency = start.elapsed(); assert_eq!(response.status(), StatusCode::OK); // Deep health let start = Instant::now(); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); let deep_latency = start.elapsed(); assert_eq!(response.status(), StatusCode::OK); assert!(shallow_latency < Duration::from_millis(50)); assert!(deep_latency < Duration::from_millis(100)); } #[tokio::test] async fn test_ml_partial_availability() { let state = MockMlHealthState::new(); // GPU down, checkpoints up state.set_gpu_available(false).await; state.set_checkpoints_accessible(true).await; let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[tokio::test] async fn test_ml_recovery_after_failure() { let state = MockMlHealthState::new(); // Service fails state.set_healthy(false).await; let app = create_ml_health_router(state.clone()); let response = app .clone() .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); // Service recovers state.set_healthy(true).await; let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_ml_health_json_format() { let state = MockMlHealthState::new(); let app = create_ml_health_router(state.clone()); let response = app .oneshot( Request::builder() .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let content_type = response.headers().get("content-type"); assert!(content_type.is_some()); let content_type_str = content_type.unwrap().to_str().expect("INVARIANT: Path should be valid UTF-8"); assert!(content_type_str.contains("application/json")); } #[tokio::test] async fn test_ml_gpu_recovery_scenario() { let state = MockMlHealthState::new(); // GPU fails state.set_gpu_available(false).await; let app = create_ml_health_router(state.clone()); let response = app .clone() .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); // GPU recovers state.set_gpu_available(true).await; let response = app .oneshot( Request::builder() .uri("/health/deep") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); }