use std::sync::Mutex; use std::time::Instant; /// A thread-safe token-bucket rate limiter. pub struct TokenBucket { capacity: f64, refill_per_second: f64, state: Mutex, } struct State { tokens: f64, last_refill: Instant, } impl TokenBucket { /// Creates a full bucket with the given capacity and refill rate. pub fn new(capacity: u64, refill_per_second: f64) -> Self { assert!(capacity > 0, "capacity must be positive"); assert!( refill_per_second.is_finite() && refill_per_second > 0.0, "refill rate must be finite and positive" ); Self { capacity: capacity as f64, refill_per_second, state: Mutex::new(State { tokens: capacity as f64, last_refill: Instant::now(), }), } } /// Attempts to consume `amount` tokens without waiting. pub fn try_acquire(&self, amount: u64) -> bool { self.try_acquire_at(amount, Instant::now()) } /// Returns the number of whole tokens currently available. pub fn available(&self) -> u64 { let now = Instant::now(); let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); self.refill(&mut state, now); state.tokens.floor() as u64 } fn try_acquire_at(&self, amount: u64, now: Instant) -> bool { if amount == 0 { return true; } if amount as f64 > self.capacity { return false; } let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner()); self.refill(&mut state, now); if state.tokens >= amount as f64 { state.tokens -= amount as f64; true } else { false } } fn refill(&self, state: &mut State, now: Instant) { let elapsed = now.saturating_duration_since(state.last_refill); state.tokens = (state.tokens + elapsed.as_secs_f64() * self.refill_per_second) .min(self.capacity); state.last_refill = now; } } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn rejects_requests_when_bucket_is_empty() { let bucket = TokenBucket::new(3, 1.0); assert!(bucket.try_acquire(2)); assert!(bucket.try_acquire(1)); assert!(!bucket.try_acquire(1)); assert_eq!(bucket.available(), 0); } #[test] fn refills_over_time_without_exceeding_capacity() { let bucket = TokenBucket::new(4, 2.0); let start = Instant::now(); assert!(bucket.try_acquire_at(4, start)); assert!(!bucket.try_acquire_at(1, start)); assert!(bucket.try_acquire_at(2, start + Duration::from_secs(1))); assert!(!bucket.try_acquire_at(1, start + Duration::from_secs(1))); // Ten seconds would add twenty tokens, but capacity remains four. assert!(bucket.try_acquire_at(4, start + Duration::from_secs(11))); assert!(!bucket.try_acquire_at(1, start + Duration::from_secs(11))); } }