clawft_kernel/
rate_limit.rs1use std::collections::HashMap;
11use std::sync::Mutex;
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct RateLimitConfig {
17 pub max_per_window: u32,
21 pub window: Duration,
23}
24
25impl RateLimitConfig {
26 pub const fn unlimited() -> Self {
28 Self {
29 max_per_window: 0,
30 window: Duration::from_secs(0),
31 }
32 }
33
34 pub const fn per_minute(max: u32) -> Self {
36 Self {
37 max_per_window: max,
38 window: Duration::from_secs(60),
39 }
40 }
41
42 pub const fn per_second(max: u32) -> Self {
44 Self {
45 max_per_window: max,
46 window: Duration::from_secs(1),
47 }
48 }
49
50 pub const fn is_enabled(&self) -> bool {
52 self.max_per_window > 0
53 }
54}
55
56pub const DEFAULT_PEER_ADD_RATE: RateLimitConfig = RateLimitConfig {
59 max_per_window: 1,
60 window: Duration::from_millis(100),
61};
62
63pub const DEFAULT_GOVERNANCE_EVAL_RATE: RateLimitConfig = RateLimitConfig {
67 max_per_window: 256,
68 window: Duration::from_secs(1),
69};
70
71pub struct KeyedRateLimiter {
74 config: Mutex<RateLimitConfig>,
75 windows: Mutex<HashMap<String, Vec<Instant>>>,
77}
78
79impl KeyedRateLimiter {
80 pub fn new(config: RateLimitConfig) -> Self {
82 Self {
83 config: Mutex::new(config),
84 windows: Mutex::new(HashMap::new()),
85 }
86 }
87
88 pub fn unlimited() -> Self {
90 Self::new(RateLimitConfig::unlimited())
91 }
92
93 pub fn config(&self) -> RateLimitConfig {
95 *self
96 .config
97 .lock()
98 .unwrap_or_else(|e| e.into_inner())
99 }
100
101 pub fn set_config(&self, config: RateLimitConfig) {
106 *self
107 .config
108 .lock()
109 .unwrap_or_else(|e| e.into_inner()) = config;
110 }
111
112 pub fn check(&self, key: &str) -> bool {
119 let config = self.config();
120 if !config.is_enabled() {
121 return true;
122 }
123
124 let now = Instant::now();
125 let mut map = match self.windows.lock() {
126 Ok(g) => g,
127 Err(_) => return true,
130 };
131
132 let entry = map.entry(key.to_owned()).or_default();
133 entry.retain(|ts| now.duration_since(*ts) < config.window);
134
135 if entry.len() >= config.max_per_window as usize {
136 return false;
137 }
138
139 entry.push(now);
140 true
141 }
142
143 pub fn tracked_keys(&self) -> usize {
145 self.windows
146 .lock()
147 .map(|m| m.len())
148 .unwrap_or(0)
149 }
150
151 pub fn reset(&self) {
153 if let Ok(mut m) = self.windows.lock() {
154 m.clear();
155 }
156 }
157}
158
159impl Default for KeyedRateLimiter {
160 fn default() -> Self {
161 Self::new(RateLimitConfig::per_minute(60))
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn unlimited_always_allows() {
171 let lim = KeyedRateLimiter::unlimited();
172 for _ in 0..100 {
173 assert!(lim.check("any"));
174 }
175 }
176
177 #[test]
178 fn blocks_after_quota() {
179 let lim = KeyedRateLimiter::new(RateLimitConfig {
180 max_per_window: 3,
181 window: Duration::from_secs(60),
182 });
183 assert!(lim.check("a"));
184 assert!(lim.check("a"));
185 assert!(lim.check("a"));
186 assert!(!lim.check("a"));
187 }
188
189 #[test]
190 fn separate_keys_independent() {
191 let lim = KeyedRateLimiter::new(RateLimitConfig {
192 max_per_window: 1,
193 window: Duration::from_secs(60),
194 });
195 assert!(lim.check("source-1"));
196 assert!(!lim.check("source-1"));
197 assert!(lim.check("source-2"));
198 }
199
200 #[test]
201 fn set_config_disables() {
202 let lim = KeyedRateLimiter::new(RateLimitConfig {
203 max_per_window: 1,
204 window: Duration::from_secs(60),
205 });
206 assert!(lim.check("x"));
207 assert!(!lim.check("x"));
208 lim.set_config(RateLimitConfig::unlimited());
209 assert!(lim.check("x"));
210 }
211
212 #[test]
213 fn default_constants_enabled() {
214 assert!(DEFAULT_PEER_ADD_RATE.is_enabled());
215 assert!(DEFAULT_GOVERNANCE_EVAL_RATE.is_enabled());
216 assert_eq!(DEFAULT_PEER_ADD_RATE.max_per_window, 1);
217 assert_eq!(DEFAULT_GOVERNANCE_EVAL_RATE.max_per_window, 256);
218 }
219}