1use std::collections::HashMap;
10use std::hash::Hash;
11use std::time::{Duration, Instant};
12
13#[cfg(test)]
14mod tests;
15
16pub trait Clock {
18 fn now(&self) -> Instant;
19}
20
21#[derive(Debug, Default, Clone, Copy)]
23pub struct SystemClock;
24
25impl Clock for SystemClock {
26 fn now(&self) -> Instant {
27 Instant::now()
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Decision {
33 Allow,
34 Deny,
35}
36
37impl Decision {
38 #[must_use]
39 pub const fn is_denied(self) -> bool {
40 matches!(self, Self::Deny)
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct Limits {
47 pub max_attempts: u32,
49 pub window: Duration,
51 pub capacity: usize,
55}
56
57struct Entry {
58 attempts: Vec<Instant>,
59 last_seen: Instant,
60}
61
62pub struct Bucket<K, C = SystemClock> {
64 entries: HashMap<K, Entry>,
65 limits: Limits,
66 clock: C,
67}
68
69impl<K: Eq + Hash + Clone> Bucket<K, SystemClock> {
70 #[must_use]
71 pub fn new(limits: Limits) -> Self {
72 Self::with_clock(limits, SystemClock)
73 }
74}
75
76impl<K: Eq + Hash + Clone, C: Clock> Bucket<K, C> {
77 pub fn with_clock(limits: Limits, clock: C) -> Self {
78 Self {
79 entries: HashMap::new(),
80 limits,
81 clock,
82 }
83 }
84
85 pub fn check(&mut self, key: &K) -> Decision {
87 let now = self.clock.now();
88 let window = self.limits.window;
89
90 if !self.entries.contains_key(key) {
91 self.make_room(now);
92 }
93
94 let entry = self.entries.entry(key.clone()).or_insert_with(|| Entry {
95 attempts: Vec::new(),
96 last_seen: now,
97 });
98 entry.last_seen = now;
99 entry.attempts.retain(|t| now.duration_since(*t) < window);
100
101 let seen = u32::try_from(entry.attempts.len()).unwrap_or(u32::MAX);
102 if seen >= self.limits.max_attempts {
103 return Decision::Deny;
104 }
105
106 entry.attempts.push(now);
107 Decision::Allow
108 }
109
110 fn make_room(&mut self, now: Instant) {
117 if self.entries.len() < self.limits.capacity {
118 return;
119 }
120
121 let window = self.limits.window;
122 self.entries
123 .retain(|_, e| now.duration_since(e.last_seen) < window);
124
125 if self.entries.len() < self.limits.capacity {
126 return;
127 }
128
129 if let Some(stale) = self
130 .entries
131 .iter()
132 .min_by_key(|(_, e)| e.last_seen)
133 .map(|(k, _)| k.clone())
134 {
135 self.entries.remove(&stale);
136 }
137 }
138
139 #[must_use]
141 pub fn tracked(&self) -> usize {
142 self.entries.len()
143 }
144}