use std::time::{Duration, Instant}; /// A simple token-bucket rate limiter. /// /// The bucket holds up to `capacity` tokens and refills continuously at /// `refill_per_second`. Each successful acquisition removes tokens. #[derive(Debug, Clone)] pub struct TokenBucket { capacity: f64, refill_per_second: f64, tokens: f64, last_refill: Instant, } impl TokenBucket { /// Creates a full bucket. pub fn new(capacity: u32, refill_per_second: f64) -> Self { assert!(capacity > 0, "capacity must be positive"); assert!( refill_per_second.is_finite() && refill_per_second > 0.0, "refill_per_second must be a positive finite number" ); Self { capacity: capacity as f64, refill_per_second, tokens: capacity as f64, last_refill: Instant::now(), } } /// Attempts to take one token immediately. pub fn try_acquire(&mut self) -> bool { self.try_acquire_n(1) } /// Attempts to take `amount` tokens immediately. pub fn try_acquire_n(&mut self, amount: u32) -> bool { self.try_acquire_n_at(amount, Instant::now()) } /// Returns how long until `amount` tokens are available. pub fn time_until_available(&mut self, amount: u32) -> Option { self.time_until_available_at(amount, Instant::now()) } /// Returns the current token count after refilling. pub fn available_tokens(&mut self) -> f64 { self.refill(Instant::now()); self.tokens } fn try_acquire_n_at(&mut self, amount: u32, now: Instant) -> bool { assert!(amount > 0, "amount must be positive"); assert!( amount as f64 <= self.capacity, "amount cannot exceed bucket capacity" ); self.refill(now); if self.tokens >= amount as f64 { self.tokens -= amount as f64; true } else { false } } fn time_until_available_at(&mut self, amount: u32, now: Instant) -> Option { assert!(amount > 0, "amount must be positive"); if amount as f64 > self.capacity { return None; } self.refill(now); if self.tokens >= amount as f64 { Some(Duration::ZERO) } else { let missing = amount as f64 - self.tokens; Some(Duration::from_secs_f64(missing / self.refill_per_second)) } } 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; } } fn main() {} #[cfg(test)] mod tests { use super::*; #[test] fn consumes_until_empty() { 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 start = Instant::now(); let mut bucket = TokenBucket { capacity: 3.0, refill_per_second: 2.0, tokens: 0.0, last_refill: start, }; assert!(!bucket.try_acquire_n_at(2, start + Duration::from_millis(500))); assert!(bucket.try_acquire_n_at(2, start + Duration::from_secs(1))); assert_eq!(bucket.tokens, 0.0); } #[test] fn reports_wait_time() { let start = Instant::now(); let mut bucket = TokenBucket { capacity: 5.0, refill_per_second: 4.0, tokens: 1.0, last_refill: start, }; let wait = bucket .time_until_available_at(3, start) .expect("request fits in bucket"); assert_eq!(wait, Duration::from_millis(500)); assert_eq!(bucket.time_until_available_at(6, start), None); } }