1use super::StoreError;
4use std::collections::HashMap;
5use std::sync::Mutex;
6
7pub trait ThrottleStore: Send + Sync {
12 fn record_failure(&self, key: &str, now: u64, window_secs: u64) -> Result<u32, StoreError>;
15 fn failure_count(&self, key: &str, now: u64, window_secs: u64) -> Result<u32, StoreError>;
20 fn is_banned(&self, key: &str, now: u64) -> Result<Option<u64>, StoreError>;
26 fn ban(&self, key: &str, until: u64) -> Result<(), StoreError>;
29 fn clear_failures(&self, key: &str) -> Result<(), StoreError>;
34 fn reset(&self, key: &str) -> Result<(), StoreError>;
36 fn purge_expired(&self, now: u64) -> Result<usize, StoreError>;
38}
39
40#[derive(Debug, Default)]
45struct Entry {
46 failures: Vec<u64>,
49 banned_until: Option<u64>,
52 window_secs: u64,
56}
57
58const MAX_TRACKED_FAILURES: usize = 1024;
69
70#[derive(Debug)]
83pub struct MemoryThrottleStore {
84 entries: Mutex<HashMap<String, Entry>>,
85}
86
87impl Default for MemoryThrottleStore {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl MemoryThrottleStore {
94 pub fn new() -> Self {
95 Self {
96 entries: Mutex::new(HashMap::new()),
97 }
98 }
99
100 fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
105 m.lock().unwrap_or_else(|e| e.into_inner())
106 }
107
108 fn cutoff(now: u64, window_secs: u64) -> u64 {
110 now.saturating_sub(window_secs)
111 }
112}
113
114impl ThrottleStore for MemoryThrottleStore {
115 fn record_failure(&self, key: &str, now: u64, window_secs: u64) -> Result<u32, StoreError> {
116 let cutoff = Self::cutoff(now, window_secs);
117 let mut g = Self::lock(&self.entries);
118 let e = g.entry(key.to_string()).or_default();
119 e.window_secs = window_secs;
120 e.failures.retain(|t| *t > cutoff);
125 if e.failures.len() < MAX_TRACKED_FAILURES {
129 e.failures.push(now);
130 }
131 Ok(e.failures.len() as u32)
132 }
133
134 fn failure_count(&self, key: &str, now: u64, window_secs: u64) -> Result<u32, StoreError> {
135 let cutoff = Self::cutoff(now, window_secs);
136 let g = Self::lock(&self.entries);
137 Ok(g.get(key)
138 .map(|e| e.failures.iter().filter(|t| **t > cutoff).count() as u32)
139 .unwrap_or(0))
140 }
141
142 fn is_banned(&self, key: &str, now: u64) -> Result<Option<u64>, StoreError> {
143 Ok(Self::lock(&self.entries)
144 .get(key)
145 .and_then(|e| e.banned_until)
146 .filter(|until| *until > now))
148 }
149
150 fn ban(&self, key: &str, until: u64) -> Result<(), StoreError> {
151 let mut g = Self::lock(&self.entries);
152 let e = g.entry(key.to_string()).or_default();
153 e.banned_until = Some(e.banned_until.map_or(until, |existing| existing.max(until)));
156 Ok(())
157 }
158
159 fn clear_failures(&self, key: &str) -> Result<(), StoreError> {
160 if let Some(e) = Self::lock(&self.entries).get_mut(key) {
161 e.failures.clear();
162 }
163 Ok(())
164 }
165
166 fn reset(&self, key: &str) -> Result<(), StoreError> {
167 Self::lock(&self.entries).remove(key);
168 Ok(())
169 }
170
171 fn purge_expired(&self, now: u64) -> Result<usize, StoreError> {
172 let mut g = Self::lock(&self.entries);
173 let before = g.len();
174 g.retain(|_, e| {
175 let banned = e.banned_until.is_some_and(|until| until > now);
179 let fresh = e
182 .failures
183 .iter()
184 .any(|t| *t > Self::cutoff(now, e.window_secs));
185 banned || fresh
186 });
187 Ok(before - g.len())
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::throttle::{Throttle, ThrottleConfig, ThrottleDecision};
195
196 const NOW: u64 = 1_000_000;
197
198 fn store() -> MemoryThrottleStore {
199 MemoryThrottleStore::new()
200 }
201
202 #[test]
203 fn record_failure_counts_within_window() {
204 let s = store();
205 assert_eq!(s.record_failure("k", NOW, 60).unwrap(), 1);
206 assert_eq!(s.record_failure("k", NOW + 1, 60).unwrap(), 2);
207 }
208
209 #[test]
210 fn failures_at_window_edge_are_dropped() {
211 let s = store();
212 s.record_failure("k", NOW, 60).unwrap();
213 assert_eq!(s.record_failure("k", NOW + 60, 60).unwrap(), 1);
215 assert_eq!(s.record_failure("k", NOW + 61, 60).unwrap(), 2);
217 }
218
219 #[test]
220 fn failure_count_is_read_only() {
221 let s = store();
222 assert_eq!(s.failure_count("k", NOW, 60).unwrap(), 0);
223 s.record_failure("k", NOW, 60).unwrap();
224 assert_eq!(s.failure_count("k", NOW, 60).unwrap(), 1);
225 for _ in 0..10 {
227 assert_eq!(s.failure_count("k", NOW, 60).unwrap(), 1);
228 }
229 assert_eq!(s.record_failure("k", NOW, 60).unwrap(), 2);
230 }
231
232 #[test]
233 fn failure_count_of_unknown_key_is_zero() {
234 let s = store();
235 assert_eq!(s.failure_count("ghost", NOW, 60).unwrap(), 0);
236 }
237
238 #[test]
239 fn failures_vec_stays_bounded_to_window() {
240 let s = store();
242 for i in 0..100 {
243 s.record_failure("k", NOW + i, 60).unwrap();
244 }
245 assert_eq!(len(&s, "k"), 60, "窗口外的失败必须真被丢弃");
247 s.record_failure("k", NOW + 1_000, 60).unwrap();
249 assert_eq!(len(&s, "k"), 1);
250 }
251
252 #[test]
253 fn frozen_clock_growth_is_capped_and_threshold_verdict_unchanged() {
254 let s = store();
256 let mut count = 0;
257 for _ in 0..(MAX_TRACKED_FAILURES + 100) {
258 count = s.record_failure("k", NOW, 60).unwrap();
259 }
260 assert_eq!(len(&s, "k"), MAX_TRACKED_FAILURES, "failures 必须有硬上限");
261 assert_eq!(count, MAX_TRACKED_FAILURES as u32, "计数在饱和值上停住");
262
263 let t = Throttle::new(
266 MemoryThrottleStore::new(),
267 ThrottleConfig {
268 threshold: 5,
269 window_secs: 60,
270 ban_secs: 900,
271 },
272 );
273 for _ in 0..(MAX_TRACKED_FAILURES + 100) {
274 t.record_failure("k", NOW).unwrap();
275 }
276 assert_eq!(
277 t.check("k", NOW),
278 ThrottleDecision::Banned { until: NOW + 900 },
279 "饱和不得改变封禁判定"
280 );
281 }
282
283 #[test]
284 fn banned_expires_at_until_exclusive() {
285 let s = store();
286 s.ban("k", NOW + 100).unwrap();
287 assert_eq!(s.is_banned("k", NOW).unwrap(), Some(NOW + 100));
288 assert_eq!(s.is_banned("k", NOW + 99).unwrap(), Some(NOW + 100));
289 assert_eq!(s.is_banned("k", NOW + 100).unwrap(), None);
290 }
291
292 #[test]
293 fn is_banned_unknown_key_is_none() {
294 let s = store();
295 assert_eq!(s.is_banned("ghost", NOW).unwrap(), None);
296 }
297
298 #[test]
299 fn reset_clears_failures_and_ban() {
300 let s = store();
301 s.record_failure("k", NOW, 60).unwrap();
302 s.ban("k", NOW + 100).unwrap();
303 s.reset("k").unwrap();
304 assert_eq!(s.failure_count("k", NOW, 60).unwrap(), 0);
305 assert_eq!(s.is_banned("k", NOW).unwrap(), None);
306 }
307
308 #[test]
309 fn reset_unknown_key_is_ok() {
310 let s = store();
311 assert!(s.reset("ghost").is_ok());
312 }
313
314 #[test]
315 fn purge_keeps_banned_and_fresh_entries() {
316 let s = store();
317 s.record_failure("stale", NOW, 60).unwrap();
318 s.record_failure("fresh", NOW + 990, 60).unwrap();
319 s.ban("banned", NOW + 5_000).unwrap();
320 assert_eq!(s.purge_expired(NOW + 1_000).unwrap(), 1);
321 assert_eq!(s.failure_count("stale", NOW + 1_000, 60).unwrap(), 0);
322 assert_eq!(s.failure_count("fresh", NOW + 1_000, 60).unwrap(), 1);
323 assert!(s.is_banned("banned", NOW + 1_000).unwrap().is_some());
324 }
325
326 #[test]
327 fn purge_with_expired_ban_drops_entry() {
328 let s = store();
329 s.record_failure("k", NOW, 60).unwrap();
330 s.ban("k", NOW + 10).unwrap();
331 assert_eq!(s.purge_expired(NOW + 100).unwrap(), 1);
333 assert_eq!(s.is_banned("k", NOW + 100).unwrap(), None);
334 }
335
336 #[test]
337 fn purge_empty_store_is_zero() {
338 let s = store();
339 assert_eq!(s.purge_expired(NOW).unwrap(), 0);
340 }
341
342 #[test]
343 fn purge_keeps_in_window_failures_after_clock_rollback() {
344 let s = store();
348 s.record_failure("k", NOW, 60).unwrap();
349 s.record_failure("k", NOW - 200, 60).unwrap();
350 assert_eq!(s.failure_count("k", NOW, 60).unwrap(), 1);
351 assert_eq!(
352 s.purge_expired(NOW).unwrap(),
353 0,
354 "窗口内仍有有效失败,不该清"
355 );
356 assert_eq!(
357 s.failure_count("k", NOW, 60).unwrap(),
358 1,
359 "计数不能被 purge 免费重置"
360 );
361 }
362
363 #[test]
364 fn ban_cannot_be_shortened_by_clock_rollback() {
365 let s = store();
366 s.ban("k", NOW + 900).unwrap();
367 s.ban("k", NOW - 5_000 + 900).unwrap();
369 assert_eq!(
370 s.is_banned("k", NOW).unwrap(),
371 Some(NOW + 900),
372 "封禁只能延长,回拨不能提前解封"
373 );
374 }
375
376 #[test]
377 fn empty_key_is_a_normal_key() {
378 let s = store();
380 assert_eq!(s.record_failure("", NOW, 60).unwrap(), 1);
381 assert_eq!(s.record_failure("", NOW, 60).unwrap(), 2);
382 assert_eq!(s.record_failure("k", NOW, 60).unwrap(), 1);
383 }
384
385 #[test]
386 fn lock_recovers_from_poisoned_mutex() {
387 let m = Mutex::new(Entry {
389 failures: vec![NOW],
390 banned_until: Some(NOW + 1),
391 window_secs: 60,
392 });
393 std::panic::catch_unwind(|| {
394 let _guard = m.lock().unwrap();
395 panic!("poison");
396 })
397 .unwrap_err();
398 assert!(m.is_poisoned());
399
400 let g = MemoryThrottleStore::lock(&m);
401 assert_eq!(g.failures, vec![NOW]);
402 assert_eq!(g.banned_until, Some(NOW + 1));
403 }
404
405 fn len(s: &MemoryThrottleStore, key: &str) -> usize {
407 MemoryThrottleStore::lock(&s.entries)
408 .get(key)
409 .map(|e| e.failures.len())
410 .unwrap_or(0)
411 }
412}