use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::Instant;
#[derive(Clone)]
pub struct TokenBucket {
inner: Arc<TokenBucketInner>,
}
struct TokenBucketInner {
capacity: f64,
refill_per_sec: f64,
state: Mutex<State>,
}
struct State {
stored: f64,
last: Instant,
}
impl TokenBucket {
pub fn new(capacity: usize, refill_per_sec: f64) -> Self {
assert!(capacity > 0, "token-bucket capacity must be >= 1");
Self {
inner: Arc::new(TokenBucketInner {
capacity: capacity as f64,
refill_per_sec: refill_per_sec.max(0.0),
state: Mutex::new(State {
stored: capacity as f64,
last: Instant::now(),
}),
}),
}
}
pub fn burst(capacity: usize) -> Self {
Self::new(capacity, 0.0)
}
pub async fn acquire(&self) {
loop {
let wait = {
let mut st = self.inner.state.lock().await;
let now = Instant::now();
let elapsed = now.duration_since(st.last).as_secs_f64();
st.last = now;
st.stored =
(st.stored + elapsed * self.inner.refill_per_sec).min(self.inner.capacity);
if st.stored >= 1.0 {
st.stored -= 1.0;
return;
}
let deficit = 1.0 - st.stored;
if self.inner.refill_per_sec > 0.0 {
Duration::from_secs_f64(deficit / self.inner.refill_per_sec)
} else {
Duration::from_secs(3600)
}
};
tokio::time::sleep(wait).await;
}
}
}
impl std::fmt::Debug for TokenBucket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenBucket")
.field("capacity", &self.inner.capacity)
.field("refill_per_sec", &self.inner.refill_per_sec)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn starts_full_and_serves_up_to_capacity() {
let b = TokenBucket::new(3, 0.0);
b.acquire().await;
b.acquire().await;
b.acquire().await; }
#[tokio::test]
async fn burst_only_bounds_concurrent_acquires() {
let b = Arc::new(TokenBucket::burst(2));
let mut handles = Vec::new();
for _ in 0..5 {
let b = b.clone();
handles.push(tokio::spawn(async move {
b.acquire().await;
}));
}
tokio::task::yield_now().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let acquired = handles.iter().filter(|h| h.is_finished()).count();
assert_eq!(acquired, 2, "only the configured burst capacity may pass");
}
#[tokio::test]
async fn refill_sustains_repeated_acquire() {
let b = TokenBucket::new(1, 50.0);
b.acquire().await; for _ in 0..3 {
b.acquire().await; }
}
#[tokio::test]
async fn stored_tokens_are_capped_at_capacity() {
let b = TokenBucket::new(1, 100_000.0);
b.acquire().await; }
#[test]
#[should_panic(expected = "capacity")]
fn zero_capacity_panics() {
let _ = TokenBucket::new(0, 1.0);
}
#[tokio::test]
async fn concurrent_consumers_share_budget() {
let b = Arc::new(TokenBucket::new(3, 0.0));
let mut handles = Vec::new();
for _ in 0..4 {
let b = b.clone();
handles.push(tokio::spawn(async move {
b.acquire().await;
}));
}
tokio::task::yield_now().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let acquired = handles.iter().filter(|h| h.is_finished()).count();
assert_eq!(
acquired, 3,
"shared bucket funds all consumers up to capacity"
);
}
}