1pub mod clock;
20pub mod quota;
21
22mod gcra;
23mod nanos;
24
25use std::{
26 collections::HashMap,
27 fmt::Debug,
28 hash::Hash,
29 num::NonZeroU64,
30 sync::{
31 Arc,
32 atomic::{AtomicU64, Ordering},
33 },
34 time::Duration,
35};
36
37use dashmap::DashMap;
38use parking_lot::Mutex;
39
40use self::{
41 clock::{Clock, FakeRelativeClock, MonotonicClock},
42 gcra::{Gcra, NotUntil},
43 nanos::Nanos,
44 quota::Quota,
45};
46
47#[derive(Debug, Default)]
56pub struct InMemoryState(AtomicU64);
57
58impl InMemoryState {
59 fn load(&self) -> Option<Nanos> {
60 NonZeroU64::new(self.0.load(Ordering::Acquire)).map(|n| n.get().into())
61 }
62
63 fn store(&self, value: Nanos) {
64 self.0.store(value.into(), Ordering::Release);
65 }
66
67 pub(crate) fn measure_and_replace_one<T, F, E>(&self, mut f: F) -> Result<T, E>
73 where
74 F: FnMut(Option<Nanos>) -> Result<(T, Nanos), E>,
75 {
76 let mut prev = self.0.load(Ordering::Acquire);
77 loop {
78 let (result, new_data) = f(NonZeroU64::new(prev).map(|n| n.get().into()))?;
79
80 match self.0.compare_exchange_weak(
83 prev,
84 new_data.into(),
85 Ordering::Release,
86 Ordering::Relaxed,
87 ) {
88 Ok(_) => return Ok(result),
89 Err(e) => prev = e, }
91 }
92 }
93}
94
95pub type DashMapStateStore<K> = DashMap<K, InMemoryState>;
97
98pub trait StateStore {
109 type Key;
111
112 fn measure_and_replace<T, F, E>(&self, key: &Self::Key, f: F) -> Result<T, E>
129 where
130 F: Fn(Option<Nanos>) -> Result<(T, Nanos), E>;
131}
132
133impl<K: Hash + Eq + Clone> StateStore for DashMapStateStore<K> {
134 type Key = K;
135
136 fn measure_and_replace<T, F, E>(&self, key: &Self::Key, f: F) -> Result<T, E>
137 where
138 F: Fn(Option<Nanos>) -> Result<(T, Nanos), E>,
139 {
140 if let Some(v) = self.get(key) {
141 return v.measure_and_replace_one(f);
143 }
144 let entry = self.entry(key.clone()).or_default();
146 (*entry).measure_and_replace_one(f)
147 }
148}
149
150pub struct RateLimiter<K, C>
155where
156 C: Clock,
157{
158 default_gcra: Option<Gcra>,
159 state: DashMapStateStore<K>,
160 gcra: DashMap<K, Gcra>,
161 clock: C,
162 start: C::Instant,
163 decision_lock: Mutex<()>,
164}
165
166impl<K, C> Debug for RateLimiter<K, C>
167where
168 K: Debug,
169 C: Clock,
170{
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct(stringify!(RateLimiter)).finish()
173 }
174}
175
176impl<K> RateLimiter<K, MonotonicClock>
177where
178 K: Eq + Hash,
179{
180 #[must_use]
185 pub fn new_with_quota(base_quota: Option<Quota>, keyed_quotas: Vec<(K, Quota)>) -> Self {
186 Self::new_with_clock(base_quota, keyed_quotas, MonotonicClock)
187 }
188}
189
190impl<K, C> RateLimiter<K, C>
191where
192 K: Eq + Hash,
193 C: Clock,
194{
195 #[must_use]
200 pub fn new_with_clock(
201 base_quota: Option<Quota>,
202 keyed_quotas: Vec<(K, Quota)>,
203 clock: C,
204 ) -> Self {
205 let start = clock.now();
206 let gcra: DashMap<_, _> = keyed_quotas
207 .into_iter()
208 .map(|(k, q)| (k, Gcra::new(q)))
209 .collect();
210 Self {
211 default_gcra: base_quota.map(Gcra::new),
212 state: DashMapStateStore::new(),
213 gcra,
214 clock,
215 start,
216 decision_lock: Mutex::new(()),
217 }
218 }
219}
220
221impl<K> RateLimiter<K, FakeRelativeClock>
222where
223 K: Hash + Eq + Clone,
224{
225 pub fn advance_clock(&self, by: Duration) {
229 self.clock.advance(by);
230 }
231}
232
233impl<K, C> RateLimiter<K, C>
234where
235 K: Hash + Eq + Clone,
236 C: Clock,
237{
238 pub fn add_quota_for_key(&self, key: K, value: Quota) {
240 let _guard = self.decision_lock.lock();
241 self.gcra.insert(key, Gcra::new(value));
242 }
243
244 pub fn check_key(&self, key: &K) -> Result<(), NotUntil<C::Instant>> {
250 let _guard = self.decision_lock.lock();
251
252 match self.gcra.get(key) {
253 Some(quota) => quota.test_and_update(self.start, key, &self.state, self.clock.now()),
254 None => self.default_gcra.as_ref().map_or(Ok(()), |gcra| {
255 gcra.test_and_update(self.start, key, &self.state, self.clock.now())
256 }),
257 }
258 }
259
260 pub async fn until_key_ready(&self, key: &K) {
262 loop {
263 match self.check_key(key) {
264 Ok(()) => {
265 break;
266 }
267 Err(e) => {
268 self.clock.sleep(e.wait_time_from(self.clock.now())).await;
269 }
270 }
271 }
272 }
273
274 pub async fn await_keys_ready(&self, keys: Option<&[K]>) {
278 let Some(keys) = keys else {
279 return;
280 };
281
282 loop {
283 let wait = {
284 let _guard = self.decision_lock.lock();
285
286 match self.plan_keys(keys, self.clock.now()) {
287 Ok(planned) => {
288 self.commit_keys(planned);
289 None
290 }
291 Err(wait) => Some(wait),
292 }
293 };
294
295 match wait {
296 Some(wait) => self.clock.sleep(wait).await,
297 None => return,
298 }
299 }
300 }
301
302 fn plan_keys<'a>(
303 &self,
304 keys: &'a [K],
305 now: C::Instant,
306 ) -> Result<HashMap<&'a K, Nanos>, Duration> {
307 let mut planned = HashMap::with_capacity(keys.len());
308 let mut wait: Option<Duration> = None;
309
310 for key in keys {
311 let tat = planned
312 .get(key)
313 .copied()
314 .or_else(|| self.state.get(key).and_then(|state| state.load()));
315 let decision = match self.gcra.get(key) {
316 Some(quota) => Some(quota.test(self.start, tat, now)),
317 None => self
318 .default_gcra
319 .as_ref()
320 .map(|gcra| gcra.test(self.start, tat, now)),
321 };
322
323 match decision {
324 Some(Ok(next)) => {
325 planned.insert(key, next);
326 }
327 Some(Err(denied)) => {
328 let duration = denied.wait_time_from(now);
329 wait = Some(wait.map_or(duration, |current| current.max(duration)));
330 }
331 None => {}
332 }
333 }
334
335 match wait {
336 Some(wait) => Err(wait),
337 None => Ok(planned),
338 }
339 }
340
341 fn commit_keys(&self, planned: HashMap<&K, Nanos>) {
342 for (key, tat) in planned {
343 self.state.entry(key.clone()).or_default().store(tat);
344 }
345 }
346}
347
348impl<K> RateLimiter<K, MonotonicClock>
349where
350 K: Hash + Eq + Clone,
351{
352 pub(crate) async fn await_limiters_ready(rate_limiters: &[Arc<Self>], keys: Option<&[K]>) {
353 let Some(keys) = keys else {
354 return;
355 };
356
357 if rate_limiters.is_empty() || keys.is_empty() {
358 return;
359 }
360
361 let mut ordered = rate_limiters.iter().map(Arc::as_ref).collect::<Vec<_>>();
362 ordered.sort_unstable_by_key(|limiter| std::ptr::from_ref(*limiter) as usize);
363 ordered.dedup_by(|a, b| std::ptr::eq(*a, *b));
364
365 loop {
366 let wait = {
367 let _guards = ordered
368 .iter()
369 .map(|limiter| limiter.decision_lock.lock())
370 .collect::<Vec<_>>();
371 let mut plans = Vec::with_capacity(ordered.len());
372 let mut wait: Option<Duration> = None;
373
374 for limiter in &ordered {
375 match limiter.plan_keys(keys, limiter.clock.now()) {
376 Ok(planned) => plans.push((*limiter, planned)),
377 Err(duration) => {
378 wait = Some(wait.map_or(duration, |current| current.max(duration)));
379 }
380 }
381 }
382
383 if wait.is_none() {
384 for (limiter, planned) in plans {
385 limiter.commit_keys(planned);
386 }
387 }
388 wait
389 };
390
391 match wait {
392 Some(wait) => ordered[0].clock.sleep(wait).await,
393 None => return,
394 }
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use std::{
402 num::NonZeroU32,
403 sync::{
404 Arc,
405 atomic::{AtomicU32, Ordering},
406 },
407 time::Duration,
408 };
409
410 use dashmap::DashMap;
411 #[cfg(all(feature = "simulation", madsim))]
412 use madsim::task as test_task;
413 use rstest::rstest;
414 #[cfg(not(all(feature = "simulation", madsim)))]
415 use tokio::task as test_task;
416
417 use super::{
418 DashMapStateStore, RateLimiter,
419 clock::{Clock, FakeRelativeClock},
420 gcra::Gcra,
421 nanos::Nanos,
422 quota::Quota,
423 };
424
425 fn initialize_mock_rate_limiter() -> RateLimiter<String, FakeRelativeClock> {
426 let clock = FakeRelativeClock::default();
427 let start = clock.now();
428 let gcra = DashMap::new();
429 let base_quota = Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap();
430 RateLimiter {
431 default_gcra: Some(Gcra::new(base_quota)),
432 state: DashMapStateStore::new(),
433 gcra,
434 clock,
435 start,
436 decision_lock: parking_lot::Mutex::new(()),
437 }
438 }
439
440 #[rstest]
441 fn test_enormous_quota_denies_after_burst() {
442 let quota = Quota::with_period(Duration::MAX)
445 .unwrap()
446 .allow_burst(NonZeroU32::new(u32::MAX).unwrap());
447 let clock = FakeRelativeClock::default();
448 let limiter: RateLimiter<String, FakeRelativeClock> =
449 RateLimiter::new_with_clock(Some(quota), vec![], clock);
450
451 let key = "key".to_string();
452 assert!(limiter.check_key(&key).is_ok());
453 assert!(limiter.check_key(&key).is_err());
454 }
455
456 #[rstest]
457 fn test_default_quota() {
458 let mock_limiter = initialize_mock_rate_limiter();
459
460 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
462 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
463
464 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
466
467 mock_limiter.advance_clock(Duration::from_secs(1));
469 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
470 }
471
472 #[rstest]
473 fn test_custom_key_quota() {
474 let mock_limiter = initialize_mock_rate_limiter();
475
476 mock_limiter.add_quota_for_key(
478 "custom".to_string(),
479 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
480 );
481
482 assert!(mock_limiter.check_key(&"custom".to_string()).is_ok());
484 assert!(mock_limiter.check_key(&"custom".to_string()).is_err());
485
486 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
488 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
489 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
490 }
491
492 #[rstest]
493 fn test_multiple_keys() {
494 let mock_limiter = initialize_mock_rate_limiter();
495
496 mock_limiter.add_quota_for_key(
497 "key1".to_string(),
498 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
499 );
500 mock_limiter.add_quota_for_key(
501 "key2".to_string(),
502 Quota::per_second(NonZeroU32::new(3).unwrap()).unwrap(),
503 );
504
505 assert!(mock_limiter.check_key(&"key1".to_string()).is_ok());
507 assert!(mock_limiter.check_key(&"key1".to_string()).is_err());
508
509 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
511 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
512 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
513 assert!(mock_limiter.check_key(&"key2".to_string()).is_err());
514 }
515
516 #[rstest]
517 fn test_quota_reset() {
518 let mock_limiter = initialize_mock_rate_limiter();
519
520 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
522 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
523 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
524
525 mock_limiter.advance_clock(Duration::from_millis(499));
527 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
528
529 mock_limiter.advance_clock(Duration::from_millis(501));
531 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
532 }
533
534 #[rstest]
535 fn test_different_quotas() {
536 let mock_limiter = initialize_mock_rate_limiter();
537
538 mock_limiter.add_quota_for_key(
539 "per_second".to_string(),
540 Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap(),
541 );
542 mock_limiter.add_quota_for_key(
543 "per_minute".to_string(),
544 Quota::per_minute(NonZeroU32::new(3).unwrap()),
545 );
546
547 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
549 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
550 assert!(mock_limiter.check_key(&"per_second".to_string()).is_err());
551
552 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
554 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
555 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
556 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
557
558 mock_limiter.advance_clock(Duration::from_secs(1));
560 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
561 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
562 }
563
564 #[tokio::test]
565 async fn test_await_keys_ready() {
566 let mock_limiter = initialize_mock_rate_limiter();
567
568 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
570 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
571
572 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
574
575 mock_limiter.advance_clock(Duration::from_secs(1));
577 let keys = ["default".to_string()];
578 mock_limiter.await_keys_ready(Some(keys.as_slice())).await;
579 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
580 }
581
582 #[cfg_attr(
583 not(all(feature = "simulation", madsim)),
584 tokio::test(start_paused = true)
585 )]
586 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
587 async fn test_await_keys_ready_reserves_keys_together() {
588 let fast = "fast".to_string();
589 let slow = "slow".to_string();
590 let limiter = Arc::new(RateLimiter::new_with_quota(
591 None,
592 vec![
593 (
594 fast.clone(),
595 Quota::with_period(Duration::from_secs(1)).unwrap(),
596 ),
597 (
598 slow.clone(),
599 Quota::with_period(Duration::from_secs(10)).unwrap(),
600 ),
601 ],
602 ));
603 limiter.check_key(&slow).unwrap();
604
605 let waiting_limiter = Arc::clone(&limiter);
606 let waiting_fast = fast.clone();
607 let waiting_slow = slow.clone();
608
609 let request = test_task::spawn(async move {
610 waiting_limiter
611 .await_keys_ready(Some(&[waiting_fast, waiting_slow]))
612 .await;
613 });
614 test_task::yield_now().await;
615
616 limiter.check_key(&fast).unwrap();
617 assert!(!request.is_finished());
618
619 advance_test_clock(Duration::from_millis(9_999)).await;
620 limiter.until_key_ready(&fast).await;
621 limiter.until_key_ready(&fast).await;
622 advance_test_clock(Duration::from_millis(1)).await;
623 test_task::yield_now().await;
624 assert!(!request.is_finished());
625
626 advance_test_clock(Duration::from_millis(998)).await;
627 test_task::yield_now().await;
628 assert!(!request.is_finished());
629
630 advance_test_clock(Duration::from_millis(1)).await;
631 request.await.unwrap();
632
633 assert!(limiter.check_key(&fast).is_err());
634 assert!(limiter.check_key(&slow).is_err());
635 }
636
637 #[cfg(all(feature = "simulation", madsim))]
638 async fn advance_test_clock(duration: Duration) {
639 madsim::time::advance(duration);
640 test_task::yield_now().await;
641 }
642
643 #[cfg(not(all(feature = "simulation", madsim)))]
644 async fn advance_test_clock(duration: Duration) {
645 tokio::time::advance(duration).await;
646 }
647
648 #[rstest]
649 fn test_per_second_returns_none_on_zero_replenish_interval() {
650 assert!(Quota::per_second(NonZeroU32::new(u32::MAX).unwrap()).is_none());
651 }
652
653 #[rstest]
654 fn test_per_minute_accepts_max_burst() {
655 let quota = Quota::per_minute(NonZeroU32::new(u32::MAX).unwrap());
656 assert!(quota.replenish_interval().as_nanos() > 0);
657 }
658
659 #[rstest]
660 fn test_per_hour_accepts_max_burst() {
661 let quota = Quota::per_hour(NonZeroU32::new(u32::MAX).unwrap());
662 assert!(quota.replenish_interval().as_nanos() > 0);
663 }
664
665 mod property_tests {
666 use proptest::prelude::*;
667 use rstest::rstest;
668
669 use crate::ratelimiter::nanos::Nanos;
670
671 proptest! {
672 #![proptest_config(ProptestConfig {
673 failure_persistence: Some(Box::new(
674 proptest::test_runner::FileFailurePersistence::WithSource("ratelimiter")
675 )),
676 ..ProptestConfig::default()
677 })]
678
679 #[rstest]
681 fn nanos_operators_never_panic(a in proptest::num::u64::ANY, b in proptest::num::u64::ANY) {
682 let na = Nanos::from(a);
683 let nb = Nanos::from(b);
684
685 prop_assert_eq!((na + nb).as_u64(), a.saturating_add(b));
686 prop_assert_eq!((na * b).as_u64(), a.saturating_mul(b));
687 prop_assert_eq!(na.saturating_sub(nb).as_u64(), a.saturating_sub(b));
688 }
689 }
690 }
691
692 #[rstest]
693 fn test_gcra_boundary_exact_replenishment() {
694 let mock_limiter = initialize_mock_rate_limiter();
697 let key = "boundary_test".to_string();
698
699 assert!(mock_limiter.check_key(&key).is_ok());
700 assert!(mock_limiter.check_key(&key).is_ok());
701 assert!(mock_limiter.check_key(&key).is_err());
702
703 let quota = Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap();
705 let replenish_interval = quota.replenish_interval();
706 mock_limiter.advance_clock(replenish_interval);
707
708 assert!(
709 mock_limiter.check_key(&key).is_ok(),
710 "Request at exact replenish boundary should be allowed"
711 );
712 assert!(
713 mock_limiter.check_key(&key).is_err(),
714 "Immediate follow-up should be rate-limited"
715 );
716 }
717
718 #[rstest]
719 fn test_per_second_boundary_exact_limit() {
720 let quota = Quota::per_second(NonZeroU32::new(1_000_000_000).unwrap()).unwrap();
722 assert_eq!(quota.replenish_interval().as_nanos(), 1);
723 }
724
725 #[rstest]
726 fn test_per_second_returns_none_above_one_billion() {
727 assert!(Quota::per_second(NonZeroU32::new(1_000_000_001).unwrap()).is_none());
729 }
730
731 #[rstest]
732 #[case::large(Duration::from_secs(100), u32::MAX, Duration::from_mins(7_158_278_825))]
733 #[case::saturated(Duration::MAX, 2, Duration::MAX)]
734 fn test_burst_size_replenished_in(
735 #[case] replenish_interval: Duration,
736 #[case] burst_size: u32,
737 #[case] expected: Duration,
738 ) {
739 let quota = Quota::with_period(replenish_interval)
740 .unwrap()
741 .allow_burst(NonZeroU32::new(burst_size).unwrap());
742
743 assert_eq!(quota.burst_size_replenished_in(), expected);
744 }
745
746 #[rstest]
747 #[should_panic(expected = "t cannot be zero")]
748 fn test_from_gcra_parameters_panics_on_zero_t() {
749 let _ = Quota::from_gcra_parameters(Nanos::from(0u64), Nanos::from(100u64));
750 }
751
752 #[rstest]
753 #[should_panic(expected = "tau/t results in zero burst capacity")]
754 fn test_from_gcra_parameters_panics_on_zero_division() {
755 let _ = Quota::from_gcra_parameters(Nanos::from(2u64), Nanos::from(1u64));
757 }
758
759 #[rstest]
760 #[should_panic(expected = "tau/t exceeds u32::MAX")]
761 fn test_from_gcra_parameters_panics_on_overflow() {
762 let _ = Quota::from_gcra_parameters(Nanos::from(1u64), Nanos::from(u64::MAX));
763 }
764
765 #[rstest]
766 fn test_concurrent_check_key_respects_burst() {
767 let rate = 10u32;
768 let clock = FakeRelativeClock::default();
769 let start = clock.now();
770 let limiter = RateLimiter {
771 default_gcra: Some(Gcra::new(
772 Quota::per_second(NonZeroU32::new(rate).unwrap()).unwrap(),
773 )),
774 state: DashMapStateStore::new(),
775 gcra: DashMap::new(),
776 clock,
777 start,
778 decision_lock: parking_lot::Mutex::new(()),
779 };
780
781 let accepted = AtomicU32::new(0);
782 let num_threads = 50;
783
784 std::thread::scope(|s| {
786 for _ in 0..num_threads {
787 s.spawn(|| {
788 if limiter.check_key(&"hot_key".to_string()).is_ok() {
789 accepted.fetch_add(1, Ordering::Relaxed);
790 }
791 });
792 }
793 });
794
795 let total = accepted.load(Ordering::Relaxed);
796 assert!(total >= 1, "At least one request should be accepted");
797 assert!(
798 total <= rate,
799 "Accepted {total} but burst capacity is {rate}"
800 );
801 }
802}