use std::time::Instant;
#[allow(dead_code)]
pub(crate) struct TokenBucket {
capacity: u32,
tokens: f64,
pub(crate) last_refill: Instant,
}
impl TokenBucket {
#[allow(dead_code)]
pub(crate) fn new(capacity: u32) -> Self {
Self {
capacity,
tokens: capacity as f64,
last_refill: Instant::now(),
}
}
#[allow(dead_code)]
pub(crate) fn try_consume(&mut self) -> bool {
let now = Instant::now();
let elapsed_secs = (now - self.last_refill).as_secs_f64();
self.tokens = f64::min(
self.capacity as f64,
self.tokens + self.capacity as f64 * elapsed_secs / 3600.0,
);
self.last_refill = now;
if self.tokens >= 1.0 {
self.tokens -= 1.0;
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn new_bucket_allows_up_to_capacity() {
let mut bucket = TokenBucket::new(3);
assert!(bucket.try_consume(), "First consume should succeed");
assert!(bucket.try_consume(), "Second consume should succeed");
assert!(bucket.try_consume(), "Third consume should succeed");
assert!(!bucket.try_consume(), "Fourth consume should fail (capacity exceeded)");
}
#[test]
fn bucket_refills_proportionally_after_half_hour() {
let mut bucket = TokenBucket::new(60);
for _ in 0..60 {
bucket.try_consume();
}
assert!(
!bucket.try_consume(),
"Bucket should be empty after consuming all tokens"
);
bucket.last_refill = Instant::now() - Duration::from_secs(1800);
assert!(
bucket.try_consume(),
"Should be able to consume after refill (30 tokens refilled in 30 min)"
);
}
#[test]
fn bucket_does_not_exceed_capacity_on_refill() {
let mut bucket = TokenBucket::new(10);
bucket.last_refill = Instant::now() - Duration::from_secs(7200);
for _ in 0..10 {
assert!(bucket.try_consume(), "Should consume token");
}
assert!(!bucket.try_consume(), "11th consume should fail (capacity is 10)");
}
}