oxihuman_core/
rate_limiter_sliding.rs1#![allow(dead_code)]
4
5#[derive(Clone, Debug)]
9pub struct SlidingRateLimiterConfig {
10 pub max_requests: usize,
12 pub window_ms: u64,
14}
15
16impl Default for SlidingRateLimiterConfig {
17 fn default() -> Self {
18 Self {
19 max_requests: 100,
20 window_ms: 1000,
21 }
22 }
23}
24
25pub struct SlidingRateLimiter {
27 pub config: SlidingRateLimiterConfig,
28 timestamps: Vec<u64>,
29}
30
31pub fn new_rate_limiter(config: SlidingRateLimiterConfig) -> SlidingRateLimiter {
33 SlidingRateLimiter {
34 config,
35 timestamps: Vec::new(),
36 }
37}
38
39pub fn check_and_record(limiter: &mut SlidingRateLimiter, now_ms: u64) -> bool {
41 evict_old(limiter, now_ms);
42 if limiter.timestamps.len() < limiter.config.max_requests {
43 limiter.timestamps.push(now_ms);
44 true
45 } else {
46 false
47 }
48}
49
50pub fn evict_old(limiter: &mut SlidingRateLimiter, now_ms: u64) {
52 let cutoff = now_ms.saturating_sub(limiter.config.window_ms);
53 limiter.timestamps.retain(|&t| t >= cutoff);
54}
55
56pub fn requests_in_window(limiter: &SlidingRateLimiter, now_ms: u64) -> usize {
58 let cutoff = now_ms.saturating_sub(limiter.config.window_ms);
59 limiter.timestamps.iter().filter(|&&t| t >= cutoff).count()
60}
61
62pub fn remaining_budget(limiter: &SlidingRateLimiter, now_ms: u64) -> usize {
64 let used = requests_in_window(limiter, now_ms);
65 limiter.config.max_requests.saturating_sub(used)
66}
67
68pub fn reset_limiter(limiter: &mut SlidingRateLimiter) {
70 limiter.timestamps.clear();
71}
72
73impl SlidingRateLimiter {
74 pub fn new(config: SlidingRateLimiterConfig) -> Self {
76 new_rate_limiter(config)
77 }
78
79 pub fn allow(&mut self, now_ms: u64) -> bool {
81 check_and_record(self, now_ms)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn make_limiter(max: usize, window_ms: u64) -> SlidingRateLimiter {
90 new_rate_limiter(SlidingRateLimiterConfig {
91 max_requests: max,
92 window_ms,
93 })
94 }
95
96 #[test]
97 fn test_allows_up_to_limit() {
98 let mut lim = make_limiter(3, 1000);
99 assert!(check_and_record(&mut lim, 0));
100 assert!(check_and_record(&mut lim, 100));
101 assert!(check_and_record(&mut lim, 200));
102 assert!(!check_and_record(&mut lim, 300));
104 }
105
106 #[test]
107 fn test_window_slides_allowing_new_requests() {
108 let mut lim = make_limiter(2, 1000);
109 check_and_record(&mut lim, 0);
110 check_and_record(&mut lim, 100);
111 assert!(check_and_record(&mut lim, 1100));
113 }
114
115 #[test]
116 fn test_evict_old_removes_entries() {
117 let mut lim = make_limiter(10, 1000);
118 check_and_record(&mut lim, 0);
119 evict_old(&mut lim, 2000);
120 assert_eq!(requests_in_window(&lim, 2000), 0);
121 }
122
123 #[test]
124 fn test_remaining_budget_decreases() {
125 let mut lim = make_limiter(5, 1000);
126 assert_eq!(remaining_budget(&lim, 0), 5);
127 check_and_record(&mut lim, 0);
128 assert_eq!(remaining_budget(&lim, 0), 4);
129 }
130
131 #[test]
132 fn test_reset_clears_timestamps() {
133 let mut lim = make_limiter(3, 1000);
134 check_and_record(&mut lim, 0);
135 check_and_record(&mut lim, 50);
136 reset_limiter(&mut lim);
137 assert_eq!(requests_in_window(&lim, 100), 0);
138 }
139
140 #[test]
141 fn test_requests_in_window_counts_correctly() {
142 let mut lim = make_limiter(10, 500);
143 check_and_record(&mut lim, 0);
144 check_and_record(&mut lim, 100);
145 check_and_record(&mut lim, 600);
149 assert_eq!(requests_in_window(&lim, 700), 1);
150 }
151
152 #[test]
153 fn test_allow_method_delegates() {
154 let mut lim = SlidingRateLimiter::new(SlidingRateLimiterConfig {
155 max_requests: 2,
156 window_ms: 500,
157 });
158 assert!(lim.allow(0));
159 assert!(lim.allow(100));
160 assert!(!lim.allow(200));
161 }
162
163 #[test]
164 fn test_saturating_sub_no_underflow() {
165 let mut lim = make_limiter(5, 5000);
167 check_and_record(&mut lim, 10);
168 assert_eq!(requests_in_window(&lim, 50), 1);
169 }
170
171 #[test]
172 fn test_zero_limit_denies_all() {
173 let mut lim = make_limiter(0, 1000);
174 assert!(!check_and_record(&mut lim, 100));
175 }
176}