use std::time::Duration;
use crate::algorithm::{current_timestamp_ms, timestamp_to_instant, Algorithm};
use crate::decision::{Decision, RateLimitInfo};
use crate::error::Result;
use crate::quota::Quota;
use crate::storage::Storage;
#[derive(Debug, Clone, Default)]
pub struct FixedWindow;
impl FixedWindow {
pub fn new() -> Self {
Self
}
fn window_start(&self, now: u64, window_ms: u64) -> u64 {
(now / window_ms) * window_ms
}
}
impl Algorithm for FixedWindow {
fn name(&self) -> &'static str {
"fixed_window"
}
async fn check_and_record<S: Storage>(
&self,
storage: &S,
key: &str,
quota: &Quota,
) -> Result<Decision> {
let now = current_timestamp_ms();
let window_ms = quota.window().as_millis() as u64;
let window_start = self.window_start(now, window_ms);
let ttl = Duration::from_millis(window_ms * 2);
let count = storage.increment(key, 1, window_start, ttl).await?;
let limit = quota.max_requests();
let remaining = limit.saturating_sub(count);
let reset_at = timestamp_to_instant(window_start + window_ms);
let window_start_instant = timestamp_to_instant(window_start);
let info = RateLimitInfo::new(limit, remaining, reset_at, window_start_instant)
.with_algorithm("fixed_window");
Ok(if count <= limit {
Decision::allowed(info)
} else {
let retry_after = Duration::from_millis(window_start + window_ms - now);
Decision::denied(info.with_retry_after(retry_after))
})
}
async fn check<S: Storage>(
&self,
storage: &S,
key: &str,
quota: &Quota,
) -> Result<Decision> {
let now = current_timestamp_ms();
let window_ms = quota.window().as_millis() as u64;
let window_start = self.window_start(now, window_ms);
let entry = storage.get(key).await?;
let count = entry
.filter(|e| e.window_start == window_start)
.map(|e| e.count)
.unwrap_or(0);
let limit = quota.max_requests();
let remaining = limit.saturating_sub(count);
let reset_at = timestamp_to_instant(window_start + window_ms);
let info = RateLimitInfo::new(limit, remaining, reset_at, timestamp_to_instant(window_start))
.with_algorithm("fixed_window");
Ok(if count < limit {
Decision::allowed(info)
} else {
let retry_after = Duration::from_millis(window_start + window_ms - now);
Decision::denied(info.with_retry_after(retry_after))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::MemoryStorage;
#[tokio::test]
async fn test_fixed_window_basic() {
let algorithm = FixedWindow::new();
let storage = MemoryStorage::new();
let quota = Quota::per_minute(5);
for i in 1..=5 {
let decision = algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_allowed(), "Request {} should be allowed", i);
}
let decision = algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_denied());
}
}