1pub mod clock;
20mod gcra;
21mod nanos;
22pub mod quota;
23
24use std::{
25 fmt::Debug,
26 hash::Hash,
27 num::NonZeroU64,
28 sync::atomic::{AtomicU64, Ordering},
29 time::Duration,
30};
31
32use dashmap::DashMap;
33use futures_util::StreamExt;
34
35use self::{
36 clock::{Clock, FakeRelativeClock, MonotonicClock},
37 gcra::{Gcra, NotUntil},
38 nanos::Nanos,
39 quota::Quota,
40};
41
42#[derive(Debug, Default)]
51pub struct InMemoryState(AtomicU64);
52
53impl InMemoryState {
54 pub(crate) fn measure_and_replace_one<T, F, E>(&self, mut f: F) -> Result<T, E>
60 where
61 F: FnMut(Option<Nanos>) -> Result<(T, Nanos), E>,
62 {
63 let mut prev = self.0.load(Ordering::Acquire);
64 let mut decision = f(NonZeroU64::new(prev).map(|n| n.get().into()));
65 while let Ok((result, new_data)) = decision {
66 match self.0.compare_exchange_weak(
69 prev,
70 new_data.into(),
71 Ordering::Release,
72 Ordering::Relaxed,
73 ) {
74 Ok(_) => return Ok(result),
75 Err(e) => prev = e, }
77 decision = f(NonZeroU64::new(prev).map(|n| n.get().into()));
78 }
79 decision.map(|(result, _)| result)
82 }
83}
84
85pub type DashMapStateStore<K> = DashMap<K, InMemoryState>;
87
88pub trait StateStore {
99 type Key;
101
102 fn measure_and_replace<T, F, E>(&self, key: &Self::Key, f: F) -> Result<T, E>
119 where
120 F: Fn(Option<Nanos>) -> Result<(T, Nanos), E>;
121}
122
123impl<K: Hash + Eq + Clone> StateStore for DashMapStateStore<K> {
124 type Key = K;
125
126 fn measure_and_replace<T, F, E>(&self, key: &Self::Key, f: F) -> Result<T, E>
127 where
128 F: Fn(Option<Nanos>) -> Result<(T, Nanos), E>,
129 {
130 if let Some(v) = self.get(key) {
131 return v.measure_and_replace_one(f);
133 }
134 let entry = self.entry(key.clone()).or_default();
136 (*entry).measure_and_replace_one(f)
137 }
138}
139
140pub struct RateLimiter<K, C>
145where
146 C: Clock,
147{
148 default_gcra: Option<Gcra>,
149 state: DashMapStateStore<K>,
150 gcra: DashMap<K, Gcra>,
151 clock: C,
152 start: C::Instant,
153}
154
155impl<K, C> Debug for RateLimiter<K, C>
156where
157 K: Debug,
158 C: Clock,
159{
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 f.debug_struct(stringify!(RateLimiter)).finish()
162 }
163}
164
165impl<K> RateLimiter<K, MonotonicClock>
166where
167 K: Eq + Hash,
168{
169 #[must_use]
174 pub fn new_with_quota(base_quota: Option<Quota>, keyed_quotas: Vec<(K, Quota)>) -> Self {
175 let clock = MonotonicClock {};
176 Self::new_with_clock(base_quota, keyed_quotas, clock)
177 }
178}
179
180impl<K, C> RateLimiter<K, C>
181where
182 K: Eq + Hash,
183 C: Clock,
184{
185 #[must_use]
190 pub fn new_with_clock(
191 base_quota: Option<Quota>,
192 keyed_quotas: Vec<(K, Quota)>,
193 clock: C,
194 ) -> Self {
195 let start = clock.now();
196 let gcra: DashMap<_, _> = keyed_quotas
197 .into_iter()
198 .map(|(k, q)| (k, Gcra::new(q)))
199 .collect();
200 Self {
201 default_gcra: base_quota.map(Gcra::new),
202 state: DashMapStateStore::new(),
203 gcra,
204 clock,
205 start,
206 }
207 }
208}
209
210impl<K> RateLimiter<K, FakeRelativeClock>
211where
212 K: Hash + Eq + Clone,
213{
214 pub fn advance_clock(&self, by: Duration) {
218 self.clock.advance(by);
219 }
220}
221
222impl<K, C> RateLimiter<K, C>
223where
224 K: Hash + Eq + Clone,
225 C: Clock,
226{
227 pub fn add_quota_for_key(&self, key: K, value: Quota) {
229 self.gcra.insert(key, Gcra::new(value));
230 }
231
232 pub fn check_key(&self, key: &K) -> Result<(), NotUntil<C::Instant>> {
238 match self.gcra.get(key) {
239 Some(quota) => quota.test_and_update(self.start, key, &self.state, self.clock.now()),
240 None => self.default_gcra.as_ref().map_or(Ok(()), |gcra| {
241 gcra.test_and_update(self.start, key, &self.state, self.clock.now())
242 }),
243 }
244 }
245
246 pub async fn until_key_ready(&self, key: &K) {
248 loop {
249 match self.check_key(key) {
250 Ok(()) => {
251 break;
252 }
253 Err(e) => {
254 self.clock.sleep(e.wait_time_from(self.clock.now())).await;
255 }
256 }
257 }
258 }
259
260 pub async fn await_keys_ready(&self, keys: Option<&[K]>) {
265 let Some(keys) = keys else {
266 return;
267 };
268
269 match keys.len() {
270 0 => {}
271 1 => self.until_key_ready(&keys[0]).await,
272 2 => {
273 tokio::join!(
274 self.until_key_ready(&keys[0]),
275 self.until_key_ready(&keys[1]),
276 );
277 }
278 _ => {
279 let tasks = keys.iter().map(|key| self.until_key_ready(key));
280 futures::stream::iter(tasks)
281 .for_each_concurrent(None, |key_future| async move {
282 key_future.await;
283 })
284 .await;
285 }
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use std::{
293 num::NonZeroU32,
294 sync::atomic::{AtomicU32, Ordering},
295 time::Duration,
296 };
297
298 use dashmap::DashMap;
299 use rstest::rstest;
300
301 use super::{
302 DashMapStateStore, RateLimiter,
303 clock::{Clock, FakeRelativeClock},
304 gcra::{Gcra, StateSnapshot},
305 nanos::Nanos,
306 quota::Quota,
307 };
308
309 fn initialize_mock_rate_limiter() -> RateLimiter<String, FakeRelativeClock> {
310 let clock = FakeRelativeClock::default();
311 let start = clock.now();
312 let gcra = DashMap::new();
313 let base_quota = Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap();
314 RateLimiter {
315 default_gcra: Some(Gcra::new(base_quota)),
316 state: DashMapStateStore::new(),
317 gcra,
318 clock,
319 start,
320 }
321 }
322
323 #[rstest]
324 fn test_enormous_quota_denies_after_burst() {
325 let quota = Quota::with_period(Duration::MAX)
328 .unwrap()
329 .allow_burst(NonZeroU32::new(u32::MAX).unwrap());
330 let clock = FakeRelativeClock::default();
331 let limiter: RateLimiter<String, FakeRelativeClock> =
332 RateLimiter::new_with_clock(Some(quota), vec![], clock);
333
334 let key = "key".to_string();
335 assert!(limiter.check_key(&key).is_ok());
336 assert!(limiter.check_key(&key).is_err());
337 }
338
339 #[rstest]
340 fn test_default_quota() {
341 let mock_limiter = initialize_mock_rate_limiter();
342
343 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
345 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
346
347 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
349
350 mock_limiter.advance_clock(Duration::from_secs(1));
352 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
353 }
354
355 #[rstest]
356 fn test_custom_key_quota() {
357 let mock_limiter = initialize_mock_rate_limiter();
358
359 mock_limiter.add_quota_for_key(
361 "custom".to_string(),
362 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
363 );
364
365 assert!(mock_limiter.check_key(&"custom".to_string()).is_ok());
367 assert!(mock_limiter.check_key(&"custom".to_string()).is_err());
368
369 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
371 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
372 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
373 }
374
375 #[rstest]
376 fn test_multiple_keys() {
377 let mock_limiter = initialize_mock_rate_limiter();
378
379 mock_limiter.add_quota_for_key(
380 "key1".to_string(),
381 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
382 );
383 mock_limiter.add_quota_for_key(
384 "key2".to_string(),
385 Quota::per_second(NonZeroU32::new(3).unwrap()).unwrap(),
386 );
387
388 assert!(mock_limiter.check_key(&"key1".to_string()).is_ok());
390 assert!(mock_limiter.check_key(&"key1".to_string()).is_err());
391
392 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
394 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
395 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
396 assert!(mock_limiter.check_key(&"key2".to_string()).is_err());
397 }
398
399 #[rstest]
400 fn test_quota_reset() {
401 let mock_limiter = initialize_mock_rate_limiter();
402
403 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
405 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
406 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
407
408 mock_limiter.advance_clock(Duration::from_millis(499));
410 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
411
412 mock_limiter.advance_clock(Duration::from_millis(501));
414 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
415 }
416
417 #[rstest]
418 fn test_different_quotas() {
419 let mock_limiter = initialize_mock_rate_limiter();
420
421 mock_limiter.add_quota_for_key(
422 "per_second".to_string(),
423 Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap(),
424 );
425 mock_limiter.add_quota_for_key(
426 "per_minute".to_string(),
427 Quota::per_minute(NonZeroU32::new(3).unwrap()),
428 );
429
430 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
432 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
433 assert!(mock_limiter.check_key(&"per_second".to_string()).is_err());
434
435 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
437 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
438 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
439 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
440
441 mock_limiter.advance_clock(Duration::from_secs(1));
443 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
444 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
445 }
446
447 #[tokio::test]
448 async fn test_await_keys_ready() {
449 let mock_limiter = initialize_mock_rate_limiter();
450
451 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
453 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
454
455 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
457
458 mock_limiter.advance_clock(Duration::from_secs(1));
460 let keys = ["default".to_string()];
461 mock_limiter.await_keys_ready(Some(keys.as_slice())).await;
462 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
463 }
464
465 #[rstest]
466 fn test_remaining_burst_capacity_zero_t() {
467 let snapshot = StateSnapshot::new(
468 Nanos::from(0u64),
469 Nanos::from(1_000_000u64),
470 Nanos::from(0u64),
471 Nanos::from(0u64),
472 );
473 assert_eq!(snapshot.remaining_burst_capacity(), 0);
474 }
475
476 #[rstest]
477 fn test_per_second_returns_none_on_zero_replenish_interval() {
478 assert!(Quota::per_second(NonZeroU32::new(u32::MAX).unwrap()).is_none());
479 }
480
481 #[rstest]
482 fn test_per_minute_accepts_max_burst() {
483 let quota = Quota::per_minute(NonZeroU32::new(u32::MAX).unwrap());
484 assert!(quota.replenish_interval().as_nanos() > 0);
485 }
486
487 #[rstest]
488 fn test_per_hour_accepts_max_burst() {
489 let quota = Quota::per_hour(NonZeroU32::new(u32::MAX).unwrap());
490 assert!(quota.replenish_interval().as_nanos() > 0);
491 }
492
493 mod property_tests {
494 use proptest::prelude::*;
495 use rstest::rstest;
496
497 use crate::ratelimiter::{gcra::StateSnapshot, nanos::Nanos};
498
499 proptest! {
500 #![proptest_config(ProptestConfig {
501 failure_persistence: Some(Box::new(
502 proptest::test_runner::FileFailurePersistence::WithSource("ratelimiter")
503 )),
504 ..ProptestConfig::default()
505 })]
506
507 #[rstest]
509 fn remaining_burst_capacity_never_panics(
510 t in proptest::num::u64::ANY,
511 tau in proptest::num::u64::ANY,
512 time_of_measurement in proptest::num::u64::ANY,
513 tat in proptest::num::u64::ANY,
514 ) {
515 let snapshot = StateSnapshot::new(
516 Nanos::from(t),
517 Nanos::from(tau),
518 Nanos::from(time_of_measurement),
519 Nanos::from(tat),
520 );
521
522 let _ = snapshot.remaining_burst_capacity();
523 }
524
525 #[rstest]
527 fn nanos_operators_never_panic(a in proptest::num::u64::ANY, b in proptest::num::u64::ANY) {
528 let na = Nanos::from(a);
529 let nb = Nanos::from(b);
530
531 prop_assert_eq!((na + nb).as_u64(), a.saturating_add(b));
532 prop_assert_eq!((na * b).as_u64(), a.saturating_mul(b));
533 prop_assert_eq!(na.saturating_sub(nb).as_u64(), a.saturating_sub(b));
534 }
535 }
536 }
537
538 #[rstest]
539 fn test_gcra_boundary_exact_replenishment() {
540 let mock_limiter = initialize_mock_rate_limiter();
543 let key = "boundary_test".to_string();
544
545 assert!(mock_limiter.check_key(&key).is_ok());
546 assert!(mock_limiter.check_key(&key).is_ok());
547 assert!(mock_limiter.check_key(&key).is_err());
548
549 let quota = Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap();
551 let replenish_interval = quota.replenish_interval();
552 mock_limiter.advance_clock(replenish_interval);
553
554 assert!(
555 mock_limiter.check_key(&key).is_ok(),
556 "Request at exact replenish boundary should be allowed"
557 );
558 assert!(
559 mock_limiter.check_key(&key).is_err(),
560 "Immediate follow-up should be rate-limited"
561 );
562 }
563
564 #[rstest]
565 fn test_per_second_boundary_exact_limit() {
566 let quota = Quota::per_second(NonZeroU32::new(1_000_000_000).unwrap()).unwrap();
568 assert_eq!(quota.replenish_interval().as_nanos(), 1);
569 }
570
571 #[rstest]
572 fn test_per_second_returns_none_above_one_billion() {
573 assert!(Quota::per_second(NonZeroU32::new(1_000_000_001).unwrap()).is_none());
575 }
576
577 #[rstest]
578 fn test_burst_size_replenished_in_truncation() {
579 let quota = Quota::with_period(Duration::from_secs(100))
581 .unwrap()
582 .allow_burst(NonZeroU32::new(u32::MAX).unwrap());
583
584 let replenished_in = quota.burst_size_replenished_in();
585 let full: u128 = 100_000_000_000u128 * u128::from(u32::MAX);
586 let truncated = full as u64;
587
588 assert_eq!(replenished_in, Duration::from_nanos(truncated));
589 assert_ne!(
590 full,
591 u128::from(truncated),
592 "Truncation should have occurred"
593 );
594 }
595
596 #[rstest]
597 #[should_panic(expected = "t cannot be zero")]
598 fn test_from_gcra_parameters_panics_on_zero_t() {
599 let _ = Quota::from_gcra_parameters(Nanos::from(0u64), Nanos::from(100u64));
600 }
601
602 #[rstest]
603 #[should_panic(expected = "tau/t results in zero burst capacity")]
604 fn test_from_gcra_parameters_panics_on_zero_division() {
605 let _ = Quota::from_gcra_parameters(Nanos::from(2u64), Nanos::from(1u64));
607 }
608
609 #[rstest]
610 #[should_panic(expected = "tau/t exceeds u32::MAX")]
611 fn test_from_gcra_parameters_panics_on_overflow() {
612 let _ = Quota::from_gcra_parameters(Nanos::from(1u64), Nanos::from(u64::MAX));
613 }
614
615 #[rstest]
616 fn test_concurrent_check_key_respects_burst() {
617 let rate = 10u32;
618 let clock = FakeRelativeClock::default();
619 let start = clock.now();
620 let limiter = RateLimiter {
621 default_gcra: Some(Gcra::new(
622 Quota::per_second(NonZeroU32::new(rate).unwrap()).unwrap(),
623 )),
624 state: DashMapStateStore::new(),
625 gcra: DashMap::new(),
626 clock,
627 start,
628 };
629
630 let accepted = AtomicU32::new(0);
631 let num_threads = 50;
632
633 std::thread::scope(|s| {
635 for _ in 0..num_threads {
636 s.spawn(|| {
637 if limiter.check_key(&"hot_key".to_string()).is_ok() {
638 accepted.fetch_add(1, Ordering::Relaxed);
639 }
640 });
641 }
642 });
643
644 let total = accepted.load(Ordering::Relaxed);
645 assert!(total >= 1, "At least one request should be accepted");
646 assert!(
647 total <= rate,
648 "Accepted {total} but burst capacity is {rate}"
649 );
650 }
651}