use parking_lot::Mutex;
use std::time::Instant;
pub struct TokenBucket {
capacity: f64,
refill_rate: f64,
state: Mutex<BucketState>,
}
struct BucketState {
tokens: f64,
last_refill: Instant,
}
impl TokenBucket {
pub fn new(capacity: u64, refill_rate: f64) -> Self {
let capacity = capacity as f64;
Self {
capacity,
refill_rate: if refill_rate.is_finite() && refill_rate > 0.0 {
refill_rate
} else {
0.0
},
state: Mutex::new(BucketState {
tokens: capacity,
last_refill: Instant::now(),
}),
}
}
pub fn try_acquire(&self) -> bool {
self.try_acquire_n(1)
}
pub fn try_acquire_n(&self, n: u64) -> bool {
self.try_acquire_n_at(n, Instant::now())
}
pub(crate) fn try_acquire_n_at(&self, n: u64, now: Instant) -> bool {
let mut state = self.state.lock();
self.refill(&mut state, now);
let cost = n as f64;
if state.tokens >= cost {
state.tokens -= cost;
true
} else {
false
}
}
pub fn available(&self) -> u64 {
let mut state = self.state.lock();
self.refill(&mut state, Instant::now());
state.tokens.floor() as u64
}
pub fn is_full(&self) -> bool {
self.is_full_at(Instant::now())
}
pub(crate) fn is_full_at(&self, now: Instant) -> bool {
let mut state = self.state.lock();
self.refill(&mut state, now);
state.tokens >= self.capacity
}
fn refill(&self, state: &mut BucketState, now: Instant) {
let elapsed = now.saturating_duration_since(state.last_refill);
state.tokens = (state.tokens + elapsed.as_secs_f64() * self.refill_rate).min(self.capacity);
if now > state.last_refill {
state.last_refill = now;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_token_bucket_basic() {
let bucket = TokenBucket::new(10, 1.0);
for _ in 0..10 {
assert!(bucket.try_acquire());
}
assert!(!bucket.try_acquire());
}
#[test]
fn test_token_bucket_burst() {
let bucket = TokenBucket::new(5, 10.0);
assert!(bucket.try_acquire_n(5));
assert!(!bucket.try_acquire());
}
#[test]
fn a_rate_below_one_per_second_still_refills() {
let bucket = TokenBucket::new(3, 3.0 / 60.0);
let start = Instant::now();
for _ in 0..3 {
assert!(bucket.try_acquire_n_at(1, start));
}
assert!(!bucket.try_acquire_n_at(1, start));
assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(19)));
assert!(bucket.try_acquire_n_at(1, start + Duration::from_secs(20)));
assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(21)));
let later = start + Duration::from_secs(3600);
assert!(bucket.is_full_at(later));
for _ in 0..3 {
assert!(bucket.try_acquire_n_at(1, later));
}
assert!(!bucket.try_acquire_n_at(1, later));
}
#[test]
fn a_zero_or_nonsense_rate_does_not_refill_or_panic() {
let start = Instant::now();
for rate in [0.0, -1.0, f64::NAN] {
let bucket = TokenBucket::new(1, rate);
assert!(bucket.try_acquire_n_at(1, start));
assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(3600)));
}
}
}