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 #[case(1)]
474 #[case(3)]
475 #[case(8)]
476 fn test_burst_capacity_and_pacing_after_idle(#[case] burst: u32) {
477 let interval = Duration::from_nanos(100);
478 let quota = Quota::with_period(interval)
479 .unwrap()
480 .allow_burst(NonZeroU32::new(burst).unwrap());
481 let clock = FakeRelativeClock::default();
482 let limiter = RateLimiter::new_with_clock(Some(quota), vec![], clock.clone());
483 let key = "requests";
484
485 for idle in [
486 Duration::ZERO,
487 Duration::from_secs(1),
488 Duration::from_secs(1),
489 ] {
490 clock.advance(idle);
491
492 for _ in 0..burst {
493 assert_eq!(limiter.check_key(&key), Ok(()));
494 }
495
496 let denied = limiter.check_key(&key).unwrap_err();
497 assert_eq!(denied.quota(), quota);
498 assert_eq!(denied.wait_time_from(clock.now()), interval);
499 clock.advance(Duration::from_nanos(99));
500 assert_eq!(
501 limiter
502 .check_key(&key)
503 .unwrap_err()
504 .wait_time_from(clock.now()),
505 Duration::from_nanos(1),
506 );
507 clock.advance(Duration::from_nanos(1));
508 assert_eq!(limiter.check_key(&key), Ok(()));
509 assert_eq!(
510 limiter
511 .check_key(&key)
512 .unwrap_err()
513 .wait_time_from(clock.now()),
514 interval,
515 );
516 }
517 }
518
519 #[rstest]
520 #[case(1)]
521 #[case(3)]
522 #[case(8)]
523 #[tokio::test]
524 async fn test_multi_key_admission_after_idle_preserves_capacity_and_atomicity(
525 #[case] burst: u32,
526 ) {
527 let interval = Duration::from_nanos(100);
528 let quota = Quota::with_period(interval)
529 .unwrap()
530 .allow_burst(NonZeroU32::new(burst).unwrap());
531 let clock = FakeRelativeClock::default();
532 let limiter = RateLimiter::new_with_clock(Some(quota), vec![], clock.clone());
533 let keys = ["first", "second"];
534
535 for _ in 0..burst {
536 limiter.await_keys_ready(Some(&keys)).await;
537 }
538
539 clock.advance(Duration::from_secs(1));
540 let now = clock.now();
541
542 for _ in 0..burst {
543 limiter.await_keys_ready(Some(&keys)).await;
544 assert_eq!(clock.now(), now);
545 }
546
547 assert_eq!(limiter.plan_keys(&["spare", "first"], now), Err(interval));
548 assert!(!limiter.state.contains_key(&"spare"));
549 limiter.await_keys_ready(Some(&["spare", "first"])).await;
550 assert_eq!(clock.now(), now + Nanos::from(interval));
551 assert_eq!(
552 limiter
553 .check_key(&"first")
554 .unwrap_err()
555 .wait_time_from(clock.now()),
556 interval
557 );
558
559 for _ in 1..burst {
560 assert_eq!(limiter.check_key(&"spare"), Ok(()));
561 }
562
563 assert_eq!(
564 limiter
565 .check_key(&"spare")
566 .unwrap_err()
567 .wait_time_from(clock.now()),
568 interval
569 );
570 }
571
572 #[rstest]
573 fn test_custom_key_quota() {
574 let mock_limiter = initialize_mock_rate_limiter();
575
576 mock_limiter.add_quota_for_key(
578 "custom".to_string(),
579 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
580 );
581
582 assert!(mock_limiter.check_key(&"custom".to_string()).is_ok());
584 assert!(mock_limiter.check_key(&"custom".to_string()).is_err());
585
586 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
588 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
589 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
590 }
591
592 #[rstest]
593 fn test_multiple_keys() {
594 let mock_limiter = initialize_mock_rate_limiter();
595
596 mock_limiter.add_quota_for_key(
597 "key1".to_string(),
598 Quota::per_second(NonZeroU32::new(1).unwrap()).unwrap(),
599 );
600 mock_limiter.add_quota_for_key(
601 "key2".to_string(),
602 Quota::per_second(NonZeroU32::new(3).unwrap()).unwrap(),
603 );
604
605 assert!(mock_limiter.check_key(&"key1".to_string()).is_ok());
607 assert!(mock_limiter.check_key(&"key1".to_string()).is_err());
608
609 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
611 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
612 assert!(mock_limiter.check_key(&"key2".to_string()).is_ok());
613 assert!(mock_limiter.check_key(&"key2".to_string()).is_err());
614 }
615
616 #[rstest]
617 fn test_quota_reset() {
618 let mock_limiter = initialize_mock_rate_limiter();
619
620 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
622 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
623 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
624
625 mock_limiter.advance_clock(Duration::from_millis(499));
627 assert!(mock_limiter.check_key(&"reset".to_string()).is_err());
628
629 mock_limiter.advance_clock(Duration::from_millis(501));
631 assert!(mock_limiter.check_key(&"reset".to_string()).is_ok());
632 }
633
634 #[rstest]
635 fn test_different_quotas() {
636 let mock_limiter = initialize_mock_rate_limiter();
637
638 mock_limiter.add_quota_for_key(
639 "per_second".to_string(),
640 Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap(),
641 );
642 mock_limiter.add_quota_for_key(
643 "per_minute".to_string(),
644 Quota::per_minute(NonZeroU32::new(3).unwrap()),
645 );
646
647 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
649 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
650 assert!(mock_limiter.check_key(&"per_second".to_string()).is_err());
651
652 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
654 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
655 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_ok());
656 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
657
658 mock_limiter.advance_clock(Duration::from_secs(1));
660 assert!(mock_limiter.check_key(&"per_second".to_string()).is_ok());
661 assert!(mock_limiter.check_key(&"per_minute".to_string()).is_err());
662 }
663
664 #[tokio::test]
665 async fn test_await_keys_ready() {
666 let mock_limiter = initialize_mock_rate_limiter();
667
668 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
670 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
671
672 assert!(mock_limiter.check_key(&"default".to_string()).is_err());
674
675 mock_limiter.advance_clock(Duration::from_secs(1));
677 let keys = ["default".to_string()];
678 mock_limiter.await_keys_ready(Some(keys.as_slice())).await;
679 assert!(mock_limiter.check_key(&"default".to_string()).is_ok());
680 }
681
682 #[cfg_attr(
683 not(all(feature = "simulation", madsim)),
684 tokio::test(start_paused = true)
685 )]
686 #[cfg_attr(all(feature = "simulation", madsim), madsim::test)]
687 async fn test_await_keys_ready_reserves_keys_together() {
688 let fast = "fast".to_string();
689 let slow = "slow".to_string();
690 let limiter = Arc::new(RateLimiter::new_with_quota(
691 None,
692 vec![
693 (
694 fast.clone(),
695 Quota::with_period(Duration::from_secs(1)).unwrap(),
696 ),
697 (
698 slow.clone(),
699 Quota::with_period(Duration::from_secs(10)).unwrap(),
700 ),
701 ],
702 ));
703 limiter.check_key(&slow).unwrap();
704
705 let waiting_limiter = Arc::clone(&limiter);
706 let waiting_fast = fast.clone();
707 let waiting_slow = slow.clone();
708
709 let request = test_task::spawn(async move {
710 waiting_limiter
711 .await_keys_ready(Some(&[waiting_fast, waiting_slow]))
712 .await;
713 });
714 test_task::yield_now().await;
715
716 limiter.check_key(&fast).unwrap();
717 assert!(!request.is_finished());
718
719 advance_test_clock(Duration::from_millis(9_999)).await;
720 limiter.until_key_ready(&fast).await;
721 advance_test_clock(Duration::from_millis(1)).await;
722 test_task::yield_now().await;
723 assert!(!request.is_finished());
724
725 advance_test_clock(Duration::from_millis(998)).await;
726 test_task::yield_now().await;
727 assert!(!request.is_finished());
728
729 advance_test_clock(Duration::from_millis(1)).await;
730 request.await.unwrap();
731
732 assert!(limiter.check_key(&fast).is_err());
733 assert!(limiter.check_key(&slow).is_err());
734 }
735
736 #[cfg(all(feature = "simulation", madsim))]
737 async fn advance_test_clock(duration: Duration) {
738 madsim::time::advance(duration);
739 test_task::yield_now().await;
740 }
741
742 #[cfg(not(all(feature = "simulation", madsim)))]
743 async fn advance_test_clock(duration: Duration) {
744 tokio::time::advance(duration).await;
745 }
746
747 #[rstest]
748 fn test_per_second_returns_none_on_zero_replenish_interval() {
749 assert!(Quota::per_second(NonZeroU32::new(u32::MAX).unwrap()).is_none());
750 }
751
752 #[rstest]
753 fn test_per_minute_accepts_max_burst() {
754 let quota = Quota::per_minute(NonZeroU32::new(u32::MAX).unwrap());
755 assert_eq!(quota.replenish_interval(), Duration::from_nanos(13));
756 }
757
758 #[rstest]
759 fn test_per_hour_accepts_max_burst() {
760 let quota = Quota::per_hour(NonZeroU32::new(u32::MAX).unwrap());
761 assert_eq!(quota.replenish_interval(), Duration::from_nanos(838));
762 }
763
764 mod property_tests {
765 use proptest::prelude::*;
766 use rstest::rstest;
767
768 use crate::ratelimiter::nanos::Nanos;
769
770 proptest! {
771 #![proptest_config(ProptestConfig {
772 failure_persistence: Some(Box::new(
773 proptest::test_runner::FileFailurePersistence::WithSource("ratelimiter")
774 )),
775 ..ProptestConfig::default()
776 })]
777
778 #[rstest]
780 fn nanos_operators_never_panic(a in proptest::num::u64::ANY, b in proptest::num::u64::ANY) {
781 let na = Nanos::from(a);
782 let nb = Nanos::from(b);
783
784 prop_assert_eq!((na + nb).as_u64(), a.saturating_add(b));
785 prop_assert_eq!((na * b).as_u64(), a.saturating_mul(b));
786 prop_assert_eq!(na.saturating_sub(nb).as_u64(), a.saturating_sub(b));
787 }
788 }
789 }
790
791 #[rstest]
792 fn test_gcra_boundary_exact_replenishment() {
793 let mock_limiter = initialize_mock_rate_limiter();
796 let key = "boundary_test".to_string();
797
798 assert!(mock_limiter.check_key(&key).is_ok());
799 assert!(mock_limiter.check_key(&key).is_ok());
800 assert!(mock_limiter.check_key(&key).is_err());
801
802 let quota = Quota::per_second(NonZeroU32::new(2).unwrap()).unwrap();
804 let replenish_interval = quota.replenish_interval();
805 mock_limiter.advance_clock(replenish_interval);
806
807 assert!(
808 mock_limiter.check_key(&key).is_ok(),
809 "Request at exact replenish boundary should be allowed"
810 );
811 assert!(
812 mock_limiter.check_key(&key).is_err(),
813 "Immediate follow-up should be rate-limited"
814 );
815 }
816
817 #[rstest]
818 fn test_per_second_boundary_exact_limit() {
819 let quota = Quota::per_second(NonZeroU32::new(1_000_000_000).unwrap()).unwrap();
821 assert_eq!(quota.replenish_interval().as_nanos(), 1);
822 }
823
824 #[rstest]
825 fn test_per_second_returns_none_above_one_billion() {
826 assert!(Quota::per_second(NonZeroU32::new(1_000_000_001).unwrap()).is_none());
828 }
829
830 #[rstest]
831 #[case::large(Duration::from_secs(100), u32::MAX, Duration::from_mins(7_158_278_825))]
832 #[case::saturated(Duration::MAX, 2, Duration::MAX)]
833 fn test_burst_size_replenished_in(
834 #[case] replenish_interval: Duration,
835 #[case] burst_size: u32,
836 #[case] expected: Duration,
837 ) {
838 let quota = Quota::with_period(replenish_interval)
839 .unwrap()
840 .allow_burst(NonZeroU32::new(burst_size).unwrap());
841
842 assert_eq!(quota.burst_size_replenished_in(), expected);
843 }
844
845 #[rstest]
846 #[should_panic(expected = "t cannot be zero")]
847 fn test_from_gcra_parameters_panics_on_zero_t() {
848 let _ = Quota::from_gcra_parameters(Nanos::from(0u64), Nanos::from(100u64));
849 }
850
851 #[rstest]
852 #[should_panic(expected = "tau/t results in zero burst capacity")]
853 fn test_from_gcra_parameters_panics_on_zero_division() {
854 let _ = Quota::from_gcra_parameters(Nanos::from(2u64), Nanos::from(1u64));
856 }
857
858 #[rstest]
859 #[should_panic(expected = "tau/t exceeds u32::MAX")]
860 fn test_from_gcra_parameters_panics_on_overflow() {
861 let _ = Quota::from_gcra_parameters(Nanos::from(1u64), Nanos::from(u64::MAX));
862 }
863
864 #[rstest]
865 fn test_concurrent_check_key_respects_burst() {
866 let rate = 10u32;
867 let clock = FakeRelativeClock::default();
868 let start = clock.now();
869 let limiter = RateLimiter {
870 default_gcra: Some(Gcra::new(
871 Quota::per_second(NonZeroU32::new(rate).unwrap()).unwrap(),
872 )),
873 state: DashMapStateStore::new(),
874 gcra: DashMap::new(),
875 clock,
876 start,
877 decision_lock: parking_lot::Mutex::new(()),
878 };
879
880 let accepted = AtomicU32::new(0);
881 let num_threads = 50;
882
883 std::thread::scope(|s| {
885 for _ in 0..num_threads {
886 s.spawn(|| {
887 if limiter.check_key(&"hot_key".to_string()).is_ok() {
888 accepted.fetch_add(1, Ordering::Relaxed);
889 }
890 });
891 }
892 });
893
894 let total = accepted.load(Ordering::Relaxed);
895 assert!(total >= 1, "At least one request should be accepted");
896 assert!(
897 total <= rate,
898 "Accepted {total} but burst capacity is {rate}"
899 );
900 }
901}