//! Rate limiting utilities use std::collections::HashMap; use std::time::{Duration, Instant}; use crate::ClientId; /// Simple token-bucket rate limiter #[derive(Debug)] pub struct RateLimiter { /// Maximum tokens (requests) per bucket max_tokens: u32, /// How often tokens are replenished refill_interval: Duration, /// Per-client state clients: HashMap, } #[derive(Debug)] struct ClientBucket { tokens: u32, last_refill: Instant, } impl RateLimiter { /// Create a new rate limiter /// /// # Arguments /// * `max_requests` - Maximum requests allowed per interval /// * `interval` - Time interval for the limit pub fn new(max_requests: u32, interval: Duration) -> Self { Self { max_tokens: max_requests, refill_interval: interval, clients: HashMap::new(), } } /// Check if a request should be allowed for the given client /// /// Returns `true` if allowed, `false` if rate limited pub fn check(&mut self, client_id: &ClientId) -> bool { let now = Instant::now(); let bucket = self.clients.entry(client_id.clone()).or_insert(ClientBucket { tokens: self.max_tokens, last_refill: now, }); // Refill tokens if interval has passed let elapsed = now.duration_since(bucket.last_refill); if elapsed >= self.refill_interval { let intervals = (elapsed.as_millis() / self.refill_interval.as_millis()) as u32; bucket.tokens = (bucket.tokens + intervals * self.max_tokens).min(self.max_tokens); bucket.last_refill = now; } // Try to consume a token if bucket.tokens > 0 { bucket.tokens -= 1; true } else { false } } /// Remove a client's rate limit state pub fn remove_client(&mut self, client_id: &ClientId) { self.clients.remove(client_id); } /// Clean up stale client entries pub fn cleanup(&mut self, stale_after: Duration) { let now = Instant::now(); self.clients.retain(|_, bucket| { now.duration_since(bucket.last_refill) < stale_after }); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_rate_limiter_allows_within_limit() { let mut limiter = RateLimiter::new(5, Duration::from_secs(1)); let client = ClientId::new(); for _ in 0..5 { assert!(limiter.check(&client)); } // 6th request should be denied assert!(!limiter.check(&client)); } #[test] fn test_rate_limiter_different_clients() { let mut limiter = RateLimiter::new(2, Duration::from_secs(1)); let client1 = ClientId::new(); let client2 = ClientId::new(); assert!(limiter.check(&client1)); assert!(limiter.check(&client1)); assert!(!limiter.check(&client1)); // Client 2 should have its own bucket assert!(limiter.check(&client2)); assert!(limiter.check(&client2)); } }