use std::time::{Duration, Instant}; /// A simple token-bucket rate limiter. /// /// Tokens refill continuously over time up to `capacity`. /// Each successful acquisition consumes one or more tokens. #[derive(Debug, Clone)] pub struct TokenBucket { capacity: f64, tokens: f64, refill_per_second: f64, last_refill: Instant, } impl TokenBucket { /// Creates a bucket initially filled to capacity. pub fn new(capacity: u64, refill_per_second: f64) -> Self { assert!(capacity > 0, "capacity must be greater than zero"); assert!( refill_per_second.is_finite() && refill_per_second > 0.0, "refill_per_second must be finite and positive" ); Self { capacity: capacity as f64, tokens: capacity as f64, refill_per_second, last_refill: Instant::now(), } } /// Attempts to consume one token. pub fn try_acquire(&mut self) -> bool { self.try_acquire_n(1) } /// Attempts to consume `amount` tokens. pub fn try_acquire_n(&mut self, amount: u64) -> bool { if amount == 0 { return true; } self.refill(); let amount = amount as f64; if amount > self.capacity || self.tokens < amount { return false; } self.tokens -= amount; true } /// Returns the currently available whole tokens. pub fn available(&mut self) -> u64 { self.refill(); self.tokens.floor() as u64 } /// Returns how long until `amount` tokens are available. pub fn time_until_available(&mut self, amount: u64) -> Option { if amount == 0 { return Some(Duration::ZERO); } self.refill(); let amount = amount as f64; if amount > self.capacity { return None; } if self.tokens >= amount { Some(Duration::ZERO) } else { let missing = amount - self.tokens; Some(Duration::from_secs_f64(missing / self.refill_per_second)) } } fn refill(&mut self) { let now = Instant::now(); let elapsed = now.duration_since(self.last_refill).as_secs_f64(); if elapsed > 0.0 { self.tokens = (self.tokens + elapsed * self.refill_per_second).min(self.capacity); self.last_refill = now; } } } #[cfg(test)] mod tests { use super::*; #[test] fn consumes_available_tokens() { let mut bucket = TokenBucket::new(2, 10.0); assert!(bucket.try_acquire()); assert!(bucket.try_acquire()); assert!(!bucket.try_acquire()); } #[test] fn refills_over_time() { let mut bucket = TokenBucket::new(2, 10.0); assert!(bucket.try_acquire_n(2)); assert!(!bucket.try_acquire()); bucket.last_refill -= Duration::from_millis(100); assert!(bucket.try_acquire()); assert!(!bucket.try_acquire()); } }