use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::Mutex;
#[derive(Debug)]
pub struct TokenBucket {
capacity: u32,
tokens: f64,
rate: f64,
last_refill: Instant,
}
impl TokenBucket {
pub fn new(capacity: u32, rate: f64) -> Self {
Self {
capacity,
tokens: capacity as f64,
rate,
last_refill: Instant::now(),
}
}
fn refill(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill);
let new_tokens = elapsed.as_secs_f64() * self.rate;
self.tokens = (self.tokens + new_tokens).min(self.capacity as f64);
self.last_refill = now;
}
pub fn try_acquire(&mut self) -> bool {
self.try_acquire_n(1)
}
pub fn try_acquire_n(&mut self, n: u32) -> bool {
self.refill();
let n = n as f64;
if self.tokens >= n {
self.tokens -= n;
true
} else {
false
}
}
pub fn time_until_available(&mut self, n: u32) -> Duration {
self.refill();
let n = n as f64;
if self.tokens >= n {
return Duration::ZERO;
}
let needed = n - self.tokens;
Duration::from_secs_f64(needed / self.rate)
}
pub fn available(&mut self) -> u32 {
self.refill();
self.tokens as u32
}
pub fn capacity(&self) -> u32 {
self.capacity
}
pub fn rate(&self) -> f64 {
self.rate
}
}
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub default_rate: f64,
pub default_burst: u32,
pub host_limits: HashMap<String, (f64, u32)>,
pub wait_on_limit: bool,
pub max_wait: Duration,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
default_rate: 10.0, default_burst: 20, host_limits: HashMap::new(),
wait_on_limit: true,
max_wait: Duration::from_secs(30),
}
}
}
impl RateLimitConfig {
pub fn new(rate: f64, burst: u32) -> Self {
Self {
default_rate: rate,
default_burst: burst,
..Default::default()
}
}
#[must_use]
pub fn with_host_limit(mut self, host: &str, rate: f64, burst: u32) -> Self {
self.host_limits.insert(host.to_string(), (rate, burst));
self
}
#[must_use]
pub fn with_wait_on_limit(mut self, wait: bool) -> Self {
self.wait_on_limit = wait;
self
}
#[must_use]
pub fn with_max_wait(mut self, duration: Duration) -> Self {
self.max_wait = duration;
self
}
pub fn get_limit(&self, host: &str) -> (f64, u32) {
self.host_limits
.get(host)
.copied()
.unwrap_or((self.default_rate, self.default_burst))
}
}
#[derive(Debug)]
pub struct RateLimiter {
config: RateLimitConfig,
buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
}
impl Default for RateLimiter {
fn default() -> Self {
Self::new(RateLimitConfig::default())
}
}
impl RateLimiter {
pub fn new(config: RateLimitConfig) -> Self {
Self {
config,
buckets: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn try_acquire(&self, host: &str) -> bool {
let mut buckets = self.buckets.lock();
let (rate, burst) = self.config.get_limit(host);
let bucket = buckets
.entry(host.to_string())
.or_insert_with(|| TokenBucket::new(burst, rate));
bucket.try_acquire()
}
pub fn time_until_available(&self, host: &str) -> Duration {
let mut buckets = self.buckets.lock();
let (rate, burst) = self.config.get_limit(host);
let bucket = buckets
.entry(host.to_string())
.or_insert_with(|| TokenBucket::new(burst, rate));
bucket.time_until_available(1)
}
pub async fn acquire(&self, host: &str) -> RateLimitResult {
if self.try_acquire(host) {
return RateLimitResult::Acquired;
}
if !self.config.wait_on_limit {
return RateLimitResult::Rejected;
}
let wait_time = self.time_until_available(host);
if wait_time > self.config.max_wait {
return RateLimitResult::Rejected;
}
tokio::time::sleep(wait_time).await;
if self.try_acquire(host) {
RateLimitResult::AcquiredAfterWait(wait_time)
} else {
RateLimitResult::Rejected
}
}
pub fn available(&self, host: &str) -> u32 {
let mut buckets = self.buckets.lock();
if let Some(bucket) = buckets.get_mut(host) {
bucket.available()
} else {
let (_, burst) = self.config.get_limit(host);
burst
}
}
pub fn cleanup(&self, max_idle: Duration) {
let mut buckets = self.buckets.lock();
let now = Instant::now();
buckets.retain(|_, bucket| now.duration_since(bucket.last_refill) < max_idle);
}
pub fn host_count(&self) -> usize {
self.buckets.lock().len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RateLimitResult {
Acquired,
AcquiredAfterWait(Duration),
Rejected,
}
impl RateLimitResult {
pub fn is_acquired(&self) -> bool {
matches!(self, Self::Acquired | Self::AcquiredAfterWait(_))
}
pub fn is_rejected(&self) -> bool {
matches!(self, Self::Rejected)
}
}
#[derive(Debug)]
pub struct SlidingWindowLimiter {
window: Duration,
max_requests: u32,
requests: Arc<Mutex<HashMap<String, Vec<Instant>>>>,
}
impl SlidingWindowLimiter {
pub fn new(window: Duration, max_requests: u32) -> Self {
Self {
window,
max_requests,
requests: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn try_acquire(&self, host: &str) -> bool {
let mut requests = self.requests.lock();
let timestamps = requests.entry(host.to_string()).or_default();
let cutoff = Instant::now() - self.window;
timestamps.retain(|&t| t > cutoff);
if timestamps.len() < self.max_requests as usize {
timestamps.push(Instant::now());
true
} else {
false
}
}
pub fn current_count(&self, host: &str) -> u32 {
let mut requests = self.requests.lock();
let timestamps = requests.entry(host.to_string()).or_default();
let cutoff = Instant::now() - self.window;
timestamps.retain(|&t| t > cutoff);
timestamps.len() as u32
}
pub fn cleanup(&self) {
let mut requests = self.requests.lock();
let cutoff = Instant::now() - self.window;
for timestamps in requests.values_mut() {
timestamps.retain(|&t| t > cutoff);
}
requests.retain(|_, v| !v.is_empty());
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_token_bucket() {
let mut bucket = TokenBucket::new(10, 5.0);
assert_eq!(bucket.available(), 10);
assert!(bucket.try_acquire());
assert_eq!(bucket.available(), 9);
for _ in 0..9 {
assert!(bucket.try_acquire());
}
assert!(!bucket.try_acquire());
}
#[test]
fn test_token_bucket_refill() {
let mut bucket = TokenBucket::new(10, 100.0);
while bucket.try_acquire() {}
std::thread::sleep(Duration::from_millis(50));
assert!(bucket.available() > 0);
}
#[test]
fn test_rate_limiter() {
let config = RateLimitConfig::new(100.0, 10).with_host_limit("slow.com", 1.0, 2);
let limiter = RateLimiter::new(config);
for _ in 0..10 {
assert!(limiter.try_acquire("fast.com"));
}
assert!(limiter.try_acquire("slow.com"));
assert!(limiter.try_acquire("slow.com"));
assert!(!limiter.try_acquire("slow.com"));
}
#[test]
fn test_sliding_window() {
let limiter = SlidingWindowLimiter::new(Duration::from_secs(1), 5);
for _ in 0..5 {
assert!(limiter.try_acquire("test.com"));
}
assert!(!limiter.try_acquire("test.com"));
assert_eq!(limiter.current_count("test.com"), 5);
}
#[tokio::test]
async fn test_rate_limiter_acquire() {
let config = RateLimitConfig::new(100.0, 2)
.with_wait_on_limit(true)
.with_max_wait(Duration::from_millis(100));
let limiter = RateLimiter::new(config);
limiter.try_acquire("test.com");
limiter.try_acquire("test.com");
let result = limiter.acquire("test.com").await;
assert!(result.is_acquired());
}
}