Skip to main content

arcly_http/resilience/
rate_limit.rs

1//! Lock-free fixed-window rate limiter.
2//!
3//! Lives under `resilience` (not `auth/guards`) because rate limiting is a
4//! load-shedding concern, independent of who the caller is. It still
5//! implements [`Guard`] so it composes with auth guards at handler level:
6//!
7//! ```ignore
8//! static PUBLIC_RATE: RateLimit = RateLimit::new(200, 60);
9//! PUBLIC_RATE.check(&ctx)?;
10//! ```
11
12use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use crate::auth::guards::Guard;
16use crate::web::{Error, RequestContext};
17
18/// Per-instance fixed-window limiter. Zero locks: a `(window_start, count)` pair
19/// is packed into a single `AtomicU64` and updated via CAS.
20pub struct RateLimit {
21    state: AtomicU64, // high 32 bits = window-start seconds, low 32 = count
22    window_secs: u32,
23    max_per_window: u32,
24}
25
26impl RateLimit {
27    pub const fn new(max_per_window: u32, window_secs: u32) -> Self {
28        Self {
29            state: AtomicU64::new(0),
30            window_secs,
31            max_per_window,
32        }
33    }
34
35    fn now_secs() -> u32 {
36        SystemTime::now()
37            .duration_since(UNIX_EPOCH)
38            .map(|d| d.as_secs() as u32)
39            .unwrap_or(0)
40    }
41}
42
43impl Guard for RateLimit {
44    fn check(&self, _ctx: &RequestContext) -> Result<(), Error> {
45        let now = Self::now_secs();
46        loop {
47            let cur = self.state.load(Relaxed);
48            let (start, count) = ((cur >> 32) as u32, cur as u32);
49            let (new_start, new_count) = if now.saturating_sub(start) >= self.window_secs {
50                (now, 1)
51            } else {
52                (start, count.saturating_add(1))
53            };
54            if new_count > self.max_per_window {
55                return Err(Error::TooManyRequests);
56            }
57            let next = ((new_start as u64) << 32) | (new_count as u64);
58            if self
59                .state
60                .compare_exchange(cur, next, Relaxed, Relaxed)
61                .is_ok()
62            {
63                return Ok(());
64            }
65        }
66    }
67}