use std::time::{Duration, Instant}; /// A simple token-bucket rate limiter. /// /// The bucket starts full. Each successful `try_acquire` removes tokens, and /// tokens are restored over time up to `capacity`. #[derive(Debug, Clone)] pub struct TokenBucket { capacity: f64, tokens: f64, refill_per_second: f64, last_refill: Instant, } impl TokenBucket { /// Creates a bucket with `capacity` tokens and a refill rate in tokens/sec. 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 greater than zero" ); Self { capacity: capacity as f64, tokens: capacity as f64, refill_per_second, last_refill: Instant::now(), } } /// Attempts to take `amount` tokens immediately. pub fn try_acquire(&mut self, amount: u64) -> bool { self.try_acquire_at(amount, Instant::now()) } /// Returns the current number of available whole tokens. pub fn available(&mut self) -> u64 { self.refill(Instant::now()); self.tokens.floor() as u64 } /// Returns how long until `amount` tokens are expected to be available. pub fn retry_after(&mut self, amount: u64) -> Duration { assert!(amount > 0, "amount must be greater than zero"); self.refill(Instant::now()); let needed = amount as f64; if self.tokens >= needed { Duration::ZERO } else { Duration::from_secs_f64((needed - self.tokens) / self.refill_per_second) } } fn try_acquire_at(&mut self, amount: u64, now: Instant) -> bool { assert!(amount > 0, "amount must be greater than zero"); self.refill(now); let needed = amount as f64; if self.tokens >= needed { self.tokens -= needed; true } else { false } } fn refill(&mut self, now: Instant) { if now <= self.last_refill { return; } let elapsed = now.duration_since(self.last_refill).as_secs_f64(); self.tokens = (self.tokens + elapsed * self.refill_per_second).min(self.capacity); self.last_refill = now; } } #[cfg(test)] mod tests { use super::*; #[test] fn starts_full_and_rejects_when_empty() { let mut bucket = TokenBucket::new(3, 1.0); assert!(bucket.try_acquire(1)); assert!(bucket.try_acquire(2)); assert!(!bucket.try_acquire(1)); } #[test] fn refills_over_time_without_exceeding_capacity() { let start = Instant::now(); let mut bucket = TokenBucket { capacity: 5.0, tokens: 0.0, refill_per_second: 2.0, last_refill: start, }; assert!(!bucket.try_acquire_at(3, start + Duration::from_secs(1))); assert!(bucket.try_acquire_at(2, start + Duration::from_secs(1))); assert_eq!(bucket.available(), 0); bucket.refill(start + Duration::from_secs(10)); assert_eq!(bucket.tokens, 5.0); } }