use std::time::{Duration, Instant}; /// A simple token-bucket rate limiter. /// /// The bucket starts full, refills over time up to `capacity`, and each /// successful acquisition consumes tokens. #[derive(Debug, Clone)] pub struct TokenBucket { capacity: f64, refill_per_second: f64, tokens: f64, last_refill: Instant, } impl TokenBucket { /// Creates a bucket that holds at most `capacity` tokens and refills at /// `refill_per_second` tokens per second. 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" ); let capacity = capacity as f64; Self { capacity, refill_per_second, tokens: capacity, last_refill: Instant::now(), } } /// Attempts to consume one token. pub fn try_acquire(&mut self) -> bool { self.try_acquire_n(1) } /// Attempts to consume `count` tokens. pub fn try_acquire_n(&mut self, count: u64) -> bool { if count == 0 { return true; } self.refill(); let needed = count as f64; if self.tokens >= needed { self.tokens -= needed; true } else { false } } /// Returns the currently available whole tokens. pub fn available(&mut self) -> u64 { self.refill(); self.tokens.floor() as u64 } /// Returns how long until `count` tokens are available. pub fn wait_time_for(&mut self, count: u64) -> Option { if count == 0 { return Some(Duration::ZERO); } let needed = count as f64; if needed > self.capacity { return None; } self.refill(); if self.tokens >= needed { Some(Duration::ZERO) } else { let missing = needed - self.tokens; Some(Duration::from_secs_f64(missing / self.refill_per_second)) } } fn refill(&mut self) { self.refill_at(Instant::now()); } fn refill_at(&mut self, now: Instant) { let Some(elapsed) = now.checked_duration_since(self.last_refill) else { return; }; if elapsed.is_zero() { return; } let added = elapsed.as_secs_f64() * self.refill_per_second; self.tokens = (self.tokens + added).min(self.capacity); self.last_refill = now; } } #[cfg(test)] mod tests { use super::*; #[test] fn consumes_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 now = Instant::now(); let mut bucket = TokenBucket::new(3, 2.0); assert!(bucket.try_acquire_n(3)); assert_eq!(bucket.available(), 0); bucket.refill_at(now + Duration::from_millis(500)); assert_eq!(bucket.available(), 1); bucket.refill_at(now + Duration::from_secs(10)); assert_eq!(bucket.available(), 3); } #[test] fn reports_wait_time_for_missing_tokens() { let mut bucket = TokenBucket::new(5, 2.0); assert!(bucket.try_acquire_n(4)); let wait = bucket.wait_time_for(3).unwrap(); assert!(wait >= Duration::from_millis(900)); assert!(wait <= Duration::from_millis(1100)); assert_eq!(bucket.wait_time_for(6), None); } }