use crate::{DbHealthChecker, HealthReport, HealthSnapshot, HealthStatus, HealthStatusProvider};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::{mpsc, Arc, Mutex, RwLock};
use std::time::{Duration, Instant};
struct CachedReport {
report: HealthReport,
cached_at: Instant,
}
struct CacheStats {
hits: u64,
misses: u64,
evictions: u64,
}
pub struct HealthCheckCache {
inner: Arc<dyn DbHealthChecker>,
ttl: Duration,
cache: RwLock<HashMap<String, CachedReport>>,
stats: Mutex<CacheStats>,
}
impl HealthCheckCache {
pub fn new(inner: Arc<dyn DbHealthChecker>, ttl: Duration) -> Self {
Self {
inner,
ttl,
cache: RwLock::new(HashMap::new()),
stats: Mutex::new(CacheStats {
hits: 0,
misses: 0,
evictions: 0,
}),
}
}
pub fn check(&self, pool: &str) -> HealthReport {
if let Ok(cache) = self.cache.read() {
if let Some(cached) = cache.get(pool) {
if cached.cached_at.elapsed() < self.ttl {
if let Ok(mut stats) = self.stats.lock() {
stats.hits += 1;
}
return cached.report.clone();
}
}
}
let report = self.inner.check(pool);
let cached = CachedReport {
report: report.clone(),
cached_at: Instant::now(),
};
if let Ok(mut cache) = self.cache.write() {
cache.insert(pool.to_string(), cached);
}
if let Ok(mut stats) = self.stats.lock() {
stats.misses += 1;
}
report
}
pub fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
pools.iter().map(|p| self.check(p)).collect()
}
pub fn invalidate(&self, pool: &str) -> bool {
let removed = if let Ok(mut cache) = self.cache.write() {
cache.remove(pool).is_some()
} else {
false
};
if removed {
if let Ok(mut stats) = self.stats.lock() {
stats.evictions += 1;
}
}
removed
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.write() {
cache.clear();
}
}
pub fn stats(&self) -> (u64, u64, u64) {
if let Ok(stats) = self.stats.lock() {
(stats.hits, stats.misses, stats.evictions)
} else {
(0, 0, 0)
}
}
pub fn hit_rate(&self) -> f64 {
if let Ok(stats) = self.stats.lock() {
let total = stats.hits + stats.misses;
if total == 0 {
0.0
} else {
stats.hits as f64 / total as f64
}
} else {
0.0
}
}
pub fn ttl(&self) -> Duration {
self.ttl
}
}
#[derive(Debug, Clone)]
pub struct CascadingReport {
pub report: HealthReport,
pub dependencies: Vec<HealthReport>,
pub all_healthy: bool,
}
pub struct CascadingHealthChecker {
checker: Arc<dyn DbHealthChecker>,
dependencies: RwLock<HashMap<String, Vec<String>>>,
}
impl CascadingHealthChecker {
pub fn new(checker: Arc<dyn DbHealthChecker>) -> Self {
Self {
checker,
dependencies: RwLock::new(HashMap::new()),
}
}
pub fn add_dependency(&self, pool: &str, depends_on: impl Into<String>) {
if let Ok(mut deps) = self.dependencies.write() {
deps.entry(pool.to_string())
.or_default()
.push(depends_on.into());
}
}
pub fn add_dependencies(&self, pool: &str, deps: Vec<String>) {
if let Ok(mut map) = self.dependencies.write() {
map.entry(pool.to_string()).or_default().extend(deps);
}
}
pub fn remove_dependency(&self, pool: &str, depends_on: &str) -> bool {
if let Ok(mut map) = self.dependencies.write() {
if let Some(deps) = map.get_mut(pool) {
let before = deps.len();
deps.retain(|d| d != depends_on);
return deps.len() < before;
}
}
false
}
pub fn dependencies(&self, pool: &str) -> Vec<String> {
if let Ok(map) = self.dependencies.read() {
map.get(pool).cloned().unwrap_or_default()
} else {
Vec::new()
}
}
pub fn clear_dependencies(&self, pool: &str) {
if let Ok(mut map) = self.dependencies.write() {
map.remove(pool);
}
}
pub fn check_with_deps(&self, pool: &str) -> CascadingReport {
let mut visited = HashSet::new();
visited.insert(pool.to_string());
let mut dep_reports = Vec::new();
self.collect_dep_reports(pool, &mut visited, &mut dep_reports);
let report = self.checker.check(pool);
let all_healthy = report.status == HealthStatus::Healthy
&& dep_reports
.iter()
.all(|r| r.status == HealthStatus::Healthy);
CascadingReport {
report,
dependencies: dep_reports,
all_healthy,
}
}
fn collect_dep_reports(
&self,
pool: &str,
visited: &mut HashSet<String>,
reports: &mut Vec<HealthReport>,
) {
let deps = self.dependencies(pool);
for dep in deps {
if visited.contains(&dep) {
continue;
}
visited.insert(dep.clone());
reports.push(self.checker.check(&dep));
self.collect_dep_reports(&dep, visited, reports);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProbeKind {
Liveness,
Readiness,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeResult {
pub kind: ProbeKind,
pub status: HealthStatus,
pub message: String,
pub timestamp: String,
}
pub struct ProbeManager {
liveness: RwLock<HashMap<String, HealthSnapshot>>,
readiness: RwLock<HashMap<String, HealthSnapshot>>,
}
impl Default for ProbeManager {
fn default() -> Self {
Self::new()
}
}
impl ProbeManager {
pub fn new() -> Self {
Self {
liveness: RwLock::new(HashMap::new()),
readiness: RwLock::new(HashMap::new()),
}
}
pub fn set_liveness(&self, name: &str, snapshot: HealthSnapshot) {
if let Ok(mut map) = self.liveness.write() {
map.insert(name.to_string(), snapshot);
}
}
pub fn set_readiness(&self, name: &str, snapshot: HealthSnapshot) {
if let Ok(mut map) = self.readiness.write() {
map.insert(name.to_string(), snapshot);
}
}
pub fn check_liveness(&self, name: &str) -> ProbeResult {
let snapshot = self.read_probe(ProbeKind::Liveness, name);
ProbeResult {
kind: ProbeKind::Liveness,
status: snapshot.status,
message: snapshot.message,
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
pub fn check_readiness(&self, name: &str) -> ProbeResult {
let snapshot = self.read_probe(ProbeKind::Readiness, name);
ProbeResult {
kind: ProbeKind::Readiness,
status: snapshot.status,
message: snapshot.message,
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
pub fn liveness_all(&self) -> Vec<ProbeResult> {
self.all_probes(ProbeKind::Liveness)
}
pub fn readiness_all(&self) -> Vec<ProbeResult> {
self.all_probes(ProbeKind::Readiness)
}
pub fn overall_liveness(&self) -> HealthStatus {
self.overall(ProbeKind::Liveness)
}
pub fn overall_readiness(&self) -> HealthStatus {
self.overall(ProbeKind::Readiness)
}
fn read_probe(&self, kind: ProbeKind, name: &str) -> HealthSnapshot {
let map = match kind {
ProbeKind::Liveness => self.liveness.read(),
ProbeKind::Readiness => self.readiness.read(),
};
match map {
Ok(guard) => guard.get(name).cloned().unwrap_or_else(|| HealthSnapshot {
status: HealthStatus::Unknown,
connection_count: 0,
slow_queries: 0,
message: format!("no {:?} probe registered for '{}'", kind, name),
}),
Err(_) => HealthSnapshot {
status: HealthStatus::Unknown,
connection_count: 0,
slow_queries: 0,
message: "lock poisoned".to_string(),
},
}
}
fn all_probes(&self, kind: ProbeKind) -> Vec<ProbeResult> {
let map = match kind {
ProbeKind::Liveness => self.liveness.read(),
ProbeKind::Readiness => self.readiness.read(),
};
let timestamp = chrono::Utc::now().to_rfc3339();
match map {
Ok(guard) => {
let mut results: Vec<ProbeResult> = guard
.values()
.map(|snap| ProbeResult {
kind,
status: snap.status.clone(),
message: snap.message.clone(),
timestamp: timestamp.clone(),
})
.collect();
results.sort_by(|a, b| a.message.cmp(&b.message));
results
}
Err(_) => Vec::new(),
}
}
fn overall(&self, kind: ProbeKind) -> HealthStatus {
let map = match kind {
ProbeKind::Liveness => self.liveness.read(),
ProbeKind::Readiness => self.readiness.read(),
};
match map {
Ok(guard) => {
if guard.is_empty() {
return HealthStatus::Unknown;
}
let mut any_unknown = false;
for snap in guard.values() {
match snap.status {
HealthStatus::Unhealthy => return HealthStatus::Unhealthy,
HealthStatus::Unknown => any_unknown = true,
HealthStatus::Healthy => {}
}
}
if any_unknown {
HealthStatus::Unknown
} else {
HealthStatus::Healthy
}
}
Err(_) => HealthStatus::Unknown,
}
}
}
struct TimeoutStats {
total_checks: u64,
timeouts: u64,
total_duration: Duration,
}
#[derive(Debug, Clone)]
pub struct TimeoutStatsSnapshot {
pub total_checks: u64,
pub timeouts: u64,
pub total_duration: Duration,
pub avg_duration: Duration,
pub timeout_rate: f64,
}
pub struct TimeoutHealthChecker {
inner: Arc<dyn HealthStatusProvider>,
timeout: Duration,
stats: Mutex<TimeoutStats>,
}
impl TimeoutHealthChecker {
pub fn new(inner: Arc<dyn HealthStatusProvider>, timeout: Duration) -> Self {
Self {
inner,
timeout,
stats: Mutex::new(TimeoutStats {
total_checks: 0,
timeouts: 0,
total_duration: Duration::ZERO,
}),
}
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn stats(&self) -> TimeoutStatsSnapshot {
if let Ok(stats) = self.stats.lock() {
let avg = if stats.total_checks > 0 {
stats.total_duration / stats.total_checks as u32
} else {
Duration::ZERO
};
let rate = if stats.total_checks > 0 {
stats.timeouts as f64 / stats.total_checks as f64
} else {
0.0
};
TimeoutStatsSnapshot {
total_checks: stats.total_checks,
timeouts: stats.timeouts,
total_duration: stats.total_duration,
avg_duration: avg,
timeout_rate: rate,
}
} else {
TimeoutStatsSnapshot {
total_checks: 0,
timeouts: 0,
total_duration: Duration::ZERO,
avg_duration: Duration::ZERO,
timeout_rate: 0.0,
}
}
}
}
impl HealthStatusProvider for TimeoutHealthChecker {
fn snapshot(&self, pool: &str) -> HealthSnapshot {
let start = Instant::now();
let (tx, rx) = mpsc::channel();
let inner = Arc::clone(&self.inner);
let pool_owned = pool.to_string();
std::thread::spawn(move || {
let result = inner.snapshot(&pool_owned);
let _ = tx.send(result);
});
let result = match rx.recv_timeout(self.timeout) {
Ok(snapshot) => snapshot,
Err(_) => {
HealthSnapshot {
status: HealthStatus::Unhealthy,
connection_count: 0,
slow_queries: 0,
message: format!(
"health check timed out after {:?} for pool '{}'",
self.timeout, pool
),
}
}
};
let elapsed = start.elapsed();
if let Ok(mut stats) = self.stats.lock() {
stats.total_checks += 1;
stats.total_duration += elapsed;
if result.status == HealthStatus::Unhealthy && result.message.contains("timed out") {
stats.timeouts += 1;
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingChecker {
call_count: AtomicU32,
status: HealthStatus,
}
impl CountingChecker {
fn new(status: HealthStatus) -> Self {
Self {
call_count: AtomicU32::new(0),
status,
}
}
fn calls(&self) -> u32 {
self.call_count.load(Ordering::SeqCst)
}
}
impl DbHealthChecker for CountingChecker {
fn check(&self, pool: &str) -> HealthReport {
self.call_count.fetch_add(1, Ordering::SeqCst);
HealthReport::new(pool).set_status(self.status.clone())
}
fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
pools.iter().map(|p| self.check(p)).collect()
}
}
struct SlowProvider {
delay: Duration,
}
impl HealthStatusProvider for SlowProvider {
fn snapshot(&self, _pool: &str) -> HealthSnapshot {
std::thread::sleep(self.delay);
HealthSnapshot::healthy()
}
}
struct FastProvider;
impl HealthStatusProvider for FastProvider {
fn snapshot(&self, _pool: &str) -> HealthSnapshot {
HealthSnapshot::healthy()
}
}
#[test]
fn test_cache_first_call_misses() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
let report = cache.check("pool-a");
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(checker.calls(), 1);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 0);
assert_eq!(misses, 1);
}
#[test]
fn test_cache_second_call_within_ttl_hits() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check("pool-a");
cache.check("pool-a");
assert_eq!(checker.calls(), 1);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 1);
assert_eq!(misses, 1);
}
#[test]
fn test_cache_expired_triggers_recheck() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_millis(50));
cache.check("pool-a");
std::thread::sleep(Duration::from_millis(60));
cache.check("pool-a");
assert_eq!(checker.calls(), 2);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 0);
assert_eq!(misses, 2);
}
#[test]
fn test_cache_invalidate_clears_entry() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check("pool-a");
assert!(cache.invalidate("pool-a"));
cache.check("pool-a");
assert_eq!(checker.calls(), 2);
let (_, _, evictions) = cache.stats();
assert_eq!(evictions, 1);
}
#[test]
fn test_cache_invalidate_missing_returns_false() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
assert!(!cache.invalidate("never-cached"));
}
#[test]
fn test_cache_clear_empties_all() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check("a");
cache.check("b");
cache.clear();
cache.check("a");
assert_eq!(checker.calls(), 3);
}
#[test]
fn test_cache_hit_rate() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check("p"); cache.check("p"); cache.check("p");
let rate = cache.hit_rate();
assert!((rate - 2.0 / 3.0).abs() < 0.01);
}
#[test]
fn test_cache_hit_rate_no_requests() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
assert_eq!(cache.hit_rate(), 0.0);
}
#[test]
fn test_cache_ttl_accessor() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker, Duration::from_secs(30));
assert_eq!(cache.ttl(), Duration::from_secs(30));
}
#[test]
fn test_cache_check_all_uses_cache() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check_all(&["a", "b"]);
cache.check_all(&["a", "b"]);
assert_eq!(checker.calls(), 2);
}
#[test]
fn test_cache_different_pools_independent() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cache = HealthCheckCache::new(checker.clone(), Duration::from_secs(60));
cache.check("a");
cache.check("b");
cache.check("a"); cache.check("b");
assert_eq!(checker.calls(), 2);
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 2);
assert_eq!(misses, 2);
}
#[test]
fn test_cascading_no_deps_returns_own_report() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
let result = cascading.check_with_deps("pool-a");
assert_eq!(result.report.status, HealthStatus::Healthy);
assert!(result.dependencies.is_empty());
assert!(result.all_healthy);
}
#[test]
fn test_cascading_with_healthy_deps() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "database");
cascading.add_dependency("app", "cache");
let result = cascading.check_with_deps("app");
assert_eq!(result.report.status, HealthStatus::Healthy);
assert_eq!(result.dependencies.len(), 2);
assert!(result.all_healthy);
}
#[test]
fn test_cascading_unhealthy_dependency_propagates() {
struct MixedChecker;
impl DbHealthChecker for MixedChecker {
fn check(&self, pool: &str) -> HealthReport {
if pool == "db-down" {
HealthReport::new(pool).set_status(HealthStatus::Unhealthy)
} else {
HealthReport::new(pool).set_healthy()
}
}
fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
pools.iter().map(|p| self.check(p)).collect()
}
}
let checker = Arc::new(MixedChecker);
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "db-down");
let result = cascading.check_with_deps("app");
assert!(!result.all_healthy);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(result.dependencies[0].status, HealthStatus::Unhealthy);
}
#[test]
fn test_cascading_nested_deps() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "middleware");
cascading.add_dependency("middleware", "database");
let result = cascading.check_with_deps("app");
assert_eq!(result.dependencies.len(), 2);
assert!(result.all_healthy);
}
#[test]
fn test_cascading_circular_dependency_no_infinite_loop() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("a", "b");
cascading.add_dependency("b", "a");
let result = cascading.check_with_deps("a");
assert_eq!(result.dependencies.len(), 1); }
#[test]
fn test_cascading_remove_dependency() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "db");
cascading.add_dependency("app", "cache");
assert_eq!(cascading.dependencies("app").len(), 2);
assert!(cascading.remove_dependency("app", "db"));
assert_eq!(cascading.dependencies("app").len(), 1);
assert_eq!(cascading.dependencies("app")[0], "cache");
}
#[test]
fn test_cascading_remove_missing_dependency_returns_false() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
assert!(!cascading.remove_dependency("app", "never-added"));
}
#[test]
fn test_cascading_clear_dependencies() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "db");
cascading.add_dependency("app", "cache");
cascading.clear_dependencies("app");
assert!(cascading.dependencies("app").is_empty());
}
#[test]
fn test_cascading_add_dependencies_batch() {
let checker = Arc::new(CountingChecker::new(HealthStatus::Healthy));
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependencies(
"app",
vec!["db".to_string(), "cache".to_string(), "queue".to_string()],
);
assert_eq!(cascading.dependencies("app").len(), 3);
}
#[test]
fn test_cascading_unknown_dependency_pool_returns_unknown() {
struct UnknownChecker;
impl DbHealthChecker for UnknownChecker {
fn check(&self, pool: &str) -> HealthReport {
HealthReport::new(pool) }
fn check_all(&self, pools: &[&str]) -> Vec<HealthReport> {
pools.iter().map(|p| self.check(p)).collect()
}
}
let checker = Arc::new(UnknownChecker);
let cascading = CascadingHealthChecker::new(checker);
cascading.add_dependency("app", "unknown-dep");
let result = cascading.check_with_deps("app");
assert!(!result.all_healthy); }
#[test]
fn test_probe_manager_default_is_unknown() {
let mgr = ProbeManager::new();
let result = mgr.check_liveness("svc");
assert_eq!(result.status, HealthStatus::Unknown);
assert_eq!(result.kind, ProbeKind::Liveness);
assert!(!result.message.is_empty());
}
#[test]
fn test_probe_manager_set_liveness_healthy() {
let mgr = ProbeManager::new();
mgr.set_liveness("svc", HealthSnapshot::healthy());
let result = mgr.check_liveness("svc");
assert_eq!(result.status, HealthStatus::Healthy);
assert_eq!(result.kind, ProbeKind::Liveness);
}
#[test]
fn test_probe_manager_set_readiness_unhealthy() {
let mgr = ProbeManager::new();
mgr.set_readiness("svc", HealthSnapshot::unhealthy("dependency down"));
let result = mgr.check_readiness("svc");
assert_eq!(result.status, HealthStatus::Unhealthy);
assert_eq!(result.message, "dependency down");
assert_eq!(result.kind, ProbeKind::Readiness);
}
#[test]
fn test_probe_manager_liveness_and_readiness_independent() {
let mgr = ProbeManager::new();
mgr.set_liveness("svc", HealthSnapshot::healthy());
mgr.set_readiness("svc", HealthSnapshot::unhealthy("warming up"));
assert_eq!(mgr.check_liveness("svc").status, HealthStatus::Healthy);
assert_eq!(mgr.check_readiness("svc").status, HealthStatus::Unhealthy);
}
#[test]
fn test_probe_manager_overall_liveness_all_healthy() {
let mgr = ProbeManager::new();
mgr.set_liveness("a", HealthSnapshot::healthy());
mgr.set_liveness("b", HealthSnapshot::healthy());
assert_eq!(mgr.overall_liveness(), HealthStatus::Healthy);
}
#[test]
fn test_probe_manager_overall_readiness_one_unhealthy() {
let mgr = ProbeManager::new();
mgr.set_readiness("a", HealthSnapshot::healthy());
mgr.set_readiness("b", HealthSnapshot::unhealthy("down"));
assert_eq!(mgr.overall_readiness(), HealthStatus::Unhealthy);
}
#[test]
fn test_probe_manager_overall_empty_returns_unknown() {
let mgr = ProbeManager::new();
assert_eq!(mgr.overall_liveness(), HealthStatus::Unknown);
assert_eq!(mgr.overall_readiness(), HealthStatus::Unknown);
}
#[test]
fn test_probe_manager_overall_one_unknown_no_unhealthy() {
let mgr = ProbeManager::new();
mgr.set_liveness("a", HealthSnapshot::healthy());
mgr.set_liveness("b", HealthSnapshot::unknown());
assert_eq!(mgr.overall_liveness(), HealthStatus::Unknown);
}
#[test]
fn test_probe_manager_liveness_all_returns_all_probes() {
let mgr = ProbeManager::new();
mgr.set_liveness("a", HealthSnapshot::healthy());
mgr.set_liveness("b", HealthSnapshot::healthy());
let results = mgr.liveness_all();
assert_eq!(results.len(), 2);
assert!(results.iter().all(|r| r.kind == ProbeKind::Liveness));
}
#[test]
fn test_probe_manager_readiness_all_returns_all_probes() {
let mgr = ProbeManager::new();
mgr.set_readiness("x", HealthSnapshot::healthy());
let results = mgr.readiness_all();
assert_eq!(results.len(), 1);
assert_eq!(results[0].kind, ProbeKind::Readiness);
}
#[test]
fn test_probe_result_serialization_roundtrip() {
let result = ProbeResult {
kind: ProbeKind::Readiness,
status: HealthStatus::Unhealthy,
message: "db down".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&result).expect("serialize");
let back: ProbeResult = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.kind, ProbeKind::Readiness);
assert_eq!(back.status, HealthStatus::Unhealthy);
assert_eq!(back.message, "db down");
}
#[test]
fn test_probe_kind_eq() {
assert_eq!(ProbeKind::Liveness, ProbeKind::Liveness);
assert_ne!(ProbeKind::Liveness, ProbeKind::Readiness);
}
#[test]
fn test_timeout_checker_fast_provider_succeeds() {
let provider = Arc::new(FastProvider);
let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
let snap = checker.snapshot("pool");
assert_eq!(snap.status, HealthStatus::Healthy);
let stats = checker.stats();
assert_eq!(stats.total_checks, 1);
assert_eq!(stats.timeouts, 0);
assert_eq!(stats.timeout_rate, 0.0);
}
#[test]
fn test_timeout_checker_slow_provider_times_out() {
let provider = Arc::new(SlowProvider {
delay: Duration::from_millis(200),
});
let checker = TimeoutHealthChecker::new(provider, Duration::from_millis(50));
let snap = checker.snapshot("pool");
assert_eq!(snap.status, HealthStatus::Unhealthy);
assert!(snap.message.contains("timed out"));
let stats = checker.stats();
assert_eq!(stats.total_checks, 1);
assert_eq!(stats.timeouts, 1);
assert!((stats.timeout_rate - 1.0).abs() < 0.01);
}
#[test]
fn test_timeout_checker_timeout_accessor() {
let provider = Arc::new(FastProvider);
let checker = TimeoutHealthChecker::new(provider, Duration::from_millis(500));
assert_eq!(checker.timeout(), Duration::from_millis(500));
}
#[test]
fn test_timeout_checker_stats_avg_duration() {
let provider = Arc::new(FastProvider);
let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
checker.snapshot("a");
checker.snapshot("b");
let stats = checker.stats();
assert_eq!(stats.total_checks, 2);
assert!(stats.avg_duration < Duration::from_millis(100));
}
#[test]
fn test_timeout_checker_stats_empty() {
let provider = Arc::new(FastProvider);
let checker = TimeoutHealthChecker::new(provider, Duration::from_secs(1));
let stats = checker.stats();
assert_eq!(stats.total_checks, 0);
assert_eq!(stats.timeouts, 0);
assert_eq!(stats.timeout_rate, 0.0);
assert_eq!(stats.avg_duration, Duration::ZERO);
}
#[test]
fn test_timeout_checker_implements_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TimeoutHealthChecker>();
assert_send_sync::<HealthCheckCache>();
assert_send_sync::<CascadingHealthChecker>();
assert_send_sync::<ProbeManager>();
}
}