use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub trait Clock: Send + Sync {
fn now(&self) -> Instant;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
}
pub struct ManualClock {
base: Instant,
offset: Mutex<Duration>,
}
impl ManualClock {
pub fn new() -> Self {
Self { base: Instant::now(), offset: Mutex::new(Duration::ZERO) }
}
pub fn advance(&self, by: Duration) {
*self.offset.lock().unwrap() += by;
}
}
impl Default for ManualClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for ManualClock {
fn now(&self) -> Instant {
self.base + *self.offset.lock().unwrap()
}
}
#[derive(Clone, Copy)]
pub struct BucketParams {
pub capacity: f64,
pub refill_per_sec: f64,
}
pub struct Acquired {
pub allowed: bool,
pub tokens_remaining: f64,
}
pub trait StateStore: Send + Sync {
fn try_acquire(&self, key: &str, cost: f64, params: BucketParams) -> Acquired;
}
#[derive(Clone, Copy)]
struct Bucket {
tokens: f64,
last: Instant,
}
struct InMemState {
buckets: HashMap<String, Bucket>,
}
const DEFAULT_MAX_TRACKED_KEYS: usize = 100_000;
pub struct InMemoryStateStore {
clock: Arc<dyn Clock>,
max_tracked_keys: usize,
inner: Mutex<InMemState>,
}
impl InMemoryStateStore {
pub fn new() -> Self {
Self::with_clock(Arc::new(SystemClock))
}
pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
Self::with_clock_and_cap(clock, DEFAULT_MAX_TRACKED_KEYS)
}
pub fn with_clock_and_cap(clock: Arc<dyn Clock>, max_tracked_keys: usize) -> Self {
Self {
clock,
max_tracked_keys: max_tracked_keys.max(1),
inner: Mutex::new(InMemState { buckets: HashMap::new() }),
}
}
pub fn tracked_keys(&self) -> usize {
self.inner.lock().unwrap().buckets.len()
}
fn sweep_full_buckets(buckets: &mut HashMap<String, Bucket>, now: Instant, params: BucketParams) {
if params.refill_per_sec <= 0.0 {
return;
}
let full_refill = Duration::from_secs_f64(params.capacity / params.refill_per_sec);
buckets.retain(|_, b| now.duration_since(b.last) < full_refill);
}
}
impl Default for InMemoryStateStore {
fn default() -> Self {
Self::new()
}
}
impl StateStore for InMemoryStateStore {
fn try_acquire(&self, key: &str, cost: f64, params: BucketParams) -> Acquired {
let now = self.clock.now();
let mut state = self.inner.lock().unwrap();
if !state.buckets.contains_key(key) && state.buckets.len() >= self.max_tracked_keys {
Self::sweep_full_buckets(&mut state.buckets, now, params);
}
let bucket = state
.buckets
.entry(key.to_string())
.or_insert(Bucket { tokens: params.capacity, last: now });
let elapsed = now.duration_since(bucket.last).as_secs_f64();
bucket.tokens = (bucket.tokens + elapsed * params.refill_per_sec).min(params.capacity);
bucket.last = now;
if bucket.tokens >= cost {
bucket.tokens -= cost;
Acquired { allowed: true, tokens_remaining: bucket.tokens }
} else {
Acquired { allowed: false, tokens_remaining: bucket.tokens }
}
}
}
#[derive(Clone)]
pub struct RateLimitState(Arc<dyn StateStore>);
impl RateLimitState {
pub fn new() -> Self {
Self::in_memory(DEFAULT_MAX_TRACKED_KEYS)
}
pub fn in_memory(max_tracked_keys: usize) -> Self {
Self::in_memory_with_clock(Arc::new(SystemClock), max_tracked_keys)
}
pub fn in_memory_with_clock(clock: Arc<dyn Clock>, max_tracked_keys: usize) -> Self {
Self(Arc::new(InMemoryStateStore::with_clock_and_cap(clock, max_tracked_keys)))
}
pub fn with_store(store: Arc<dyn StateStore>) -> Self {
Self(store)
}
pub fn try_acquire(&self, key: &str, cost: f64, params: BucketParams) -> Acquired {
self.0.try_acquire(key, cost, params)
}
}
impl Default for RateLimitState {
fn default() -> Self {
Self::new()
}
}