use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub requests_per_second: u32,
pub burst_size: u32,
pub per_host_limiting: bool,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
requests_per_second: 10,
burst_size: 20,
per_host_limiting: true,
}
}
}
#[derive(Debug)]
struct TokenBucket {
tokens: f64,
max_tokens: f64,
refill_rate: f64,
last_refill: Instant,
}
impl TokenBucket {
fn new(max_tokens: u32, refill_rate: u32) -> Self {
Self {
tokens: max_tokens as f64,
max_tokens: max_tokens as f64,
refill_rate: refill_rate as f64,
last_refill: Instant::now(),
}
}
fn try_consume(&mut self, tokens: f64) -> bool {
self.refill();
if self.tokens >= tokens {
self.tokens -= tokens;
true
} else {
false
}
}
fn refill(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill).as_secs_f64();
self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
self.last_refill = now;
}
fn time_until_tokens(&self, needed: f64) -> Duration {
if self.tokens >= needed {
return Duration::ZERO;
}
let tokens_needed = needed - self.tokens;
let seconds_needed = tokens_needed / self.refill_rate;
Duration::from_secs_f64(seconds_needed)
}
}
pub struct RateLimiter {
config: RateLimitConfig,
global_bucket: Arc<Mutex<TokenBucket>>,
host_buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
}
impl RateLimiter {
pub fn new(config: RateLimitConfig) -> Self {
let global_bucket = TokenBucket::new(config.burst_size, config.requests_per_second);
Self {
config,
global_bucket: Arc::new(Mutex::new(global_bucket)),
host_buckets: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn check_rate_limit(&self, host: Option<&str>) -> Result<()> {
if self.config.per_host_limiting {
if let Some(host) = host {
let mut host_buckets = self.host_buckets.lock().await;
let bucket = host_buckets.entry(host.to_string()).or_insert_with(|| {
TokenBucket::new(
self.config.burst_size, self.config.requests_per_second, )
});
if !bucket.try_consume(1.0) {
let wait_time = bucket.time_until_tokens(1.0);
return Err(anyhow!(
"Rate limit exceeded for host '{}'. Retry after {:?}",
host,
wait_time
));
}
} else {
let mut global = self.global_bucket.lock().await;
if !global.try_consume(1.0) {
let wait_time = global.time_until_tokens(1.0);
return Err(anyhow!(
"Global rate limit exceeded. Retry after {:?}",
wait_time
));
}
}
} else {
let mut global = self.global_bucket.lock().await;
if !global.try_consume(1.0) {
let wait_time = global.time_until_tokens(1.0);
return Err(anyhow!(
"Global rate limit exceeded. Retry after {:?}",
wait_time
));
}
}
Ok(())
}
pub async fn wait_if_needed(&self, host: Option<&str>) -> Result<()> {
loop {
match self.check_rate_limit(host).await {
Ok(()) => return Ok(()),
Err(_e) => {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
}
#[allow(dead_code)]
pub async fn cleanup_old_hosts(&self) {
let mut host_buckets = self.host_buckets.lock().await;
if host_buckets.len() > 1000 {
host_buckets.clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_rate_limiting() {
let config = RateLimitConfig {
requests_per_second: 2,
burst_size: 4,
per_host_limiting: false,
};
let limiter = RateLimiter::new(config);
for _ in 0..4 {
assert!(limiter.check_rate_limit(None).await.is_ok());
}
assert!(limiter.check_rate_limit(None).await.is_err());
}
#[tokio::test]
async fn test_per_host_limiting() {
let config = RateLimitConfig {
requests_per_second: 10,
burst_size: 20,
per_host_limiting: true,
};
let limiter = RateLimiter::new(config);
for _ in 0..20 {
assert!(limiter.check_rate_limit(Some("host1.com")).await.is_ok());
assert!(limiter.check_rate_limit(Some("host2.com")).await.is_ok());
}
assert!(limiter.check_rate_limit(Some("host1.com")).await.is_err());
assert!(limiter.check_rate_limit(Some("host2.com")).await.is_err());
assert!(limiter.check_rate_limit(Some("host3.com")).await.is_ok());
}
}