fraiseql_core/validation/
rate_limiting.rs1use std::{
11 num::NonZeroUsize,
12 sync::{Arc, Mutex},
13};
14
15use lru::LruCache;
16
17use crate::{
18 error::FraiseQLError,
19 utils::clock::{Clock, SystemClock},
20};
21
22const MAX_RATE_LIMITER_ENTRIES: usize = 100_000;
25
26#[derive(Debug, Clone)]
28pub struct RateLimitDimension {
29 pub max_requests: u32,
31 pub window_secs: u64,
33}
34
35impl RateLimitDimension {
36 const fn is_rate_limited(&self) -> bool {
37 self.max_requests == 0
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct ValidationRateLimitingConfig {
44 pub enabled: bool,
46 pub validation_errors_max_requests: u32,
48 pub validation_errors_window_secs: u64,
50 pub depth_errors_max_requests: u32,
52 pub depth_errors_window_secs: u64,
54 pub complexity_errors_max_requests: u32,
56 pub complexity_errors_window_secs: u64,
58 pub malformed_errors_max_requests: u32,
60 pub malformed_errors_window_secs: u64,
62 pub async_validation_errors_max_requests: u32,
64 pub async_validation_errors_window_secs: u64,
66}
67
68impl Default for ValidationRateLimitingConfig {
69 fn default() -> Self {
70 Self {
71 enabled: true,
72 validation_errors_max_requests: 100,
73 validation_errors_window_secs: 60,
74 depth_errors_max_requests: 50,
75 depth_errors_window_secs: 60,
76 complexity_errors_max_requests: 30,
77 complexity_errors_window_secs: 60,
78 malformed_errors_max_requests: 40,
79 malformed_errors_window_secs: 60,
80 async_validation_errors_max_requests: 60,
81 async_validation_errors_window_secs: 60,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88struct RequestRecord {
89 count: u32,
91 window_start: u64,
93}
94
95struct DimensionRateLimiter {
97 records: Arc<Mutex<LruCache<String, RequestRecord>>>,
98 dimension: RateLimitDimension,
99 clock: Arc<dyn Clock>,
100}
101
102impl DimensionRateLimiter {
103 #[cfg(test)]
104 fn new(max_requests: u32, window_secs: u64) -> Self {
105 Self::new_with_clock(max_requests, window_secs, Arc::new(SystemClock))
106 }
107
108 fn new_with_clock(max_requests: u32, window_secs: u64, clock: Arc<dyn Clock>) -> Self {
109 #[allow(clippy::expect_used)]
110 let cap = NonZeroUsize::new(MAX_RATE_LIMITER_ENTRIES)
112 .expect("MAX_RATE_LIMITER_ENTRIES must be > 0");
113 Self {
114 records: Arc::new(Mutex::new(LruCache::new(cap))),
115 dimension: RateLimitDimension {
116 max_requests,
117 window_secs,
118 },
119 clock,
120 }
121 }
122
123 fn check(&self, key: &str) -> Result<(), FraiseQLError> {
124 if self.dimension.is_rate_limited() {
125 return Ok(());
126 }
127
128 let mut records = self.records.lock().expect("records mutex poisoned");
129 let now = self.clock.now_secs();
130
131 let record = records.get_or_insert_mut(key.to_string(), || RequestRecord {
134 count: 0,
135 window_start: now,
136 });
137
138 if now >= record.window_start + self.dimension.window_secs {
140 record.count = 1;
142 record.window_start = now;
143 Ok(())
144 } else if record.count < self.dimension.max_requests {
145 record.count += 1;
147 Ok(())
148 } else {
149 Err(FraiseQLError::RateLimited {
151 message: "Rate limit exceeded for validation errors".to_string(),
152 retry_after_secs: self.dimension.window_secs,
153 })
154 }
155 }
156
157 fn clear(&self) {
158 let mut records = self.records.lock().expect("records mutex poisoned");
159 records.clear();
160 }
161}
162
163impl Clone for DimensionRateLimiter {
164 fn clone(&self) -> Self {
165 Self {
166 records: Arc::clone(&self.records),
167 dimension: self.dimension.clone(),
168 clock: Arc::clone(&self.clock),
169 }
170 }
171}
172
173#[derive(Clone)]
175#[allow(clippy::module_name_repetitions, clippy::struct_field_names)] pub struct ValidationRateLimiter {
177 validation_errors: DimensionRateLimiter,
178 depth_errors: DimensionRateLimiter,
179 complexity_errors: DimensionRateLimiter,
180 malformed_errors: DimensionRateLimiter,
181 async_validation_errors: DimensionRateLimiter,
182}
183
184impl ValidationRateLimiter {
185 pub fn new(config: &ValidationRateLimitingConfig) -> Self {
187 Self::new_with_clock(config, Arc::new(SystemClock))
188 }
189
190 pub fn new_with_clock(config: &ValidationRateLimitingConfig, clock: Arc<dyn Clock>) -> Self {
192 Self {
193 validation_errors: DimensionRateLimiter::new_with_clock(
194 config.validation_errors_max_requests,
195 config.validation_errors_window_secs,
196 Arc::clone(&clock),
197 ),
198 depth_errors: DimensionRateLimiter::new_with_clock(
199 config.depth_errors_max_requests,
200 config.depth_errors_window_secs,
201 Arc::clone(&clock),
202 ),
203 complexity_errors: DimensionRateLimiter::new_with_clock(
204 config.complexity_errors_max_requests,
205 config.complexity_errors_window_secs,
206 Arc::clone(&clock),
207 ),
208 malformed_errors: DimensionRateLimiter::new_with_clock(
209 config.malformed_errors_max_requests,
210 config.malformed_errors_window_secs,
211 Arc::clone(&clock),
212 ),
213 async_validation_errors: DimensionRateLimiter::new_with_clock(
214 config.async_validation_errors_max_requests,
215 config.async_validation_errors_window_secs,
216 clock,
217 ),
218 }
219 }
220
221 pub fn check_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
228 self.validation_errors.check(key)
229 }
230
231 pub fn check_depth_errors(&self, key: &str) -> Result<(), FraiseQLError> {
238 self.depth_errors.check(key)
239 }
240
241 pub fn check_complexity_errors(&self, key: &str) -> Result<(), FraiseQLError> {
248 self.complexity_errors.check(key)
249 }
250
251 pub fn check_malformed_errors(&self, key: &str) -> Result<(), FraiseQLError> {
258 self.malformed_errors.check(key)
259 }
260
261 pub fn check_async_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
268 self.async_validation_errors.check(key)
269 }
270
271 pub fn clear(&self) {
273 self.validation_errors.clear();
274 self.depth_errors.clear();
275 self.complexity_errors.clear();
276 self.malformed_errors.clear();
277 self.async_validation_errors.clear();
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_dimension_rate_limiter_allows_within_limit() {
287 let limiter = DimensionRateLimiter::new(3, 60);
288 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok on request 1: {e}"));
289 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok on request 2: {e}"));
290 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok on request 3: {e}"));
291 }
292
293 #[test]
294 fn test_dimension_rate_limiter_rejects_over_limit() {
295 let limiter = DimensionRateLimiter::new(2, 60);
296 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok on request 1: {e}"));
297 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok on request 2: {e}"));
298 assert!(
299 matches!(limiter.check("key"), Err(FraiseQLError::RateLimited { .. })),
300 "expected RateLimited error on request 3, got: {:?}",
301 limiter.check("key")
302 );
303 }
304
305 #[test]
306 fn test_dimension_rate_limiter_per_key() {
307 let limiter = DimensionRateLimiter::new(2, 60);
308 limiter
309 .check("key1")
310 .unwrap_or_else(|e| panic!("expected Ok for key1 request 1: {e}"));
311 limiter
312 .check("key1")
313 .unwrap_or_else(|e| panic!("expected Ok for key1 request 2: {e}"));
314 limiter
315 .check("key2")
316 .unwrap_or_else(|e| panic!("expected Ok for key2 request 1 (independent key): {e}"));
317 }
318
319 #[test]
320 fn test_dimension_rate_limiter_clear() {
321 let limiter = DimensionRateLimiter::new(1, 60);
322 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok before limit: {e}"));
323 assert!(
324 matches!(limiter.check("key"), Err(FraiseQLError::RateLimited { .. })),
325 "expected RateLimited error at limit, got: {:?}",
326 limiter.check("key")
327 );
328 limiter.clear();
329 limiter.check("key").unwrap_or_else(|e| panic!("expected Ok after clear: {e}"));
330 }
331
332 #[test]
333 fn test_config_defaults() {
334 let config = ValidationRateLimitingConfig::default();
335 assert!(config.enabled);
336 assert!(config.validation_errors_max_requests > 0);
337 assert!(config.depth_errors_max_requests > 0);
338 assert!(config.complexity_errors_max_requests > 0);
339 assert!(config.malformed_errors_max_requests > 0);
340 assert!(config.async_validation_errors_max_requests > 0);
341 }
342
343 #[test]
344 fn test_validation_limiter_independent_dimensions() {
345 let config = ValidationRateLimitingConfig::default();
346 let limiter = ValidationRateLimiter::new(&config);
347 let key = "test-key";
348
349 for _ in 0..100 {
351 let _ = limiter.check_validation_errors(key);
352 }
353
354 assert!(
356 matches!(limiter.check_validation_errors(key), Err(FraiseQLError::RateLimited { .. })),
357 "expected RateLimited after exhausting validation_errors quota"
358 );
359
360 limiter
362 .check_depth_errors(key)
363 .unwrap_or_else(|e| panic!("depth_errors should still allow: {e}"));
364 limiter
365 .check_complexity_errors(key)
366 .unwrap_or_else(|e| panic!("complexity_errors should still allow: {e}"));
367 limiter
368 .check_malformed_errors(key)
369 .unwrap_or_else(|e| panic!("malformed_errors should still allow: {e}"));
370 limiter
371 .check_async_validation_errors(key)
372 .unwrap_or_else(|e| panic!("async_validation_errors should still allow: {e}"));
373 }
374
375 #[test]
376 fn test_validation_limiter_clone_shares_state() {
377 let config = ValidationRateLimitingConfig::default();
378 let limiter1 = ValidationRateLimiter::new(&config);
379 let limiter2 = limiter1.clone();
380
381 let key = "shared-key";
382
383 for _ in 0..100 {
384 let _ = limiter1.check_validation_errors(key);
385 }
386
387 assert!(
389 matches!(limiter2.check_validation_errors(key), Err(FraiseQLError::RateLimited { .. })),
390 "cloned limiter should share rate limit state"
391 );
392 }
393
394 #[test]
395 fn test_window_rollover_does_not_leak_across_windows() {
396 use std::time::Duration;
397
398 use crate::utils::clock::ManualClock;
399
400 let clock = ManualClock::new();
401 let clock_arc: Arc<dyn Clock> = Arc::new(clock.clone());
402 let config = ValidationRateLimitingConfig {
403 enabled: true,
404 validation_errors_max_requests: 2,
405 validation_errors_window_secs: 60,
406 ..ValidationRateLimitingConfig::default()
407 };
408 let limiter = ValidationRateLimiter::new_with_clock(&config, clock_arc);
409
410 limiter
411 .check_validation_errors("u1")
412 .unwrap_or_else(|e| panic!("expected Ok on 1st request: {e}")); limiter
414 .check_validation_errors("u1")
415 .unwrap_or_else(|e| panic!("expected Ok on 2nd request: {e}")); assert!(
417 matches!(limiter.check_validation_errors("u1"), Err(FraiseQLError::RateLimited { .. })),
418 "expected RateLimited on 3rd request (over limit)"
419 ); clock.advance(Duration::from_secs(61)); limiter
424 .check_validation_errors("u1")
425 .unwrap_or_else(|e| panic!("expected Ok after window rollover: {e}")); }
427
428 #[test]
433 fn test_window_exact_boundary_triggers_rollover() {
434 use std::time::Duration;
435
436 use crate::utils::clock::ManualClock;
437
438 let clock = ManualClock::new();
439 let clock_arc: Arc<dyn Clock> = Arc::new(clock.clone());
440 let window_secs = 60u64;
441 let max = 2u32;
442 let limiter = DimensionRateLimiter::new_with_clock(max, window_secs, clock_arc);
443
444 for _ in 0..max {
446 limiter.check("u").unwrap_or_else(|e| panic!("expected Ok filling window: {e}"));
447 }
448 assert!(
449 matches!(limiter.check("u"), Err(FraiseQLError::RateLimited { .. })),
450 "expected RateLimited when over limit"
451 );
452
453 clock.advance(Duration::from_secs(window_secs));
455
456 limiter
457 .check("u")
458 .unwrap_or_else(|e| panic!("expected Ok at exact window boundary (>= not >): {e}"));
459 }
460
461 #[test]
465 fn test_max_requests_zero_disables_limiter() {
466 let limiter = DimensionRateLimiter::new(0, 60);
467
468 for i in 0..10u32 {
469 limiter
470 .check("key")
471 .unwrap_or_else(|e| panic!("expected Ok with max_requests=0 on request {i}: {e}"));
472 }
473 }
474
475 #[test]
480 fn test_window_secs_zero_does_not_panic() {
481 use crate::utils::clock::ManualClock;
482
483 let clock_arc: Arc<dyn Clock> = Arc::new(ManualClock::new());
484 let limiter = DimensionRateLimiter::new_with_clock(5, 0, clock_arc);
486
487 limiter
489 .check("key")
490 .unwrap_or_else(|e| panic!("expected Ok with window_secs=0 (1st): {e}"));
491 limiter
492 .check("key")
493 .unwrap_or_else(|e| panic!("expected Ok with window_secs=0 (2nd): {e}"));
494 limiter
495 .check("key")
496 .unwrap_or_else(|e| panic!("expected Ok with window_secs=0 (3rd): {e}"));
497 }
498}