use std::time::{Duration, Instant}; /// A simple token-bucket rate limiter. /// /// The bucket refills continuously at `refill_rate_per_second` up to `capacity`. /// Calls to `try_acquire` consume tokens immediately when enough are available. #[derive(Debug, Clone)] pub struct TokenBucket { capacity: f64, refill_rate_per_second: f64, tokens: f64, last_refill: Instant, } impl TokenBucket { /// Creates a full bucket. pub fn new(capacity: u32, refill_rate_per_second: f64) -> Self { assert!(capacity > 0, "capacity must be greater than zero"); assert!( refill_rate_per_second.is_finite() && refill_rate_per_second > 0.0, "refill rate must be a positive finite number" ); let capacity = capacity as f64; Self { capacity, refill_rate_per_second, tokens: capacity, last_refill: Instant::now(), } } /// Attempts to acquire one token. pub fn try_acquire(&mut self) -> bool { self.try_acquire_n(1) } /// Attempts to acquire `n` tokens. pub fn try_acquire_n(&mut self, n: u32) -> bool { if n == 0 { return true; } self.refill(); let requested = n as f64; if self.tokens >= requested { self.tokens -= requested; true } else { false } } /// Returns the current number of whole available tokens. pub fn available(&mut self) -> u32 { self.refill(); self.tokens.floor() as u32 } /// Returns how long until `n` tokens are available. pub fn wait_time_for(&mut self, n: u32) -> Duration { if n == 0 { return Duration::ZERO; } self.refill(); let requested = n as f64; if self.tokens >= requested { Duration::ZERO } else { Duration::from_secs_f64((requested - self.tokens) / self.refill_rate_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_rate_per_second).min(self.capacity); self.last_refill = now; } } } #[cfg(test)] mod tests { use super::*; #[test] fn consumes_tokens_until_empty() { let mut bucket = TokenBucket::new(2, 1.0); assert!(bucket.try_acquire()); assert!(bucket.try_acquire()); assert!(!bucket.try_acquire()); assert_eq!(bucket.available(), 0); } #[test] fn refills_over_time_without_exceeding_capacity() { let mut bucket = TokenBucket::new(3, 2.0); assert!(bucket.try_acquire_n(3)); assert!(!bucket.try_acquire()); bucket.last_refill = bucket .last_refill .checked_sub(Duration::from_secs(2)) .unwrap(); assert_eq!(bucket.available(), 3); assert!(bucket.try_acquire_n(3)); assert!(!bucket.try_acquire()); } }