use std::collections::HashMap;
use std::env;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tracing::info;
#[derive(Clone)]
pub struct RateLimiter {
max_requests: usize,
period: Duration,
requests: Arc<Mutex<HashMap<String, Vec<Instant>>>>,
}
#[derive(Clone)]
pub struct LoginRateLimiter(pub RateLimiter);
impl std::ops::Deref for LoginRateLimiter {
type Target = RateLimiter;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl RateLimiter {
pub fn new(max_requests: usize, period: Duration) -> Self {
Self {
max_requests,
period,
requests: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn check(&self, key: String) -> Result<(), Duration> {
let now = Instant::now();
let mut requests = self.requests.lock().unwrap();
let key_requests = requests.entry(key).or_default();
key_requests.retain(|&time| now.duration_since(time) < self.period);
if key_requests.len() >= self.max_requests {
if let Some(&oldest) = key_requests.first() {
let retry_after = self.period.saturating_sub(now.duration_since(oldest));
return Err(retry_after);
}
return Err(self.period);
}
key_requests.push(now);
Ok(())
}
}
pub fn create_rate_limiter() -> LoginRateLimiter {
let max_requests = env::var("RATE_LIMIT_MAX_REQUESTS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(5);
let period_secs = env::var("RATE_LIMIT_PERIOD_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(60);
info!(
max_requests = max_requests,
period_secs = period_secs,
"Configuring login rate limiter"
);
LoginRateLimiter(RateLimiter::new(
max_requests,
Duration::from_secs(period_secs),
))
}
pub fn create_global_rate_limiter() -> RateLimiter {
let max_requests = env::var("GLOBAL_RATE_LIMIT_MAX_REQUESTS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(100);
let period_secs = env::var("GLOBAL_RATE_LIMIT_PERIOD_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(60);
info!(
max_requests = max_requests,
period_secs = period_secs,
"Configuring global rate limiter"
);
RateLimiter::new(max_requests, Duration::from_secs(period_secs))
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_rate_limiter_allows_within_limit() {
let limiter = RateLimiter::new(3, Duration::from_secs(1));
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_ok());
}
#[test]
fn test_rate_limiter_blocks_over_limit() {
let limiter = RateLimiter::new(2, Duration::from_secs(1));
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_err());
}
#[test]
fn test_rate_limiter_resets_after_period() {
let limiter = RateLimiter::new(2, Duration::from_millis(100));
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_ok());
assert!(limiter.check("test_key".to_string()).is_err());
sleep(Duration::from_millis(150));
assert!(limiter.check("test_key".to_string()).is_ok());
}
#[test]
fn test_rate_limiter_separate_keys() {
let limiter = RateLimiter::new(1, Duration::from_secs(1));
assert!(limiter.check("key1".to_string()).is_ok());
assert!(limiter.check("key2".to_string()).is_ok());
assert!(limiter.check("key1".to_string()).is_err());
assert!(limiter.check("key2".to_string()).is_err());
}
}