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
86impl ValidationRateLimitingConfig {
87 #[must_use = "builder does nothing until .build() is called"]
89 pub fn builder() -> ValidationRateLimitingConfigBuilder {
90 ValidationRateLimitingConfigBuilder::default()
91 }
92}
93
94#[derive(Debug, Default)]
96pub struct ValidationRateLimitingConfigBuilder {
97 inner: ValidationRateLimitingConfig,
98}
99
100impl ValidationRateLimitingConfigBuilder {
101 #[must_use = "builder method returns modified builder"]
103 pub const fn enabled(mut self, enabled: bool) -> Self {
104 self.inner.enabled = enabled;
105 self
106 }
107
108 #[must_use = "builder method returns modified builder"]
110 pub const fn validation_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
111 self.inner.validation_errors_max_requests = max_requests;
112 self.inner.validation_errors_window_secs = window_secs;
113 self
114 }
115
116 #[must_use = "builder method returns modified builder"]
118 pub const fn depth_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
119 self.inner.depth_errors_max_requests = max_requests;
120 self.inner.depth_errors_window_secs = window_secs;
121 self
122 }
123
124 #[must_use = "builder method returns modified builder"]
126 pub const fn complexity_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
127 self.inner.complexity_errors_max_requests = max_requests;
128 self.inner.complexity_errors_window_secs = window_secs;
129 self
130 }
131
132 #[must_use = "builder method returns modified builder"]
134 pub const fn malformed_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
135 self.inner.malformed_errors_max_requests = max_requests;
136 self.inner.malformed_errors_window_secs = window_secs;
137 self
138 }
139
140 #[must_use = "builder method returns modified builder"]
142 pub const fn async_validation_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
143 self.inner.async_validation_errors_max_requests = max_requests;
144 self.inner.async_validation_errors_window_secs = window_secs;
145 self
146 }
147
148 #[must_use = "building a config that is not used has no effect"]
150 pub const fn build(self) -> ValidationRateLimitingConfig {
151 self.inner
152 }
153}
154
155#[derive(Debug, Clone)]
157struct RequestRecord {
158 count: u32,
160 window_start: u64,
162}
163
164pub(crate) struct DimensionRateLimiter {
166 records: Arc<Mutex<LruCache<String, RequestRecord>>>,
167 dimension: RateLimitDimension,
168 clock: Arc<dyn Clock>,
169}
170
171impl DimensionRateLimiter {
172 #[cfg(test)]
173 pub(crate) fn new(max_requests: u32, window_secs: u64) -> Self {
174 Self::new_with_clock(max_requests, window_secs, Arc::new(SystemClock))
175 }
176
177 pub(crate) fn new_with_clock(
178 max_requests: u32,
179 window_secs: u64,
180 clock: Arc<dyn Clock>,
181 ) -> Self {
182 #[allow(clippy::expect_used)]
183 let cap = NonZeroUsize::new(MAX_RATE_LIMITER_ENTRIES)
185 .expect("MAX_RATE_LIMITER_ENTRIES must be > 0");
186 Self {
187 records: Arc::new(Mutex::new(LruCache::new(cap))),
188 dimension: RateLimitDimension {
189 max_requests,
190 window_secs,
191 },
192 clock,
193 }
194 }
195
196 pub(crate) fn check(&self, key: &str) -> Result<(), FraiseQLError> {
197 if self.dimension.is_rate_limited() {
198 return Ok(());
199 }
200
201 let mut records = self.records.lock().expect("records mutex poisoned");
202 let now = self.clock.now_secs();
203
204 let record = records.get_or_insert_mut(key.to_string(), || RequestRecord {
207 count: 0,
208 window_start: now,
209 });
210
211 if now >= record.window_start + self.dimension.window_secs {
213 record.count = 1;
215 record.window_start = now;
216 Ok(())
217 } else if record.count < self.dimension.max_requests {
218 record.count += 1;
220 Ok(())
221 } else {
222 Err(FraiseQLError::RateLimited {
224 message: "Rate limit exceeded for validation errors".to_string(),
225 retry_after_secs: self.dimension.window_secs,
226 })
227 }
228 }
229
230 pub(crate) fn clear(&self) {
231 let mut records = self.records.lock().expect("records mutex poisoned");
232 records.clear();
233 }
234}
235
236impl Clone for DimensionRateLimiter {
237 fn clone(&self) -> Self {
238 Self {
239 records: Arc::clone(&self.records),
240 dimension: self.dimension.clone(),
241 clock: Arc::clone(&self.clock),
242 }
243 }
244}
245
246#[derive(Clone)]
248#[allow(clippy::module_name_repetitions, clippy::struct_field_names)] pub struct ValidationRateLimiter {
250 validation_errors: DimensionRateLimiter,
251 depth_errors: DimensionRateLimiter,
252 complexity_errors: DimensionRateLimiter,
253 malformed_errors: DimensionRateLimiter,
254 async_validation_errors: DimensionRateLimiter,
255}
256
257impl ValidationRateLimiter {
258 #[must_use]
260 pub fn new(config: &ValidationRateLimitingConfig) -> Self {
261 Self::new_with_clock(config, Arc::new(SystemClock))
262 }
263
264 pub fn new_with_clock(config: &ValidationRateLimitingConfig, clock: Arc<dyn Clock>) -> Self {
266 Self {
267 validation_errors: DimensionRateLimiter::new_with_clock(
268 config.validation_errors_max_requests,
269 config.validation_errors_window_secs,
270 Arc::clone(&clock),
271 ),
272 depth_errors: DimensionRateLimiter::new_with_clock(
273 config.depth_errors_max_requests,
274 config.depth_errors_window_secs,
275 Arc::clone(&clock),
276 ),
277 complexity_errors: DimensionRateLimiter::new_with_clock(
278 config.complexity_errors_max_requests,
279 config.complexity_errors_window_secs,
280 Arc::clone(&clock),
281 ),
282 malformed_errors: DimensionRateLimiter::new_with_clock(
283 config.malformed_errors_max_requests,
284 config.malformed_errors_window_secs,
285 Arc::clone(&clock),
286 ),
287 async_validation_errors: DimensionRateLimiter::new_with_clock(
288 config.async_validation_errors_max_requests,
289 config.async_validation_errors_window_secs,
290 clock,
291 ),
292 }
293 }
294
295 pub fn check_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
302 self.validation_errors.check(key)
303 }
304
305 pub fn check_depth_errors(&self, key: &str) -> Result<(), FraiseQLError> {
312 self.depth_errors.check(key)
313 }
314
315 pub fn check_complexity_errors(&self, key: &str) -> Result<(), FraiseQLError> {
322 self.complexity_errors.check(key)
323 }
324
325 pub fn check_malformed_errors(&self, key: &str) -> Result<(), FraiseQLError> {
332 self.malformed_errors.check(key)
333 }
334
335 pub fn check_async_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
342 self.async_validation_errors.check(key)
343 }
344
345 pub fn clear(&self) {
347 self.validation_errors.clear();
348 self.depth_errors.clear();
349 self.complexity_errors.clear();
350 self.malformed_errors.clear();
351 self.async_validation_errors.clear();
352 }
353}