use std::fmt;
#[derive(Debug, Clone)]
pub struct RateLimiterMetrics {
pub total_acquired: u64,
pub total_rejected: u64,
pub total_refills: u64,
pub current_tokens: u64,
pub max_tokens: u64,
pub consecutive_rejections: u32,
pub max_wait_time_ns: u64,
pub pressure_ratio: f64,
}
impl RateLimiterMetrics {
#[inline]
pub fn success_rate(&self) -> f64 {
let total = self.total_acquired + self.total_rejected;
if total == 0 {
1.0 } else {
self.total_acquired as f64 / total as f64
}
}
#[inline]
pub fn rejection_rate(&self) -> f64 {
1.0 - self.success_rate()
}
#[inline]
pub fn is_under_pressure(&self) -> bool {
self.success_rate() < 0.5 || self.current_tokens == 0
}
#[inline]
pub fn utilization(&self) -> f64 {
if self.max_tokens == 0 {
0.0
} else {
1.0 - (self.current_tokens as f64 / self.max_tokens as f64)
}
}
#[inline]
pub fn availability_percentage(&self) -> f64 {
if self.max_tokens == 0 {
0.0
} else {
(self.current_tokens as f64 / self.max_tokens as f64) * 100.0
}
}
#[inline]
pub fn is_under_sustained_pressure(&self) -> bool {
self.consecutive_rejections > 10 || self.pressure_ratio > 0.3
}
#[inline]
pub fn max_wait_time_us(&self) -> f64 {
self.max_wait_time_ns as f64 / 1000.0
}
#[inline]
pub fn max_wait_time_ms(&self) -> f64 {
self.max_wait_time_ns as f64 / 1_000_000.0
}
#[inline]
pub fn total_requests(&self) -> u64 {
self.total_acquired + self.total_rejected
}
pub fn health_status(&self) -> HealthStatus {
if self.is_under_sustained_pressure() {
HealthStatus::Critical
} else if self.is_under_pressure() {
HealthStatus::Degraded
} else {
HealthStatus::Healthy
}
}
pub fn summary(&self) -> String {
format!(
"RateLimiter Metrics:\n\
├─ Performance:\n\
│ ├─ Success Rate: {:.2}%\n\
│ ├─ Rejection Rate: {:.2}%\n\
│ └─ Max Wait Time: {:.3}ms\n\
├─ Capacity:\n\
│ ├─ Available Tokens: {}/{}\n\
│ ├─ Utilization: {:.2}%\n\
│ └─ Availability: {:.2}%\n\
├─ Counters:\n\
│ ├─ Total Acquired: {}\n\
│ ├─ Total Rejected: {}\n\
│ ├─ Total Refills: {}\n\
│ └─ Consecutive Rejections: {}\n\
└─ Health:\n\
├─ Status: {:?}\n\
├─ Under Pressure: {}\n\
└─ Under Sustained Pressure: {}",
self.success_rate() * 100.0,
self.rejection_rate() * 100.0,
self.max_wait_time_ms(),
self.current_tokens,
self.max_tokens,
self.utilization() * 100.0,
self.availability_percentage(),
self.total_acquired,
self.total_rejected,
self.total_refills,
self.consecutive_rejections,
self.health_status(),
self.is_under_pressure(),
self.is_under_sustained_pressure()
)
}
}
impl fmt::Display for RateLimiterMetrics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Degraded,
Critical,
}
impl HealthStatus {
pub fn is_unhealthy(&self) -> bool {
!matches!(self, Self::Healthy)
}
pub fn suggested_action(&self) -> &'static str {
match self {
Self::Healthy => "No action needed",
Self::Degraded => "Monitor closely, consider increasing capacity",
Self::Critical => "Immediate action required: scale up or reduce load",
}
}
}
impl fmt::Display for HealthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Healthy => write!(f, "✅ Healthy"),
Self::Degraded => write!(f, "⚠️ Degraded"),
Self::Critical => write!(f, "🔴 Critical"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_calculations() {
let metrics = RateLimiterMetrics {
total_acquired: 80,
total_rejected: 20,
total_refills: 10,
current_tokens: 25,
max_tokens: 100,
consecutive_rejections: 5,
max_wait_time_ns: 1_000_000,
pressure_ratio: 0.2,
};
assert_eq!(metrics.success_rate(), 0.8);
assert_eq!(metrics.utilization(), 0.75);
assert!(!metrics.is_under_pressure());
assert_eq!(metrics.health_status(), HealthStatus::Healthy);
}
#[test]
fn test_health_status() {
let metrics = RateLimiterMetrics {
total_acquired: 40,
total_rejected: 60,
total_refills: 10,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 15,
max_wait_time_ns: 0,
pressure_ratio: 0.6,
};
assert!(metrics.is_under_pressure());
assert!(metrics.is_under_sustained_pressure());
assert_eq!(metrics.health_status(), HealthStatus::Critical);
}
#[test]
fn test_edge_cases() {
let metrics = RateLimiterMetrics {
total_acquired: 0,
total_rejected: 0,
total_refills: 0,
current_tokens: 50,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert_eq!(metrics.success_rate(), 1.0);
assert_eq!(metrics.utilization(), 0.5);
assert!(!metrics.is_under_pressure());
let metrics = RateLimiterMetrics {
total_acquired: 0,
total_rejected: 0,
total_refills: 0,
current_tokens: 0,
max_tokens: 0,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert_eq!(metrics.utilization(), 0.0);
assert_eq!(metrics.availability_percentage(), 0.0);
}
#[test]
fn test_health_status_methods() {
assert!(!HealthStatus::Healthy.is_unhealthy());
assert!(HealthStatus::Degraded.is_unhealthy());
assert!(HealthStatus::Critical.is_unhealthy());
assert_eq!(HealthStatus::Healthy.suggested_action(), "No action needed");
assert!(HealthStatus::Degraded
.suggested_action()
.contains("Monitor"));
assert!(HealthStatus::Critical
.suggested_action()
.contains("Immediate"));
}
#[test]
fn test_health_status_display() {
let healthy = format!("{}", HealthStatus::Healthy);
assert!(healthy.contains("Healthy"));
let degraded = format!("{}", HealthStatus::Degraded);
assert!(degraded.contains("Degraded"));
let critical = format!("{}", HealthStatus::Critical);
assert!(critical.contains("Critical"));
}
#[test]
fn test_metrics_display() {
let metrics = RateLimiterMetrics {
total_acquired: 100,
total_rejected: 20,
total_refills: 5,
current_tokens: 30,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 1_500_000,
pressure_ratio: 0.1,
};
let display = format!("{}", metrics);
assert!(display.contains("RateLimiter Metrics"));
assert!(display.contains("Success Rate"));
let summary = metrics.summary();
assert!(summary.contains("Performance"));
assert!(summary.contains("Capacity"));
assert!(summary.contains("Health"));
}
#[test]
fn test_metrics_time_conversions() {
let metrics = RateLimiterMetrics {
total_acquired: 0,
total_rejected: 0,
total_refills: 0,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 1_500_000_000, pressure_ratio: 0.0,
};
assert_eq!(metrics.max_wait_time_us(), 1_500_000.0);
assert_eq!(metrics.max_wait_time_ms(), 1_500.0);
}
#[test]
fn test_total_requests() {
let metrics = RateLimiterMetrics {
total_acquired: 75,
total_rejected: 25,
total_refills: 0,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert_eq!(metrics.total_requests(), 100);
}
#[test]
fn test_degraded_status() {
let metrics = RateLimiterMetrics {
total_acquired: 30,
total_rejected: 70,
total_refills: 0,
current_tokens: 5,
max_tokens: 100,
consecutive_rejections: 3,
max_wait_time_ns: 0,
pressure_ratio: 0.1,
};
assert!(metrics.is_under_pressure());
assert!(!metrics.is_under_sustained_pressure());
assert_eq!(metrics.health_status(), HealthStatus::Degraded);
}
#[test]
fn test_degraded_by_empty_tokens() {
let metrics = RateLimiterMetrics {
total_acquired: 90,
total_rejected: 10,
total_refills: 5,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 2,
max_wait_time_ns: 0,
pressure_ratio: 0.1,
};
assert!(metrics.is_under_pressure());
assert_eq!(metrics.health_status(), HealthStatus::Degraded);
}
#[test]
fn test_rejection_rate() {
let metrics = RateLimiterMetrics {
total_acquired: 60,
total_rejected: 40,
total_refills: 0,
current_tokens: 50,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert!((metrics.rejection_rate() - 0.4).abs() < 0.001);
assert!((metrics.success_rate() - 0.6).abs() < 0.001);
}
#[test]
fn test_sustained_pressure_by_consecutive_rejections() {
let metrics = RateLimiterMetrics {
total_acquired: 90,
total_rejected: 10,
total_refills: 0,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 15,
max_wait_time_ns: 0,
pressure_ratio: 0.05,
};
assert!(metrics.is_under_sustained_pressure());
assert_eq!(metrics.health_status(), HealthStatus::Critical);
}
#[test]
fn test_sustained_pressure_by_pressure_ratio() {
let metrics = RateLimiterMetrics {
total_acquired: 60,
total_rejected: 40,
total_refills: 0,
current_tokens: 50,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.4,
};
assert!(metrics.is_under_sustained_pressure());
}
#[test]
fn test_utilization_boundaries() {
let full = RateLimiterMetrics {
total_acquired: 0,
total_rejected: 0,
total_refills: 0,
current_tokens: 100,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert_eq!(full.utilization(), 0.0);
assert_eq!(full.availability_percentage(), 100.0);
let empty = RateLimiterMetrics {
total_acquired: 0,
total_rejected: 0,
total_refills: 0,
current_tokens: 0,
max_tokens: 100,
consecutive_rejections: 0,
max_wait_time_ns: 0,
pressure_ratio: 0.0,
};
assert_eq!(empty.utilization(), 1.0);
assert_eq!(empty.availability_percentage(), 0.0);
}
#[test]
fn test_metrics_clone() {
let metrics = RateLimiterMetrics {
total_acquired: 42,
total_rejected: 13,
total_refills: 7,
current_tokens: 30,
max_tokens: 100,
consecutive_rejections: 2,
max_wait_time_ns: 500,
pressure_ratio: 0.1,
};
let cloned = metrics.clone();
assert_eq!(cloned.total_acquired, 42);
assert_eq!(cloned.total_rejected, 13);
assert_eq!(cloned.current_tokens, 30);
}
#[test]
fn test_health_status_clone_eq() {
let h1 = HealthStatus::Healthy;
let h2 = h1;
assert_eq!(h1, h2);
let h3 = HealthStatus::Critical;
assert_ne!(h1, h3);
}
#[test]
fn test_summary_contains_all_fields() {
let metrics = RateLimiterMetrics {
total_acquired: 50,
total_rejected: 10,
total_refills: 3,
current_tokens: 40,
max_tokens: 100,
consecutive_rejections: 1,
max_wait_time_ns: 2_000_000,
pressure_ratio: 0.1,
};
let summary = metrics.summary();
assert!(summary.contains("50")); assert!(summary.contains("10")); assert!(summary.contains("3")); assert!(summary.contains("40/100")); assert!(summary.contains("Consecutive Rejections: 1"));
}
}