use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Unready,
Unhealthy,
Unknown,
}
#[derive(Debug, Clone)]
pub struct HealthConfig {
pub check_interval: Duration,
pub timeout: Duration,
pub failure_threshold: u32,
pub success_threshold: u32,
}
impl Default for HealthConfig {
fn default() -> Self {
Self {
check_interval: Duration::from_secs(10),
timeout: Duration::from_secs(5),
failure_threshold: 3,
success_threshold: 2,
}
}
}
#[derive(Debug, Clone)]
pub struct HealthCheckResult {
pub timestamp: Instant,
pub success: bool,
pub latency_us: u64,
pub error: Option<String>,
}
#[derive(Debug)]
pub struct HealthTracker {
module_name: String,
status: Mutex<HealthStatus>,
failures: AtomicU32,
successes: AtomicU32,
total_checks: AtomicU64,
last_success: Mutex<Option<Instant>>,
last_failure: Mutex<Option<Instant>>,
config: HealthConfig,
}
impl HealthTracker {
pub fn new(module_name: String, config: HealthConfig) -> Self {
Self {
module_name,
status: Mutex::new(HealthStatus::Unknown),
failures: AtomicU32::new(0),
successes: AtomicU32::new(0),
total_checks: AtomicU64::new(0),
last_success: Mutex::new(None),
last_failure: Mutex::new(None),
config,
}
}
pub async fn record_success(&self, _latency_us: u64) {
self.total_checks.fetch_add(1, Ordering::Relaxed);
self.successes.fetch_add(1, Ordering::Relaxed);
self.failures.store(0, Ordering::Relaxed);
*self.last_success.lock().await = Some(Instant::now());
let mut status = self.status.lock().await;
let current = *status;
if current == HealthStatus::Unhealthy || current == HealthStatus::Unknown {
let successes = self.successes.load(Ordering::Relaxed);
if successes >= self.config.success_threshold {
*status = HealthStatus::Healthy;
} else if current == HealthStatus::Unhealthy {
*status = HealthStatus::Unready;
}
}
}
pub async fn record_failure(&self, _error: Option<String>) {
self.total_checks.fetch_add(1, Ordering::Relaxed);
self.failures.fetch_add(1, Ordering::Relaxed);
self.successes.store(0, Ordering::Relaxed);
*self.last_failure.lock().await = Some(Instant::now());
let mut status = self.status.lock().await;
let failures = self.failures.load(Ordering::Relaxed);
if failures >= self.config.failure_threshold {
*status = HealthStatus::Unhealthy;
}
}
pub async fn status(&self) -> HealthStatus {
*self.status.lock().await
}
pub async fn is_healthy(&self) -> bool {
*self.status.lock().await == HealthStatus::Healthy
}
pub async fn is_ready(&self) -> bool {
let status = *self.status.lock().await;
status == HealthStatus::Healthy || status == HealthStatus::Unready
}
pub fn stats(&self) -> HealthStats {
HealthStats {
module_name: self.module_name.clone(),
total_checks: self.total_checks.load(Ordering::Relaxed),
consecutive_failures: self.failures.load(Ordering::Relaxed),
consecutive_successes: self.successes.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone)]
pub struct HealthStats {
pub module_name: String,
pub total_checks: u64,
pub consecutive_failures: u32,
pub consecutive_successes: u32,
}
#[derive(Debug)]
pub struct HealthRegistry {
trackers: DashMap<String, Arc<HealthTracker>>,
default_config: HealthConfig,
}
impl HealthRegistry {
pub fn new() -> Self {
Self {
trackers: DashMap::new(),
default_config: HealthConfig::default(),
}
}
pub fn get_or_create(&self, module_name: &str) -> Arc<HealthTracker> {
self.trackers
.entry(module_name.to_string())
.or_insert_with(|| {
Arc::new(HealthTracker::new(
module_name.to_string(),
self.default_config.clone(),
))
})
.clone()
}
pub async fn unhealthy_modules(&self) -> Vec<String> {
let mut result = Vec::new();
for entry in self.trackers.iter() {
if !entry.value().is_healthy().await {
result.push(entry.key().clone());
}
}
result
}
pub async fn summary(&self) -> HealthSummary {
let mut healthy = 0;
let mut unready = 0;
let mut unhealthy = 0;
let mut unknown = 0;
for entry in self.trackers.iter() {
match entry.value().status().await {
HealthStatus::Healthy => healthy += 1,
HealthStatus::Unready => unready += 1,
HealthStatus::Unhealthy => unhealthy += 1,
HealthStatus::Unknown => unknown += 1,
}
}
HealthSummary {
total: self.trackers.len(),
healthy,
unready,
unhealthy,
unknown,
}
}
}
impl Default for HealthRegistry {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct HealthSummary {
pub total: usize,
pub healthy: usize,
pub unready: usize,
pub unhealthy: usize,
pub unknown: usize,
}
use dashmap::DashMap;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_health_tracker_transitions() {
let tracker = HealthTracker::new("test".to_string(), HealthConfig::default());
assert_eq!(tracker.status().await, HealthStatus::Unknown);
tracker.record_success(100).await;
tracker.record_success(100).await;
assert_eq!(tracker.status().await, HealthStatus::Healthy);
assert!(tracker.is_healthy().await);
assert!(tracker.is_ready().await);
}
#[tokio::test]
async fn test_health_tracker_failure() {
let config = HealthConfig {
failure_threshold: 2,
..HealthConfig::default()
};
let tracker = HealthTracker::new("test".to_string(), config);
tracker.record_failure(Some("error".to_string())).await;
tracker.record_failure(Some("error".to_string())).await;
tracker.record_failure(Some("error".to_string())).await;
assert_eq!(tracker.status().await, HealthStatus::Unhealthy);
assert!(!tracker.is_healthy().await);
assert!(!tracker.is_ready().await);
}
}