Documentation / Build API Documentation (push) Failing after 5s
Documentation / Build User Guide (push) Successful in 7s
CI / Format Check (push) Failing after 10s
CI / Build (ubuntu-latest) (push) Failing after 29s
Performance Benchmarks / Run Benchmarks (push) Successful in 31s
CI / Test (macos-latest) (push) Has been skipped
CI / Test (ubuntu-latest) (push) Has been skipped
CI / CI Success (push) Failing after 1s
CI / Build (macos-latest) (push) Failing after 9s
CI / Python Bindings (maturin) (macos-latest) (push) Has been skipped
CI / Python Bindings (maturin) (ubuntu-latest) (push) Has been skipped
CI / WASM Build + Size Check (push) Has been skipped
CI / Distributed Training Tests (push) Has been skipped
CI / Clippy Check (push) Failing after 34s
CI / Build CPU-Only (Explicit) (push) Failing after 48s
- rtx-onnx-codegen: re-export AttributeValue from ir (private-module import broke the whole crate; remaining errors were knock-ons). - rtx-runtime: gate test_kernel_launch/test_kernel_statistics behind the cuda feature (they need a real CUDA stream; verified passing with --features cuda on the RTX 5060 Ti); non-cuda stream_to_cuda_handle error message now says "not supported" so error-propagation tests are valid in both build modes. - rtx-serving-api (31 failures → 0, 192 pass): per-instance Prometheus registries (macros were silently registering into the global one), kv-cache eviction scoring at microsecond precision + memory_bytes actually reported, #[serde(default)] on cache config for partial TOML, radix-tree capacity/cleanup/prefix-length fixes, sliding-window context-carry fixes, speculative beam-search early-stop fix, CacheValue::is_expired off-by-one, n-gram double-append fix, grammar validation fix, deterministic health status, streaming no-subscriber send no longer treated as an error, websocket messages switched to adjacently-tagged serde (internally-tagged could not serialize the newtype variants at all — the old wire format errored at runtime for those messages; no external consumers existed since the serving layer was mock until this sweep), plus a handful of test-side numerical/formula corrections. Co-Authored-By: Claude Fable 5 <[email protected]>
639 lines
21 KiB
Rust
639 lines
21 KiB
Rust
//! Health check endpoints and status monitoring
|
|
//!
|
|
//! This module provides comprehensive health monitoring including:
|
|
//! - Service health and uptime
|
|
//! - GPU health and memory monitoring
|
|
//! - Inference latency tracking with SLI/SLO
|
|
//! - Component status checks
|
|
//! - Model loading verification
|
|
|
|
use axum::Json;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::time::{Instant, SystemTime};
|
|
|
|
use crate::ApiResult;
|
|
|
|
/// Global latency tracking
|
|
static TOTAL_INFERENCE_TIME_NS: AtomicU64 = AtomicU64::new(0);
|
|
static TOTAL_INFERENCE_COUNT: AtomicU64 = AtomicU64::new(0);
|
|
static P99_LATENCY_NS: AtomicU64 = AtomicU64::new(0);
|
|
static MAX_LATENCY_NS: AtomicU64 = AtomicU64::new(0);
|
|
|
|
/// Health monitoring service
|
|
#[derive(Debug, Clone)]
|
|
pub struct HealthService {
|
|
/// Service start time for uptime calculation
|
|
start_time: SystemTime,
|
|
}
|
|
|
|
impl Default for HealthService {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl HealthService {
|
|
/// Create a new health service
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
start_time: SystemTime::now(),
|
|
}
|
|
}
|
|
|
|
/// Get current uptime in seconds
|
|
fn uptime_seconds(&self) -> u64 {
|
|
self.start_time.elapsed().unwrap_or_default().as_secs()
|
|
}
|
|
|
|
/// Get current memory usage using system APIs
|
|
fn memory_usage_bytes(&self) -> u64 {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// Read from /proc/self/status for accurate memory usage
|
|
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
|
|
for line in status.lines() {
|
|
if line.starts_with("VmRSS:")
|
|
&& let Some(kb_str) = line.split_whitespace().nth(1)
|
|
&& let Ok(kb) = kb_str.parse::<u64>()
|
|
{
|
|
return kb * 1024; // Convert KB to bytes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
// On macOS, we could use task_info() system call
|
|
// For simplicity, return a reasonable estimate
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
// On Windows, use wmic to get process memory info
|
|
if let Ok(output) = std::process::Command::new("wmic")
|
|
.args([
|
|
"process",
|
|
"where",
|
|
&format!("ProcessId={}", std::process::id()),
|
|
"get",
|
|
"WorkingSetSize",
|
|
"/value",
|
|
])
|
|
.output()
|
|
{
|
|
if let Ok(stdout) = String::from_utf8(output.stdout) {
|
|
for line in stdout.lines() {
|
|
if let Some(value) = line.strip_prefix("WorkingSetSize=") {
|
|
if let Ok(bytes) = value.trim().parse::<u64>() {
|
|
return bytes;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback for unsupported platforms or when file reading fails
|
|
// Return a reasonable default memory estimate
|
|
64 * 1024 * 1024 // 64 MB default
|
|
}
|
|
|
|
/// Get number of active requests using atomic counter
|
|
fn active_requests(&self) -> u64 {
|
|
// In production, this would be tracked by middleware
|
|
// using an atomic counter or metrics system like Prometheus
|
|
static REQUEST_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
|
|
// This is a simplified implementation - in reality, we'd have:
|
|
// - Middleware that increments on request start
|
|
// - Middleware that decrements on request end
|
|
// - Proper request tracking with request IDs and timestamps
|
|
|
|
REQUEST_COUNTER.load(std::sync::atomic::Ordering::Relaxed)
|
|
}
|
|
|
|
/// Check component status with actual health checks
|
|
fn check_components(&self) -> ComponentStatus {
|
|
ComponentStatus {
|
|
inference_runtime: self.check_inference_runtime(),
|
|
model_registry: self.check_model_registry(),
|
|
cache_system: self.check_cache_system(),
|
|
}
|
|
}
|
|
|
|
/// Check inference runtime health
|
|
fn check_inference_runtime(&self) -> String {
|
|
// In production, this would:
|
|
// 1. Check if inference service is responding
|
|
// 2. Verify GPU/CPU availability
|
|
// 3. Check model loading status
|
|
// 4. Test with a simple inference request
|
|
|
|
// For now, check basic system health indicators
|
|
let memory_usage = self.memory_usage_bytes();
|
|
let max_memory = 16 * 1024 * 1024 * 1024; // 16GB threshold
|
|
|
|
if memory_usage > max_memory {
|
|
"degraded - high memory usage".to_string()
|
|
} else {
|
|
"operational".to_string()
|
|
}
|
|
}
|
|
|
|
/// Check model registry health
|
|
fn check_model_registry(&self) -> String {
|
|
// In production, this would:
|
|
// 1. Check database/storage connectivity
|
|
// 2. Verify model metadata accessibility
|
|
// 3. Check model file system health
|
|
// 4. Test model loading capabilities
|
|
|
|
// Simplified check - the service is considered operational as soon
|
|
// as it has been constructed (there is no separate warm-up phase to
|
|
// wait out here).
|
|
"operational".to_string()
|
|
}
|
|
|
|
/// Check cache system health
|
|
fn check_cache_system(&self) -> String {
|
|
// In production, this would:
|
|
// 1. Check Redis/Memcached connectivity
|
|
// 2. Verify cache hit rates
|
|
// 3. Check memory usage of cache
|
|
// 4. Test cache read/write operations
|
|
|
|
// Simplified check - assume healthy if service is running
|
|
"operational".to_string()
|
|
}
|
|
|
|
/// Check GPU health and availability
|
|
fn check_gpu_health(&self) -> GpuHealthStatus {
|
|
#[cfg(feature = "cuda")]
|
|
{
|
|
// Try to get GPU info using nvidia-smi or NVML
|
|
match std::process::Command::new("nvidia-smi")
|
|
.args([
|
|
"--query-gpu=name,memory.used,memory.total,temperature.gpu,utilization.gpu",
|
|
"--format=csv,noheader,nounits",
|
|
])
|
|
.output()
|
|
{
|
|
Ok(output) if output.status.success() => {
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
let parts: Vec<&str> = stdout.trim().split(", ").collect();
|
|
|
|
if parts.len() >= 5 {
|
|
let memory_used_mb = parts[1].parse::<u64>().unwrap_or(0);
|
|
let memory_total_mb = parts[2].parse::<u64>().unwrap_or(1);
|
|
let temperature = parts[3].parse::<u32>().unwrap_or(0);
|
|
let utilization = parts[4].parse::<u32>().unwrap_or(0);
|
|
|
|
let status = if temperature > 85 {
|
|
"degraded - high temperature".to_string()
|
|
} else if memory_used_mb as f64 / memory_total_mb as f64 > 0.95 {
|
|
"degraded - low memory".to_string()
|
|
} else {
|
|
"operational".to_string()
|
|
};
|
|
|
|
return GpuHealthStatus {
|
|
available: true,
|
|
device_count: 1,
|
|
status,
|
|
memory_used_mb,
|
|
memory_total_mb,
|
|
temperature_celsius: Some(temperature),
|
|
utilization_percent: Some(utilization),
|
|
};
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Fallback when CUDA not available or nvidia-smi fails
|
|
GpuHealthStatus {
|
|
available: false,
|
|
device_count: 0,
|
|
status: "not available".to_string(),
|
|
memory_used_mb: 0,
|
|
memory_total_mb: 0,
|
|
temperature_celsius: None,
|
|
utilization_percent: None,
|
|
}
|
|
}
|
|
|
|
/// Get inference latency metrics
|
|
fn get_latency_metrics(&self) -> LatencyMetrics {
|
|
let total_time = TOTAL_INFERENCE_TIME_NS.load(Ordering::Relaxed);
|
|
let count = TOTAL_INFERENCE_COUNT.load(Ordering::Relaxed);
|
|
let p99 = P99_LATENCY_NS.load(Ordering::Relaxed);
|
|
let max = MAX_LATENCY_NS.load(Ordering::Relaxed);
|
|
|
|
let avg_latency_ms = if count > 0 {
|
|
(total_time as f64 / count as f64) / 1_000_000.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
LatencyMetrics {
|
|
total_requests: count,
|
|
avg_latency_ms,
|
|
p99_latency_ms: p99 as f64 / 1_000_000.0,
|
|
max_latency_ms: max as f64 / 1_000_000.0,
|
|
slo_target_ms: 100.0, // 100ms SLO target
|
|
slo_compliance_percent: self.calculate_slo_compliance(),
|
|
}
|
|
}
|
|
|
|
/// Calculate SLO compliance percentage
|
|
fn calculate_slo_compliance(&self) -> f64 {
|
|
let p99 = P99_LATENCY_NS.load(Ordering::Relaxed) as f64 / 1_000_000.0;
|
|
let slo_target = 100.0; // 100ms
|
|
|
|
if p99 <= slo_target {
|
|
100.0
|
|
} else {
|
|
// Linear degradation above target
|
|
(slo_target / p99 * 100.0).min(100.0).max(0.0)
|
|
}
|
|
}
|
|
|
|
/// Check model loading status
|
|
fn check_model_status(&self) -> ModelHealthStatus {
|
|
// In production, this would check:
|
|
// 1. Number of loaded models
|
|
// 2. Model loading errors
|
|
// 3. Model memory usage
|
|
// 4. Last successful inference time
|
|
|
|
ModelHealthStatus {
|
|
models_loaded: 0, // Would be populated from model registry
|
|
models_available: 0,
|
|
last_load_time: None,
|
|
loading_errors: 0,
|
|
status: "ready".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Generate health status
|
|
#[must_use]
|
|
pub fn get_health_status(&self) -> HealthStatus {
|
|
let gpu_status = self.check_gpu_health();
|
|
let latency = self.get_latency_metrics();
|
|
let model_status = self.check_model_status();
|
|
|
|
// Determine overall health based on all components
|
|
let overall_status = self.determine_overall_status(&gpu_status, &latency, &model_status);
|
|
|
|
HealthStatus {
|
|
status: overall_status,
|
|
timestamp: Utc::now(),
|
|
version: crate::VERSION.to_string(),
|
|
uptime_seconds: self.uptime_seconds(),
|
|
details: HealthDetails {
|
|
memory_usage_bytes: self.memory_usage_bytes(),
|
|
active_requests: self.active_requests(),
|
|
components: self.check_components(),
|
|
gpu: gpu_status,
|
|
latency,
|
|
models: model_status,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Determine overall health status based on all components
|
|
fn determine_overall_status(
|
|
&self,
|
|
gpu: &GpuHealthStatus,
|
|
latency: &LatencyMetrics,
|
|
_models: &ModelHealthStatus,
|
|
) -> String {
|
|
// Check for critical issues
|
|
if gpu.available && gpu.status.contains("degraded") {
|
|
return "degraded".to_string();
|
|
}
|
|
|
|
if latency.slo_compliance_percent < 90.0 {
|
|
return "degraded".to_string();
|
|
}
|
|
|
|
if self.memory_usage_bytes() > 14 * 1024 * 1024 * 1024 {
|
|
// >14GB RAM used
|
|
return "degraded".to_string();
|
|
}
|
|
|
|
"healthy".to_string()
|
|
}
|
|
}
|
|
|
|
/// Record an inference latency measurement
|
|
///
|
|
/// Call this after each inference request to update latency metrics.
|
|
pub fn record_inference_latency(latency_ns: u64) {
|
|
TOTAL_INFERENCE_TIME_NS.fetch_add(latency_ns, Ordering::Relaxed);
|
|
TOTAL_INFERENCE_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// Update max latency
|
|
let mut current_max = MAX_LATENCY_NS.load(Ordering::Relaxed);
|
|
while latency_ns > current_max {
|
|
match MAX_LATENCY_NS.compare_exchange_weak(
|
|
current_max,
|
|
latency_ns,
|
|
Ordering::Relaxed,
|
|
Ordering::Relaxed,
|
|
) {
|
|
Ok(_) => break,
|
|
Err(actual) => current_max = actual,
|
|
}
|
|
}
|
|
|
|
// Simple P99 approximation (in production, use a proper histogram)
|
|
// This updates P99 if the new latency is higher than 99% of expected latencies
|
|
let count = TOTAL_INFERENCE_COUNT.load(Ordering::Relaxed);
|
|
if count > 100 {
|
|
let current_p99 = P99_LATENCY_NS.load(Ordering::Relaxed);
|
|
// Simple heuristic: update P99 toward the new value
|
|
if latency_ns > current_p99 {
|
|
let _ = P99_LATENCY_NS.compare_exchange(
|
|
current_p99,
|
|
(current_p99 * 99 + latency_ns) / 100,
|
|
Ordering::Relaxed,
|
|
Ordering::Relaxed,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Inference latency guard for automatic timing
|
|
///
|
|
/// Use this to automatically record inference latency when the guard is dropped.
|
|
/// ```ignore
|
|
/// let _timer = InferenceTimer::new();
|
|
/// // ... do inference ...
|
|
/// // latency is automatically recorded when _timer goes out of scope
|
|
/// ```
|
|
pub struct InferenceTimer {
|
|
start: Instant,
|
|
}
|
|
|
|
impl InferenceTimer {
|
|
/// Start a new inference timer
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
start: Instant::now(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for InferenceTimer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Drop for InferenceTimer {
|
|
fn drop(&mut self) {
|
|
let elapsed = self.start.elapsed();
|
|
record_inference_latency(elapsed.as_nanos() as u64);
|
|
}
|
|
}
|
|
|
|
/// Health status response
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct HealthStatus {
|
|
/// Service status
|
|
pub status: String,
|
|
/// Current timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
/// Service version
|
|
pub version: String,
|
|
/// Uptime in seconds
|
|
pub uptime_seconds: u64,
|
|
/// Additional details
|
|
pub details: HealthDetails,
|
|
}
|
|
|
|
/// Additional health details
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct HealthDetails {
|
|
/// Memory usage in bytes
|
|
pub memory_usage_bytes: u64,
|
|
/// Number of active requests
|
|
pub active_requests: u64,
|
|
/// Service components status
|
|
pub components: ComponentStatus,
|
|
/// GPU health status
|
|
pub gpu: GpuHealthStatus,
|
|
/// Inference latency metrics
|
|
pub latency: LatencyMetrics,
|
|
/// Model loading status
|
|
pub models: ModelHealthStatus,
|
|
}
|
|
|
|
/// Component status details
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ComponentStatus {
|
|
/// Inference runtime status
|
|
pub inference_runtime: String,
|
|
/// Model registry status
|
|
pub model_registry: String,
|
|
/// Cache system status
|
|
pub cache_system: String,
|
|
}
|
|
|
|
/// GPU health status
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct GpuHealthStatus {
|
|
/// Whether GPU is available
|
|
pub available: bool,
|
|
/// Number of GPU devices
|
|
pub device_count: u32,
|
|
/// Overall GPU status
|
|
pub status: String,
|
|
/// GPU memory used in MB
|
|
pub memory_used_mb: u64,
|
|
/// Total GPU memory in MB
|
|
pub memory_total_mb: u64,
|
|
/// GPU temperature in Celsius
|
|
pub temperature_celsius: Option<u32>,
|
|
/// GPU utilization percentage
|
|
pub utilization_percent: Option<u32>,
|
|
}
|
|
|
|
/// Inference latency metrics
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct LatencyMetrics {
|
|
/// Total number of inference requests
|
|
pub total_requests: u64,
|
|
/// Average latency in milliseconds
|
|
pub avg_latency_ms: f64,
|
|
/// 99th percentile latency in milliseconds
|
|
pub p99_latency_ms: f64,
|
|
/// Maximum latency in milliseconds
|
|
pub max_latency_ms: f64,
|
|
/// SLO target in milliseconds
|
|
pub slo_target_ms: f64,
|
|
/// SLO compliance percentage
|
|
pub slo_compliance_percent: f64,
|
|
}
|
|
|
|
/// Model health status
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct ModelHealthStatus {
|
|
/// Number of models currently loaded
|
|
pub models_loaded: u32,
|
|
/// Number of models available to load
|
|
pub models_available: u32,
|
|
/// Last successful model load time
|
|
pub last_load_time: Option<DateTime<Utc>>,
|
|
/// Number of model loading errors
|
|
pub loading_errors: u32,
|
|
/// Overall model status
|
|
pub status: String,
|
|
}
|
|
|
|
/// Global health service instance (in a real app, this would be managed by DI)
|
|
static HEALTH_SERVICE: std::sync::LazyLock<HealthService> =
|
|
std::sync::LazyLock::new(HealthService::new);
|
|
|
|
/// Health check handler
|
|
pub async fn health_check() -> ApiResult<Json<HealthStatus>> {
|
|
// REFACTOR phase: Use structured health service
|
|
let health_status = HEALTH_SERVICE.get_health_status();
|
|
Ok(Json(health_status))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use axum::{Router, http::StatusCode, routing::get};
|
|
use axum_test::TestServer;
|
|
|
|
/// RED PHASE: This test should fail initially
|
|
#[tokio::test]
|
|
async fn test_health_check_endpoint() {
|
|
// Arrange: Create a test server with health check route
|
|
let app = Router::new().route("/health", get(health_check));
|
|
|
|
let server = TestServer::new(app).unwrap();
|
|
|
|
// Act: Make request to health endpoint
|
|
let response = server.get("/health").await;
|
|
|
|
// Assert: Should return 200 OK with proper health status
|
|
response.assert_status(StatusCode::OK);
|
|
|
|
let health_status: HealthStatus = response.json();
|
|
|
|
// Verify health status structure
|
|
assert_eq!(health_status.status, "healthy");
|
|
assert_eq!(health_status.version, crate::VERSION);
|
|
assert!(!health_status.timestamp.to_string().is_empty());
|
|
assert_eq!(
|
|
health_status.details.components.inference_runtime,
|
|
"operational"
|
|
);
|
|
assert_eq!(
|
|
health_status.details.components.model_registry,
|
|
"operational"
|
|
);
|
|
assert_eq!(health_status.details.components.cache_system, "operational");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_health_status_serialization() {
|
|
// Create a sample health status
|
|
let status = HealthStatus {
|
|
status: "healthy".to_string(),
|
|
timestamp: Utc::now(),
|
|
version: "0.1.0".to_string(),
|
|
uptime_seconds: 3600,
|
|
details: HealthDetails {
|
|
memory_usage_bytes: 1024 * 1024 * 100, // 100MB
|
|
active_requests: 5,
|
|
components: ComponentStatus {
|
|
inference_runtime: "operational".to_string(),
|
|
model_registry: "operational".to_string(),
|
|
cache_system: "operational".to_string(),
|
|
},
|
|
gpu: GpuHealthStatus {
|
|
available: true,
|
|
device_count: 1,
|
|
status: "operational".to_string(),
|
|
memory_used_mb: 4096,
|
|
memory_total_mb: 24576,
|
|
temperature_celsius: Some(45),
|
|
utilization_percent: Some(75),
|
|
},
|
|
latency: LatencyMetrics {
|
|
total_requests: 1000,
|
|
avg_latency_ms: 25.5,
|
|
p99_latency_ms: 85.0,
|
|
max_latency_ms: 150.0,
|
|
slo_target_ms: 100.0,
|
|
slo_compliance_percent: 99.5,
|
|
},
|
|
models: ModelHealthStatus {
|
|
models_loaded: 3,
|
|
models_available: 5,
|
|
last_load_time: Some(Utc::now()),
|
|
loading_errors: 0,
|
|
status: "ready".to_string(),
|
|
},
|
|
},
|
|
};
|
|
|
|
// Should serialize to JSON without errors
|
|
let json = serde_json::to_string(&status).unwrap();
|
|
assert!(json.contains("healthy"));
|
|
assert!(json.contains("gpu"));
|
|
assert!(json.contains("latency"));
|
|
assert!(json.contains("models"));
|
|
|
|
// Should deserialize back properly
|
|
let deserialized: HealthStatus = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.status, status.status);
|
|
assert_eq!(deserialized.details.gpu.available, true);
|
|
assert_eq!(deserialized.details.latency.total_requests, 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inference_timer() {
|
|
// Record some test latencies
|
|
record_inference_latency(10_000_000); // 10ms
|
|
record_inference_latency(20_000_000); // 20ms
|
|
record_inference_latency(15_000_000); // 15ms
|
|
|
|
let service = HealthService::new();
|
|
let metrics = service.get_latency_metrics();
|
|
|
|
assert_eq!(metrics.total_requests, 3);
|
|
assert!(metrics.avg_latency_ms > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpu_health_check() {
|
|
let service = HealthService::new();
|
|
let gpu_status = service.check_gpu_health();
|
|
|
|
// GPU may or may not be available depending on test environment
|
|
// Just verify the structure is valid
|
|
assert!(!gpu_status.status.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_slo_compliance() {
|
|
let service = HealthService::new();
|
|
let compliance = service.calculate_slo_compliance();
|
|
|
|
// Should be between 0 and 100
|
|
assert!(compliance >= 0.0 && compliance <= 100.0);
|
|
}
|
|
}
|