use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use super::limits::GuardRailViolation;
pub(super) const MAX_TRACKED_CLIENTS: usize = 100_000;
const EVICTION_SAMPLE_SIZE: usize = 16;
#[derive(Debug)]
pub struct RateLimiter {
limit_qps: std::sync::atomic::AtomicU32,
clients: parking_lot::RwLock<HashMap<String, TokenBucket>>,
}
#[derive(Debug)]
struct TokenBucket {
tokens: f64,
last_update: Instant,
}
impl RateLimiter {
#[must_use]
pub fn new(limit_qps: u32) -> Self {
Self {
limit_qps: std::sync::atomic::AtomicU32::new(limit_qps),
clients: parking_lot::RwLock::new(HashMap::new()),
}
}
pub fn check(&self, client_id: &str) -> Result<(), GuardRailViolation> {
let mut clients = self.clients.write();
let now = Instant::now();
let limit_qps = self.limit_qps.load(std::sync::atomic::Ordering::Relaxed);
let limit = f64::from(limit_qps);
if !clients.contains_key(client_id) && clients.len() >= MAX_TRACKED_CLIENTS {
Self::evict_one(&mut clients, limit);
}
let bucket = clients.entry(client_id.to_string()).or_insert(TokenBucket {
tokens: limit,
last_update: now,
});
let elapsed = now.duration_since(bucket.last_update).as_secs_f64();
bucket.tokens = (bucket.tokens + elapsed * limit).min(limit);
bucket.last_update = now;
let verdict = if bucket.tokens >= 1.0 {
bucket.tokens -= 1.0;
Ok(())
} else {
Err(GuardRailViolation::RateLimitExceeded { limit_qps })
};
drop(clients);
verdict
}
fn evict_one(clients: &mut HashMap<String, TokenBucket>, limit: f64) {
let now = Instant::now();
let mut idle: Option<String> = None;
let mut oldest: Option<(String, Instant)> = None;
for (id, bucket) in clients.iter().take(EVICTION_SAMPLE_SIZE) {
let elapsed = now.duration_since(bucket.last_update).as_secs_f64();
if (bucket.tokens + elapsed * limit).min(limit) >= limit {
idle = Some(id.clone());
break;
}
if oldest
.as_ref()
.is_none_or(|(_, ts)| bucket.last_update < *ts)
{
oldest = Some((id.clone(), bucket.last_update));
}
}
if let Some(key) = idle.or_else(|| oldest.map(|(id, _)| id)) {
clients.remove(&key);
}
}
#[cfg(test)]
pub(crate) fn tracked_clients(&self) -> usize {
self.clients.read().len()
}
pub fn exhaust(&self, client_id: &str) {
let mut clients = self.clients.write();
let now = Instant::now();
let bucket = clients.entry(client_id.to_string()).or_insert(TokenBucket {
tokens: 0.0,
last_update: now,
});
bucket.tokens = -1_000_000.0;
bucket.last_update = now;
drop(clients);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CircuitState {
Closed,
Open,
HalfOpen,
}
#[derive(Debug, Clone, Copy)]
struct CircuitCore {
state: CircuitState,
opened_at: Option<Instant>,
}
#[derive(Debug)]
pub struct CircuitBreaker {
core: parking_lot::RwLock<CircuitCore>,
failure_count: AtomicU64,
failure_threshold: u32,
recovery_seconds: u64,
}
impl CircuitBreaker {
#[must_use]
pub fn new(failure_threshold: u32, recovery_seconds: u64) -> Self {
Self {
core: parking_lot::RwLock::new(CircuitCore {
state: CircuitState::Closed,
opened_at: None,
}),
failure_count: AtomicU64::new(0),
failure_threshold,
recovery_seconds,
}
}
pub fn check(&self) -> Result<(), GuardRailViolation> {
let core = *self.core.read();
if core.state != CircuitState::Open {
return Ok(());
}
let Some(opened_at) = core.opened_at else {
return Ok(());
};
let elapsed = opened_at.elapsed().as_secs();
if elapsed < self.recovery_seconds {
return Err(GuardRailViolation::CircuitOpen {
recovery_in_seconds: self.recovery_seconds.saturating_sub(elapsed),
});
}
let mut core = self.core.write();
if core.state == CircuitState::Open {
core.state = CircuitState::HalfOpen;
}
drop(core);
Ok(())
}
pub fn record_success(&self) {
self.failure_count.store(0, Ordering::Relaxed);
let mut core = self.core.write();
if core.state == CircuitState::HalfOpen {
core.state = CircuitState::Closed;
core.opened_at = None;
}
}
pub fn record_failure(&self) {
let mut core = self.core.write();
let count = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
if count >= u64::from(self.failure_threshold)
&& (core.state == CircuitState::Closed || core.state == CircuitState::HalfOpen)
{
core.state = CircuitState::Open;
core.opened_at = Some(Instant::now());
}
}
#[must_use]
pub fn state(&self) -> CircuitState {
self.core.read().state
}
}