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)]
#[path = "distributed_backend_tests.rs"]
mod tests;