use axum::http::StatusCode;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::Mutex;
use super::Metrics;
#[derive(Clone)]
pub struct RateLimiter {
buckets: Arc<Mutex<HashMap<String, TokenBucket>>>,
capacity: u32,
refill_rate: f64,
}
struct TokenBucket {
tokens: f64,
last_refill: Instant,
}
impl RateLimiter {
pub fn new(capacity: u32, refill_rate: f64) -> Self {
Self {
buckets: Arc::new(Mutex::new(HashMap::new())),
capacity,
refill_rate,
}
}
async fn check(&self, key: &str) -> bool {
let mut buckets = self.buckets.lock().await;
let now = Instant::now();
let bucket = buckets.entry(key.to_string()).or_insert_with(|| {
let tokens = self.capacity as f64;
TokenBucket {
tokens,
last_refill: now,
}
});
let elapsed = now.duration_since(bucket.last_refill).as_secs_f64();
let refill = elapsed * self.refill_rate;
bucket.tokens = (bucket.tokens + refill).min(self.capacity as f64);
bucket.last_refill = now;
if bucket.tokens >= 1.0 {
bucket.tokens -= 1.0;
true
} else {
false
}
}
}
pub(super) fn rate_limiter_from_env() -> RateLimiter {
let rpm = std::env::var("RECURSIVE_RATE_LIMIT_RPM")
.ok()
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(60.0);
let burst = std::env::var("RECURSIVE_RATE_LIMIT_BURST")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(10);
let refill_rate = rpm / 60.0;
RateLimiter::new(burst, refill_rate)
}
pub(super) fn extract_client_key(req: &axum::extract::Request) -> String {
if let Some(api_key) = req.headers().get("x-api-key") {
if let Ok(key) = api_key.to_str() {
return format!("apikey:{}", key);
}
}
req.extensions()
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|info| format!("ip:{}", info.ip()))
.unwrap_or_else(|| "ip:unknown".to_string())
}
pub(super) async fn metrics_middleware(
axum::extract::State(metrics): axum::extract::State<Arc<Metrics>>,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
metrics.requests_total.fetch_add(1, Ordering::Relaxed);
metrics.requests_active.fetch_add(1, Ordering::Relaxed);
let response = next.run(req).await;
metrics.requests_active.fetch_sub(1, Ordering::Relaxed);
response
}
pub(super) async fn rate_limit_middleware(
axum::extract::State(limiter): axum::extract::State<RateLimiter>,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let key = extract_client_key(&req);
if !limiter.check(&key).await {
let mut resp = axum::response::Response::new(axum::body::Body::from("rate limit exceeded"));
*resp.status_mut() = StatusCode::TOO_MANY_REQUESTS;
return resp;
}
next.run(req).await
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn test_limiter(capacity: u32, rpm: f64) -> RateLimiter {
RateLimiter::new(capacity, rpm / 60.0)
}
#[tokio::test]
async fn test_requests_within_limit_succeed() {
let limiter = test_limiter(5, 60.0); for _ in 0..5 {
assert!(limiter.check("client-a").await, "request should be allowed");
}
}
#[tokio::test]
async fn test_requests_exceeding_limit_get_429() {
let limiter = test_limiter(3, 60.0); for _ in 0..3 {
assert!(limiter.check("client-b").await, "request should be allowed");
}
assert!(
!limiter.check("client-b").await,
"request should be rate limited"
);
}
#[tokio::test]
async fn test_tokens_refill_after_waiting() {
let limiter = test_limiter(2, 60.0); assert!(limiter.check("client-c").await);
assert!(limiter.check("client-c").await);
assert!(!limiter.check("client-c").await, "should be denied");
tokio::time::sleep(Duration::from_millis(1100)).await;
assert!(
limiter.check("client-c").await,
"should be allowed after refill"
);
}
#[tokio::test]
async fn test_different_clients_have_independent_buckets() {
let limiter = test_limiter(2, 60.0);
assert!(limiter.check("client-d").await);
assert!(limiter.check("client-d").await);
assert!(
!limiter.check("client-d").await,
"client-d should be denied"
);
assert!(
limiter.check("client-e").await,
"client-e should be allowed"
);
assert!(
limiter.check("client-e").await,
"client-e should be allowed"
);
}
#[tokio::test]
async fn test_extract_client_key_with_api_key() {
let req = axum::http::Request::builder()
.header("x-api-key", "test-key-123")
.body(axum::body::Body::empty())
.unwrap();
let key = extract_client_key(&req);
assert_eq!(key, "apikey:test-key-123");
}
#[tokio::test]
async fn test_extract_client_key_without_api_key() {
let req = axum::http::Request::builder()
.body(axum::body::Body::empty())
.unwrap();
let key = extract_client_key(&req);
assert_eq!(key, "ip:unknown");
}
#[tokio::test]
async fn test_rate_limiter_from_env_defaults() {
std::env::remove_var("RECURSIVE_RATE_LIMIT_RPM");
std::env::remove_var("RECURSIVE_RATE_LIMIT_BURST");
let limiter = rate_limiter_from_env();
for _ in 0..10 {
assert!(limiter.check("default-client").await);
}
assert!(!limiter.check("default-client").await, "burst exceeded");
}
}