use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use http::Extensions;
use reqwest::{Request, Response};
use super::{Middleware, Next, Result};
#[derive(Debug)]
struct TokenBucket {
rate: f64,
capacity: f64,
tokens: f64,
last_refill: Instant,
}
impl TokenBucket {
fn new(rate: f64, capacity: f64) -> Self {
Self {
rate,
capacity,
tokens: capacity,
last_refill: Instant::now(),
}
}
fn try_acquire(&mut self) -> Duration {
let now = Instant::now();
let refill = now.duration_since(self.last_refill).as_secs_f64() * self.rate;
self.tokens = (self.tokens + refill).min(self.capacity);
self.last_refill = now;
if self.tokens >= 1.0 {
self.tokens -= 1.0;
Duration::ZERO
} else {
Duration::from_secs_f64((1.0 - self.tokens) / self.rate)
}
}
}
#[derive(Debug, Clone)]
pub struct ThrottleMiddleware {
rate: f64,
capacity: f64,
buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
}
impl ThrottleMiddleware {
pub fn new(rate: f64, burst_capacity: u32) -> Self {
assert!(
rate.is_finite() && rate > 0.0,
"ThrottleMiddleware rate must be a positive finite number, got {rate}"
);
Self {
rate,
capacity: f64::from(burst_capacity.max(1)),
buckets: Arc::new(Mutex::new(HashMap::new())),
}
}
fn lock_buckets(&self) -> std::sync::MutexGuard<'_, HashMap<String, TokenBucket>> {
match self.buckets.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
}
#[async_trait]
impl Middleware for ThrottleMiddleware {
async fn handle(
&self,
req: Request,
extensions: &mut Extensions,
next: Next<'_>,
) -> Result<Response> {
let key = req.url().authority().to_owned();
loop {
let wait = {
let mut buckets = self.lock_buckets();
let bucket = buckets
.entry(key.clone())
.or_insert_with(|| TokenBucket::new(self.rate, self.capacity));
bucket.try_acquire()
};
if wait.is_zero() {
break;
}
tokio::time::sleep(wait).await;
}
next.run(req, extensions).await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
#[test]
fn first_token_is_immediate_when_bucket_starts_full() {
let mut bucket = TokenBucket::new(100.0, 1.0);
assert_eq!(bucket.try_acquire(), Duration::ZERO);
}
#[test]
fn exhausted_bucket_waits_at_least_one_rate_period() {
let mut bucket = TokenBucket::new(100.0, 1.0);
assert_eq!(bucket.try_acquire(), Duration::ZERO);
let wait = bucket.try_acquire();
assert!(
wait >= Duration::from_millis(9) && wait <= Duration::from_millis(11),
"wait was {wait:?}, expected ~10ms"
);
}
#[test]
fn refill_accumulates_over_elapsed_time() {
let mut bucket = TokenBucket::new(1000.0, 10.0);
for _ in 0..5 {
assert_eq!(bucket.try_acquire(), Duration::ZERO);
}
for _ in 0..5 {
bucket.try_acquire();
}
std::thread::sleep(Duration::from_millis(20));
let wait = bucket.try_acquire();
assert!(wait < Duration::from_millis(1), "wait was {wait:?}");
}
#[test]
fn capacity_is_clamped_to_at_least_one() {
let mw = ThrottleMiddleware::new(10.0, 0);
assert_eq!(mw.capacity, 1.0);
}
#[test]
#[should_panic(expected = "positive finite")]
fn zero_rate_panics() {
ThrottleMiddleware::new(0.0, 1);
}
#[test]
#[should_panic(expected = "positive finite")]
fn negative_rate_panics() {
ThrottleMiddleware::new(-1.0, 1);
}
#[test]
#[should_panic(expected = "positive finite")]
fn infinite_rate_panics() {
ThrottleMiddleware::new(f64::INFINITY, 1);
}
}