Skip to main content

fraiseql_core/validation/
rate_limiting.rs

1//! Validation-specific rate limiting with per-dimension tracking.
2//!
3//! Provides rate limiting for different types of validation errors:
4//! - validation_errors: General field-level validation failures
5//! - depth_errors: Query depth limit violations
6//! - complexity_errors: Query complexity limit violations
7//! - malformed_errors: Malformed query/input errors
8//! - async_validation_errors: Async validator failures
9
10use std::{
11    collections::HashMap,
12    sync::{Arc, Mutex},
13    time::{SystemTime, UNIX_EPOCH},
14};
15
16use crate::error::FraiseQLError;
17
18/// Rate limit configuration for a single dimension
19#[derive(Debug, Clone)]
20pub struct RateLimitDimension {
21    /// Maximum number of errors allowed in the window
22    pub max_requests: u32,
23    /// Window duration in seconds
24    pub window_secs:  u64,
25}
26
27impl RateLimitDimension {
28    fn is_rate_limited(&self) -> bool {
29        self.max_requests == 0
30    }
31}
32
33/// Configuration for validation-specific rate limiting
34#[derive(Debug, Clone)]
35pub struct ValidationRateLimitingConfig {
36    /// Enable validation rate limiting
37    pub enabled: bool,
38    /// Validation errors limit
39    pub validation_errors_max_requests: u32,
40    /// Validation errors window in seconds
41    pub validation_errors_window_secs: u64,
42    /// Depth errors limit
43    pub depth_errors_max_requests: u32,
44    /// Depth errors window in seconds
45    pub depth_errors_window_secs: u64,
46    /// Complexity errors limit
47    pub complexity_errors_max_requests: u32,
48    /// Complexity errors window in seconds
49    pub complexity_errors_window_secs: u64,
50    /// Malformed errors limit
51    pub malformed_errors_max_requests: u32,
52    /// Malformed errors window in seconds
53    pub malformed_errors_window_secs: u64,
54    /// Async validation errors limit
55    pub async_validation_errors_max_requests: u32,
56    /// Async validation errors window in seconds
57    pub async_validation_errors_window_secs: u64,
58}
59
60impl Default for ValidationRateLimitingConfig {
61    fn default() -> Self {
62        Self {
63            enabled: true,
64            validation_errors_max_requests: 100,
65            validation_errors_window_secs: 60,
66            depth_errors_max_requests: 50,
67            depth_errors_window_secs: 60,
68            complexity_errors_max_requests: 30,
69            complexity_errors_window_secs: 60,
70            malformed_errors_max_requests: 40,
71            malformed_errors_window_secs: 60,
72            async_validation_errors_max_requests: 60,
73            async_validation_errors_window_secs: 60,
74        }
75    }
76}
77
78/// Request record for tracking
79#[derive(Debug, Clone)]
80struct RequestRecord {
81    /// Number of errors in current window
82    count:        u32,
83    /// Unix timestamp of window start
84    window_start: u64,
85}
86
87/// Single dimension rate limiter
88struct DimensionRateLimiter {
89    records:   Arc<Mutex<HashMap<String, RequestRecord>>>,
90    dimension: RateLimitDimension,
91}
92
93impl DimensionRateLimiter {
94    fn new(max_requests: u32, window_secs: u64) -> Self {
95        Self {
96            records:   Arc::new(Mutex::new(HashMap::new())),
97            dimension: RateLimitDimension {
98                max_requests,
99                window_secs,
100            },
101        }
102    }
103
104    fn get_timestamp() -> u64 {
105        SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
106    }
107
108    fn check(&self, key: &str) -> Result<(), FraiseQLError> {
109        if self.dimension.is_rate_limited() {
110            return Ok(());
111        }
112
113        let mut records = self.records.lock().unwrap();
114        let now = Self::get_timestamp();
115
116        let record = records.entry(key.to_string()).or_insert_with(|| RequestRecord {
117            count:        0,
118            window_start: now,
119        });
120
121        // Check if window has expired
122        if now >= record.window_start + self.dimension.window_secs {
123            // Reset window
124            record.count = 1;
125            record.window_start = now;
126            Ok(())
127        } else if record.count < self.dimension.max_requests {
128            // Request allowed
129            record.count += 1;
130            Ok(())
131        } else {
132            // Rate limited
133            Err(FraiseQLError::RateLimited {
134                message:          "Rate limit exceeded for validation errors".to_string(),
135                retry_after_secs: self.dimension.window_secs,
136            })
137        }
138    }
139
140    fn clear(&self) {
141        let mut records = self.records.lock().unwrap();
142        records.clear();
143    }
144}
145
146impl Clone for DimensionRateLimiter {
147    fn clone(&self) -> Self {
148        Self {
149            records:   Arc::clone(&self.records),
150            dimension: self.dimension.clone(),
151        }
152    }
153}
154
155/// Validation-specific rate limiter with per-dimension tracking
156#[derive(Clone)]
157#[allow(clippy::module_name_repetitions, clippy::struct_field_names)] // Reason: RateLimiting prefix provides clarity at call sites
158pub struct ValidationRateLimiter {
159    validation_errors:       DimensionRateLimiter,
160    depth_errors:            DimensionRateLimiter,
161    complexity_errors:       DimensionRateLimiter,
162    malformed_errors:        DimensionRateLimiter,
163    async_validation_errors: DimensionRateLimiter,
164}
165
166impl ValidationRateLimiter {
167    /// Create a new validation rate limiter with the given configuration
168    pub fn new(config: ValidationRateLimitingConfig) -> Self {
169        Self {
170            validation_errors:       DimensionRateLimiter::new(
171                config.validation_errors_max_requests,
172                config.validation_errors_window_secs,
173            ),
174            depth_errors:            DimensionRateLimiter::new(
175                config.depth_errors_max_requests,
176                config.depth_errors_window_secs,
177            ),
178            complexity_errors:       DimensionRateLimiter::new(
179                config.complexity_errors_max_requests,
180                config.complexity_errors_window_secs,
181            ),
182            malformed_errors:        DimensionRateLimiter::new(
183                config.malformed_errors_max_requests,
184                config.malformed_errors_window_secs,
185            ),
186            async_validation_errors: DimensionRateLimiter::new(
187                config.async_validation_errors_max_requests,
188                config.async_validation_errors_window_secs,
189            ),
190        }
191    }
192
193    /// Check rate limit for validation errors
194    pub fn check_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
195        self.validation_errors.check(key)
196    }
197
198    /// Check rate limit for depth errors
199    pub fn check_depth_errors(&self, key: &str) -> Result<(), FraiseQLError> {
200        self.depth_errors.check(key)
201    }
202
203    /// Check rate limit for complexity errors
204    pub fn check_complexity_errors(&self, key: &str) -> Result<(), FraiseQLError> {
205        self.complexity_errors.check(key)
206    }
207
208    /// Check rate limit for malformed errors
209    pub fn check_malformed_errors(&self, key: &str) -> Result<(), FraiseQLError> {
210        self.malformed_errors.check(key)
211    }
212
213    /// Check rate limit for async validation errors
214    pub fn check_async_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
215        self.async_validation_errors.check(key)
216    }
217
218    /// Clear all rate limiter state
219    pub fn clear(&self) {
220        self.validation_errors.clear();
221        self.depth_errors.clear();
222        self.complexity_errors.clear();
223        self.malformed_errors.clear();
224        self.async_validation_errors.clear();
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_dimension_rate_limiter_allows_within_limit() {
234        let limiter = DimensionRateLimiter::new(3, 60);
235        assert!(limiter.check("key").is_ok());
236        assert!(limiter.check("key").is_ok());
237        assert!(limiter.check("key").is_ok());
238    }
239
240    #[test]
241    fn test_dimension_rate_limiter_rejects_over_limit() {
242        let limiter = DimensionRateLimiter::new(2, 60);
243        assert!(limiter.check("key").is_ok());
244        assert!(limiter.check("key").is_ok());
245        assert!(limiter.check("key").is_err());
246    }
247
248    #[test]
249    fn test_dimension_rate_limiter_per_key() {
250        let limiter = DimensionRateLimiter::new(2, 60);
251        assert!(limiter.check("key1").is_ok());
252        assert!(limiter.check("key1").is_ok());
253        assert!(limiter.check("key2").is_ok());
254    }
255
256    #[test]
257    fn test_dimension_rate_limiter_clear() {
258        let limiter = DimensionRateLimiter::new(1, 60);
259        assert!(limiter.check("key").is_ok());
260        assert!(limiter.check("key").is_err());
261        limiter.clear();
262        assert!(limiter.check("key").is_ok());
263    }
264
265    #[test]
266    fn test_config_defaults() {
267        let config = ValidationRateLimitingConfig::default();
268        assert!(config.enabled);
269        assert!(config.validation_errors_max_requests > 0);
270        assert!(config.depth_errors_max_requests > 0);
271        assert!(config.complexity_errors_max_requests > 0);
272        assert!(config.malformed_errors_max_requests > 0);
273        assert!(config.async_validation_errors_max_requests > 0);
274    }
275
276    #[test]
277    fn test_validation_limiter_independent_dimensions() {
278        let config = ValidationRateLimitingConfig::default();
279        let limiter = ValidationRateLimiter::new(config);
280        let key = "test-key";
281
282        // Fill up validation errors
283        for _ in 0..100 {
284            let _ = limiter.check_validation_errors(key);
285        }
286
287        // Validation errors should be limited
288        assert!(limiter.check_validation_errors(key).is_err());
289
290        // But other dimensions should still work
291        assert!(limiter.check_depth_errors(key).is_ok());
292        assert!(limiter.check_complexity_errors(key).is_ok());
293        assert!(limiter.check_malformed_errors(key).is_ok());
294        assert!(limiter.check_async_validation_errors(key).is_ok());
295    }
296
297    #[test]
298    fn test_validation_limiter_clone_shares_state() {
299        let config = ValidationRateLimitingConfig::default();
300        let limiter1 = ValidationRateLimiter::new(config);
301        let limiter2 = limiter1.clone();
302
303        let key = "shared-key";
304
305        for _ in 0..100 {
306            let _ = limiter1.check_validation_errors(key);
307        }
308
309        // limiter2 should see the same limit
310        assert!(limiter2.check_validation_errors(key).is_err());
311    }
312}