use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use harn_clock::Clock;
use super::algorithms::{Algorithm, CellDecision, RateAlgorithm};
#[derive(Clone, Debug)]
pub struct BucketSpec {
pub algorithm: Algorithm,
pub rate_per_sec: f64,
pub capacity: u32,
}
impl BucketSpec {
pub fn new(algorithm: Algorithm, rate_per_sec: f64, capacity: u32) -> Self {
Self {
algorithm,
rate_per_sec: rate_per_sec.max(0.0),
capacity: capacity.max(1),
}
}
}
pub trait LimitStore: Send + Sync {
fn try_admit(&self, key: &str, spec: &BucketSpec, cost: u32) -> CellDecision;
}
type SharedCell = Arc<Mutex<Box<dyn RateAlgorithm>>>;
pub struct InMemoryLimitStore {
clock: Arc<dyn Clock>,
cells: Mutex<HashMap<String, SharedCell>>,
}
impl std::fmt::Debug for InMemoryLimitStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let len = self.cells.lock().map(|map| map.len()).unwrap_or(0);
f.debug_struct("InMemoryLimitStore")
.field("cells", &len)
.finish()
}
}
impl InMemoryLimitStore {
pub fn new(clock: Arc<dyn Clock>) -> Self {
Self {
clock,
cells: Mutex::new(HashMap::new()),
}
}
pub fn cell_count(&self) -> usize {
self.cells.lock().expect("limit cells poisoned").len()
}
fn cell_handle(&self, key: &str, spec: &BucketSpec, now_ms: i64) -> SharedCell {
let mut cells = self.cells.lock().expect("limit cells poisoned");
if let Some(handle) = cells.get(key) {
return handle.clone();
}
let cell = spec
.algorithm
.new_cell(spec.rate_per_sec, spec.capacity, now_ms);
let handle = Arc::new(Mutex::new(cell));
cells.insert(key.to_string(), handle.clone());
handle
}
}
impl LimitStore for InMemoryLimitStore {
fn try_admit(&self, key: &str, spec: &BucketSpec, _cost: u32) -> CellDecision {
let now_ms = self.clock.monotonic_ms();
let handle = self.cell_handle(key, spec, now_ms);
let mut cell = handle.lock().expect("limit cell poisoned");
cell.try_admit(now_ms)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use harn_clock::PausedClock;
use time::OffsetDateTime;
fn clock() -> Arc<PausedClock> {
PausedClock::new(OffsetDateTime::UNIX_EPOCH)
}
#[test]
fn in_memory_store_admits_burst_then_rejects() {
let store = InMemoryLimitStore::new(clock());
let spec = BucketSpec::new(Algorithm::TokenBucket, 1.0, 3);
for _ in 0..3 {
assert!(matches!(
store.try_admit("t:a", &spec, 1),
CellDecision::Allowed
));
}
let rejected = store.try_admit("t:a", &spec, 1);
assert!(matches!(rejected, CellDecision::Rejected { .. }));
assert!(matches!(
store.try_admit("t:b", &spec, 1),
CellDecision::Allowed
));
}
#[test]
fn in_memory_store_recovers_after_clock_advance() {
let clock = clock();
let store = InMemoryLimitStore::new(clock.clone());
let spec = BucketSpec::new(Algorithm::TokenBucket, 1.0, 1);
assert!(matches!(
store.try_admit("t:a", &spec, 1),
CellDecision::Allowed
));
assert!(matches!(
store.try_admit("t:a", &spec, 1),
CellDecision::Rejected { .. }
));
clock.advance(Duration::from_secs(1));
assert!(matches!(
store.try_admit("t:a", &spec, 1),
CellDecision::Allowed
));
}
}