use std::collections::VecDeque;
use std::time::Instant;
use crate::backend::RateLimitBackend;
use crate::metrics::RateLimitResult;
use crate::quota::Quota;
#[derive(Clone)]
pub struct SlidingWindowBackend {
entries: dashmap::DashMap<String, VecDeque<Instant>>,
}
impl SlidingWindowBackend {
pub fn new() -> Self {
Self {
entries: dashmap::DashMap::new(),
}
}
}
impl Default for SlidingWindowBackend {
fn default() -> Self {
Self::new()
}
}
impl RateLimitBackend for SlidingWindowBackend {
async fn check(&self, key: &str, quota: &Quota) -> RateLimitResult {
let window = quota.interval() * quota.burst;
let limit = quota.burst as u64;
let mut entry = self.entries.entry(key.to_string()).or_default();
let now = Instant::now();
let window_start = now.checked_sub(window).unwrap_or(now);
while entry.front().is_some_and(|&ts| ts < window_start) {
entry.pop_front();
}
let count = entry.len() as u64;
let allowed = count < limit;
if allowed {
entry.push_back(now);
}
let remaining = if allowed { limit - count - 1 } else { 0 };
let reset_at = entry.front().map(|&ts| ts + window).unwrap_or(now + window);
let retry_after = if allowed {
None
} else {
entry.front().map(|&ts| ts + window - now)
};
RateLimitResult {
allowed,
remaining,
reset_at,
limit,
retry_after,
}
}
}
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn sliding_window_allows_requests_within_limit() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(3);
let r1 = backend.check("key", "a).await;
assert!(r1.allowed);
assert_eq!(r1.remaining, 2);
let r2 = backend.check("key", "a).await;
assert!(r2.allowed);
assert_eq!(r2.remaining, 1);
let r3 = backend.check("key", "a).await;
assert!(r3.allowed);
assert_eq!(r3.remaining, 0);
}
#[tokio::test]
async fn sliding_window_rejects_when_exceeded() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(2);
let r1 = backend.check("key", "a).await;
assert!(r1.allowed);
let r2 = backend.check("key", "a).await;
assert!(r2.allowed);
let r3 = backend.check("key", "a).await;
assert!(!r3.allowed);
assert!(r3.retry_after.is_some());
assert_eq!(r3.remaining, 0);
}
#[tokio::test]
async fn sliding_window_keys_are_isolated() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(1);
let r1 = backend.check("a", "a).await;
assert!(r1.allowed);
let r2 = backend.check("a", "a).await;
assert!(!r2.allowed);
let r3 = backend.check("b", "a).await;
assert!(r3.allowed);
}
#[tokio::test]
async fn sliding_window_requests_expire() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(1);
let r1 = backend.check("key", "a).await;
assert!(r1.allowed);
let r2 = backend.check("key", "a).await;
assert!(!r2.allowed);
tokio::time::sleep(Duration::from_millis(1100)).await;
let r3 = backend.check("key", "a).await;
assert!(r3.allowed);
}
#[test]
fn sliding_window_new_and_default() {
let _ = SlidingWindowBackend::new();
let _ = SlidingWindowBackend::default();
}
#[tokio::test]
async fn sliding_window_resets_only_after_the_full_window() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(2);
assert!(backend.check("w", "a).await.allowed);
assert!(backend.check("w", "a).await.allowed);
assert!(!backend.check("w", "a).await.allowed);
tokio::time::sleep(Duration::from_millis(600)).await;
assert!(
!backend.check("w", "a).await.allowed,
"requests inside the full window must still count"
);
}
#[tokio::test]
async fn sliding_window_reset_at_is_in_the_future_when_allowed() {
let backend = SlidingWindowBackend::new();
let r = backend.check("reset", &Quota::per_second(2)).await;
assert!(r.allowed);
assert!(
r.reset_at > std::time::Instant::now(),
"reset_at must be in the future for an allowed request"
);
}
#[tokio::test]
async fn sliding_window_retry_after_is_within_the_window() {
let backend = SlidingWindowBackend::new();
let quota = Quota::per_second(1);
assert!(backend.check("retry", "a).await.allowed);
tokio::time::sleep(Duration::from_millis(5)).await;
let r2 = backend.check("retry", "a).await;
assert!(!r2.allowed);
let retry = r2.retry_after.expect("denied request carries retry_after");
assert!(retry > Duration::ZERO, "retry_after must be positive");
assert!(
retry <= quota.interval(),
"retry_after must not exceed the window"
);
}
}