use tokio::sync::Mutex;
use tokio::time::Instant;
struct BucketState {
tokens: f64,
last_refill: Instant,
}
pub(crate) struct TokenBucket {
capacity: f64,
refill_per_sec: f64,
state: Mutex<BucketState>,
}
impl TokenBucket {
pub(crate) fn new(capacity_units: u32, refill_per_sec: u32) -> Self {
let capacity = f64::from(capacity_units);
Self {
capacity,
refill_per_sec: f64::from(refill_per_sec),
state: Mutex::new(BucketState {
tokens: capacity,
last_refill: Instant::now(),
}),
}
}
pub(crate) async fn acquire(&self, cost: u32) {
let cost = f64::from(cost).min(self.capacity);
loop {
let wait = {
let mut state = self.state.lock().await;
let now = Instant::now();
let elapsed = now.duration_since(state.last_refill).as_secs_f64();
state.tokens = elapsed
.mul_add(self.refill_per_sec, state.tokens)
.min(self.capacity);
state.last_refill = now;
if state.tokens >= cost {
state.tokens -= cost;
None
} else {
let deficit = cost - state.tokens;
Some(std::time::Duration::from_secs_f64(
deficit / self.refill_per_sec,
))
}
};
match wait {
None => return,
Some(duration) => tokio::time::sleep(duration).await,
}
}
}
#[cfg(test)]
pub(crate) async fn available(&self) -> f64 {
self.state.lock().await.tokens
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::sync::Arc;
use std::time::Duration;
#[tokio::test(start_paused = true)]
async fn burst_within_capacity_resolves_instantly() {
let bucket = TokenBucket::new(250, 250);
let start = Instant::now();
for _ in 0..50 {
bucket.acquire(5).await;
}
assert_eq!(Instant::now() - start, Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn acquisition_past_capacity_waits_the_exact_deficit() {
let bucket = TokenBucket::new(250, 250);
bucket.acquire(250).await;
let start = Instant::now();
bucket.acquire(5).await;
assert_eq!(Instant::now() - start, Duration::from_millis(20));
}
#[tokio::test(start_paused = true)]
async fn sustained_rate_never_exceeds_the_configured_budget() {
let bucket = TokenBucket::new(250, 250);
let start = Instant::now();
let n = 100u32;
for _ in 0..n {
bucket.acquire(5).await;
}
let elapsed = Instant::now() - start;
let min_elapsed_secs = f64::from(n).mul_add(5.0, -250.0).max(0.0) / 250.0;
assert!(elapsed.as_secs_f64() >= min_elapsed_secs - f64::EPSILON);
}
#[tokio::test(start_paused = true)]
async fn concurrent_acquirers_share_the_same_ceiling() {
let bucket = Arc::new(TokenBucket::new(250, 250));
let start = Instant::now();
let mut handles = Vec::new();
for _ in 0..20 {
let bucket = Arc::clone(&bucket);
handles.push(tokio::spawn(async move {
for _ in 0..5 {
bucket.acquire(5).await;
}
}));
}
for handle in handles {
handle.await.unwrap();
}
let elapsed = Instant::now() - start;
assert!(elapsed >= Duration::from_secs(1) - Duration::from_millis(1));
}
#[tokio::test(start_paused = true)]
async fn acquire_above_capacity_is_clamped_and_completes() {
let bucket = TokenBucket::new(250, 250);
tokio::time::timeout(Duration::from_secs(5), bucket.acquire(10_000))
.await
.expect("acquire(cost > capacity) must not hang forever");
}
}