axum_limit/policy/
fixed_window.rs1use super::{PolicyState, RateLimitPolicy};
2use crate::codec::{decode_json, encode_json, CodecError};
3use crate::quota::Quota;
4use crate::snapshot::RateLimitSnapshot;
5use crate::time::saturating_sub_ms;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, Default)]
13pub struct FixedWindowPolicy;
14
15impl RateLimitPolicy for FixedWindowPolicy {
16 type State = FixedWindowState;
17 const STATE_ID: &'static str = "fixed_window";
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FixedWindowState {
23 count: usize,
24 window_start_ms: u64,
25 window_size_ms: u64,
26 max: usize,
27}
28
29impl FixedWindowState {
30 pub(crate) fn new_at(quota: Quota, now_ms: u64) -> Self {
31 Self {
32 count: 0,
33 window_start_ms: now_ms,
34 window_size_ms: quota.per_ms,
35 max: quota.max.max(1),
36 }
37 }
38
39 fn maybe_reset_window(&mut self, now_ms: u64) {
40 if saturating_sub_ms(now_ms, self.window_start_ms) >= self.window_size_ms {
41 self.count = 0;
42 self.window_start_ms = now_ms;
43 }
44 }
45
46 fn reset_at_ms(&self) -> u64 {
47 self.window_start_ms.saturating_add(self.window_size_ms)
48 }
49}
50
51impl PolicyState for FixedWindowState {
52 fn try_acquire(&mut self, now_ms: u64) -> RateLimitSnapshot {
53 self.maybe_reset_window(now_ms);
54 let limit = self.max;
55
56 if self.count < limit {
57 self.count += 1;
58 RateLimitSnapshot {
59 allowed: true,
60 limit,
61 remaining: limit.saturating_sub(self.count),
62 reset_at_ms: self.reset_at_ms(),
63 }
64 } else {
65 RateLimitSnapshot {
66 allowed: false,
67 limit,
68 remaining: 0,
69 reset_at_ms: self.reset_at_ms(),
70 }
71 }
72 }
73
74 fn encode(&self) -> Result<Vec<u8>, CodecError> {
75 encode_json(self)
76 }
77
78 fn decode(bytes: &[u8], _quota: Quota) -> Result<Self, CodecError> {
79 decode_json(bytes)
80 }
81
82 fn create(quota: Quota, now_ms: u64) -> Self {
83 Self::new_at(quota, now_ms)
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 const BASE_MS: u64 = 2_000_000;
92
93 fn at(offset_ms: u64) -> u64 {
94 BASE_MS + offset_ms
95 }
96
97 fn window(quota: Quota) -> FixedWindowState {
98 FixedWindowState::new_at(quota, BASE_MS)
99 }
100
101 #[test]
102 fn allows_up_to_max_requests_in_window() {
103 let mut window = window(Quota::new(3, 1000));
104
105 assert!(window.try_acquire(at(0)).allowed);
106 assert!(window.try_acquire(at(100)).allowed);
107 assert!(window.try_acquire(at(500)).allowed);
108 assert!(!window.try_acquire(at(900)).allowed);
109 }
110
111 #[test]
112 fn resets_after_window_expires() {
113 let mut window = window(Quota::new(2, 1000));
114
115 assert!(window.try_acquire(at(0)).allowed);
116 assert!(window.try_acquire(at(100)).allowed);
117 assert!(!window.try_acquire(at(200)).allowed);
118
119 assert!(window.try_acquire(at(1000)).allowed);
120 assert!(window.try_acquire(at(1100)).allowed);
121 assert!(!window.try_acquire(at(1200)).allowed);
122 }
123
124 #[test]
125 fn round_trips_through_json() {
126 let mut state = window(Quota::new(3, 1000));
127 assert!(state.try_acquire(at(0)).allowed);
128 assert!(state.try_acquire(at(100)).allowed);
129
130 let encoded = state.encode().expect("encode");
131 let mut decoded = FixedWindowState::decode(&encoded, Quota::new(3, 1000)).expect("decode");
132
133 assert!(decoded.try_acquire(at(200)).allowed);
134 assert!(!decoded.try_acquire(at(300)).allowed);
135 }
136}