use std::collections::HashMap;
use std::sync::Mutex;
use super::clock::{Clock, SystemClock};
pub trait Backend: Send + Sync {
fn incr(&self, key: &str, window_start_ns: u64, ttl_ns: u64) -> u64;
fn read(&self, key: &str, window_start_ns: u64) -> u64;
}
pub struct InMemoryBackend {
inner: Mutex<Inner>,
}
struct Inner {
counters: HashMap<(String, u64), Cell>,
}
#[derive(Clone, Copy)]
struct Cell {
count: u64,
expires_ns: u64,
}
impl InMemoryBackend {
pub fn new() -> Self {
Self {
inner: Mutex::new(Inner {
counters: HashMap::new(),
}),
}
}
}
impl Default for InMemoryBackend {
fn default() -> Self {
Self::new()
}
}
impl Backend for InMemoryBackend {
fn incr(&self, key: &str, window_start_ns: u64, ttl_ns: u64) -> u64 {
let mut g = self.inner.lock().unwrap();
let now = window_start_ns;
g.counters.retain(|_, c| c.expires_ns > now);
let entry = g
.counters
.entry((key.to_string(), window_start_ns))
.or_insert(Cell {
count: 0,
expires_ns: window_start_ns.saturating_add(ttl_ns),
});
entry.count = entry.count.saturating_add(1);
entry.count
}
fn read(&self, key: &str, window_start_ns: u64) -> u64 {
let g = self.inner.lock().unwrap();
g.counters
.get(&(key.to_string(), window_start_ns))
.map(|c| c.count)
.unwrap_or(0)
}
}
pub struct DistributedLimiter {
backend: Box<dyn Backend>,
clock: Box<dyn Clock>,
limit: u64,
window_ns: u64,
}
impl DistributedLimiter {
pub fn new(backend: Box<dyn Backend>, limit: u64, window_ns: u64) -> Self {
Self::with_clock(backend, limit, window_ns, Box::new(SystemClock::new()))
}
pub fn with_clock(
backend: Box<dyn Backend>,
limit: u64,
window_ns: u64,
clock: Box<dyn Clock>,
) -> Self {
Self {
backend,
clock,
limit: limit.max(1),
window_ns: window_ns.max(1),
}
}
pub fn try_acquire(&self, key: &str) -> bool {
let now = self.clock.now_ns();
let window_start = now - (now % self.window_ns);
let count = self.backend.incr(key, window_start, self.window_ns);
count <= self.limit
}
pub fn limit(&self) -> u64 {
self.limit
}
pub fn window_ns(&self) -> u64 {
self.window_ns
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::features::clock::TestClock;
struct ArcClock(Arc<TestClock>);
impl Clock for ArcClock {
fn now_ns(&self) -> u64 {
self.0.now_ns()
}
}
fn make(limit: u64, window_ms: u64) -> (DistributedLimiter, Arc<TestClock>) {
let clk = Arc::new(TestClock::new());
let c = clk.clone();
let backend: Box<dyn Backend> = Box::new(InMemoryBackend::new());
let limiter = DistributedLimiter::with_clock(
backend,
limit,
window_ms.saturating_mul(1_000_000),
Box::new(ArcClock(c)),
);
(limiter, clk)
}
#[test]
fn first_burst_grants_up_to_limit() {
let (lim, _clk) = make(5, 1000);
for _ in 0..5 {
assert!(lim.try_acquire("user-1"));
}
assert!(!lim.try_acquire("user-1"), "limit reached");
}
#[test]
fn window_roll_resets_count() {
let (lim, clk) = make(3, 100); for _ in 0..3 {
assert!(lim.try_acquire("k"));
}
assert!(!lim.try_acquire("k"));
clk.advance_ms(150);
for _ in 0..3 {
assert!(lim.try_acquire("k"), "new window should grant");
}
assert!(!lim.try_acquire("k"));
}
#[test]
fn keys_are_isolated() {
let (lim, _clk) = make(2, 1000);
for _ in 0..2 {
assert!(lim.try_acquire("a"));
}
assert!(!lim.try_acquire("a"));
for _ in 0..2 {
assert!(lim.try_acquire("b"));
}
}
#[test]
fn backend_swap_preserves_contract() {
let clk = Arc::new(TestClock::new());
let shared: Arc<InMemoryBackend> = Arc::new(InMemoryBackend::new());
struct SharedBackend(Arc<InMemoryBackend>);
impl Backend for SharedBackend {
fn incr(&self, key: &str, ws: u64, ttl: u64) -> u64 {
self.0.incr(key, ws, ttl)
}
fn read(&self, key: &str, ws: u64) -> u64 {
self.0.read(key, ws)
}
}
let l1 = DistributedLimiter::with_clock(
Box::new(SharedBackend(shared.clone())),
4,
1_000_000_000,
Box::new(ArcClock(clk.clone())),
);
let l2 = DistributedLimiter::with_clock(
Box::new(SharedBackend(shared.clone())),
4,
1_000_000_000,
Box::new(ArcClock(clk.clone())),
);
assert!(l1.try_acquire("k"));
assert!(l1.try_acquire("k"));
assert!(l2.try_acquire("k"));
assert!(l2.try_acquire("k"));
assert!(!l1.try_acquire("k"));
assert!(!l2.try_acquire("k"));
}
#[test]
fn read_without_bump_is_observation() {
let backend = InMemoryBackend::new();
assert_eq!(backend.read("k", 0), 0);
let _ = backend.incr("k", 0, 1_000_000);
let _ = backend.incr("k", 0, 1_000_000);
assert_eq!(backend.read("k", 0), 2);
}
#[test]
fn limit_and_window_accessors() {
let (lim, _clk) = make(42, 250);
assert_eq!(lim.limit(), 42);
assert_eq!(lim.window_ns(), 250_000_000);
}
}