use std::time::{Duration, Instant};
use roxy_traits::{HealthStatus, HealthTracker};
#[derive(Debug, Clone)]
pub struct HealthConfig {
pub degraded_latency: Duration,
pub unhealthy_error_rate: f64,
pub min_requests: u64,
}
impl Default for HealthConfig {
fn default() -> Self {
Self {
degraded_latency: Duration::from_millis(500),
unhealthy_error_rate: 0.5,
min_requests: 10,
}
}
}
#[derive(Debug)]
pub struct EmaHealthTracker {
latency_ema: Duration,
error_count: u64,
request_count: u64,
last_success: Option<Instant>,
config: HealthConfig,
}
impl EmaHealthTracker {
pub const fn new(config: HealthConfig) -> Self {
Self {
latency_ema: Duration::ZERO,
error_count: 0,
request_count: 0,
last_success: None,
config,
}
}
}
impl HealthTracker for EmaHealthTracker {
fn record(&mut self, duration: Duration, success: bool) {
self.request_count += 1;
if success {
self.last_success = Some(Instant::now());
} else {
self.error_count += 1;
}
self.latency_ema = (self.latency_ema + duration) / 2;
}
fn latency_ema(&self) -> Duration {
self.latency_ema
}
fn error_rate(&self) -> f64 {
if self.request_count < self.config.min_requests {
return 0.0;
}
self.error_count as f64 / self.request_count as f64
}
fn status(&self) -> HealthStatus {
let error_rate = self.error_rate();
if error_rate >= self.config.unhealthy_error_rate {
HealthStatus::Unhealthy { error_rate }
} else if self.latency_ema >= self.config.degraded_latency {
HealthStatus::Degraded { latency_ema: self.latency_ema }
} else {
HealthStatus::Healthy
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_state() {
let tracker = EmaHealthTracker::new(HealthConfig::default());
assert_eq!(tracker.latency_ema(), Duration::ZERO);
assert_eq!(tracker.error_rate(), 0.0);
assert!(matches!(tracker.status(), HealthStatus::Healthy));
}
#[test]
fn test_record_success() {
let mut tracker = EmaHealthTracker::new(HealthConfig::default());
tracker.record(Duration::from_millis(100), true);
assert_eq!(tracker.latency_ema(), Duration::from_millis(50));
assert_eq!(tracker.error_rate(), 0.0);
}
#[test]
fn test_latency_ema_calculation() {
let mut tracker = EmaHealthTracker::new(HealthConfig::default());
tracker.record(Duration::from_millis(100), true);
assert_eq!(tracker.latency_ema(), Duration::from_millis(50));
tracker.record(Duration::from_millis(100), true);
assert_eq!(tracker.latency_ema(), Duration::from_millis(75));
tracker.record(Duration::from_millis(100), true);
let ema = tracker.latency_ema();
assert!(
ema >= Duration::from_millis(87) && ema <= Duration::from_millis(88),
"Expected ~87-88ms, got {:?}",
ema
);
}
#[test]
fn test_error_rate_below_min_requests() {
let config = HealthConfig { min_requests: 10, ..Default::default() };
let mut tracker = EmaHealthTracker::new(config);
for _ in 0..5 {
tracker.record(Duration::from_millis(10), false);
}
assert_eq!(tracker.error_rate(), 0.0);
}
#[test]
fn test_error_rate_above_min_requests() {
let config = HealthConfig { min_requests: 5, ..Default::default() };
let mut tracker = EmaHealthTracker::new(config);
for _ in 0..5 {
tracker.record(Duration::from_millis(10), true);
}
for _ in 0..5 {
tracker.record(Duration::from_millis(10), false);
}
assert_eq!(tracker.error_rate(), 0.5);
}
#[test]
fn test_status_healthy() {
let config = HealthConfig {
degraded_latency: Duration::from_millis(500),
unhealthy_error_rate: 0.5,
min_requests: 1,
};
let mut tracker = EmaHealthTracker::new(config);
tracker.record(Duration::from_millis(50), true);
assert!(matches!(tracker.status(), HealthStatus::Healthy));
}
#[test]
fn test_status_degraded() {
let config = HealthConfig {
degraded_latency: Duration::from_millis(100),
unhealthy_error_rate: 0.5,
min_requests: 1,
};
let mut tracker = EmaHealthTracker::new(config);
for _ in 0..5 {
tracker.record(Duration::from_millis(200), true);
}
match tracker.status() {
HealthStatus::Degraded { latency_ema } => {
assert!(latency_ema >= Duration::from_millis(100));
}
_ => panic!("Expected Degraded status"),
}
}
#[test]
fn test_status_unhealthy() {
let config = HealthConfig {
degraded_latency: Duration::from_millis(500),
unhealthy_error_rate: 0.3,
min_requests: 5,
};
let mut tracker = EmaHealthTracker::new(config);
for _ in 0..3 {
tracker.record(Duration::from_millis(10), true);
}
for _ in 0..7 {
tracker.record(Duration::from_millis(10), false);
}
match tracker.status() {
HealthStatus::Unhealthy { error_rate } => {
assert!(error_rate >= 0.3);
}
_ => panic!("Expected Unhealthy status"),
}
}
}