1use core::fmt;
2use std::cmp::Ordering;
3use std::collections::BinaryHeap;
4use std::sync::Arc;
5use std::time::Duration;
6
7use ahash::{HashMap, HashMapExt as _};
8use parking_lot::Mutex;
9use rama_core::error::{BoxError, ErrorExt as _};
10use rama_core::layer::limit::policy::{Policy, PolicyOutput, PolicyResult, RateLimitReached};
11use rama_utils::rate::{Acquire, Rate, RateLimiter};
12
13use super::InputToRateKey;
14
15pub struct KeyedRatePolicy<X, K> {
51 extractor: X,
52 rate: Rate,
53 burst: u64,
54 mode: Mode,
55 missing_key_allowed: bool,
56 max_keys: u64,
57 idle_timeout: Duration,
58 buckets: BucketCache<K>,
59}
60
61impl<X: fmt::Debug, K> fmt::Debug for KeyedRatePolicy<X, K> {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.debug_struct("KeyedRatePolicy")
64 .field("extractor", &self.extractor)
65 .field("rate", &self.rate)
66 .field("burst", &self.burst)
67 .field("mode", &self.mode)
68 .field("missing_key_allowed", &self.missing_key_allowed)
69 .field("max_keys", &self.max_keys)
70 .field("idle_timeout", &self.idle_timeout)
71 .finish_non_exhaustive()
72 }
73}
74
75#[derive(Debug, Clone, Copy)]
76enum Mode {
77 Wait,
78 Abort,
79}
80
81const DEFAULT_MAX_KEYS: u64 = 65_536;
82const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_mins(1);
83
84impl<X, K> KeyedRatePolicy<X, K>
85where
86 K: super::RateKey,
87{
88 pub fn wait(extractor: X, rate: Rate) -> Self {
93 Self::new(extractor, rate, Mode::Wait)
94 }
95
96 pub fn abort(extractor: X, rate: Rate) -> Self {
99 Self::new(extractor, rate, Mode::Abort)
100 }
101
102 fn new(extractor: X, rate: Rate, mode: Mode) -> Self {
103 let burst = rate.units();
104 Self {
105 extractor,
106 rate,
107 burst,
108 mode,
109 missing_key_allowed: true,
110 max_keys: DEFAULT_MAX_KEYS,
111 idle_timeout: DEFAULT_IDLE_TIMEOUT,
112 buckets: BucketCache::new(
113 DEFAULT_MAX_KEYS,
114 DEFAULT_IDLE_TIMEOUT.max(full_refill_time(rate, burst)),
115 rate,
116 burst,
117 ),
118 }
119 }
120
121 rama_utils::macros::generate_set_and_with! {
122 pub fn burst(mut self, burst: u64) -> Self {
129 assert!(burst > 0, "KeyedRatePolicy: burst must be non-zero");
130 self.burst = burst;
131 self.rebuild_buckets();
132 self
133 }
134 }
135
136 rama_utils::macros::generate_set_and_with! {
137 pub fn missing_key_allowed(mut self, allowed: bool) -> Self {
140 self.missing_key_allowed = allowed;
141 self
142 }
143 }
144
145 rama_utils::macros::generate_set_and_with! {
146 pub fn max_keys(mut self, max_keys: u64) -> Self {
160 assert!(max_keys > 0, "KeyedRatePolicy: max_keys must be non-zero");
161 self.max_keys = max_keys;
162 self.rebuild_buckets();
163 self
164 }
165 }
166
167 rama_utils::macros::generate_set_and_with! {
168 pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
171 self.idle_timeout = idle_timeout;
172 self.rebuild_buckets();
173 self
174 }
175 }
176
177 fn rebuild_buckets(&mut self) {
180 self.buckets = BucketCache::new(
181 self.max_keys,
182 self.idle_timeout
183 .max(full_refill_time(self.rate, self.burst)),
184 self.rate,
185 self.burst,
186 );
187 }
188
189 fn limiter(&self, key: K) -> Result<Arc<RateLimiter>, RateKeyCapacityReached> {
190 self.buckets.get_or_insert(key)
191 }
192}
193
194struct BucketCache<K> {
195 state: Mutex<BucketCacheState<K>>,
196 max_keys: u64,
197 idle_timeout: Duration,
198 rate: Rate,
199 burst: u64,
200}
201
202struct BucketCacheState<K> {
203 entries: HashMap<K, BucketEntry>,
204 expirations: BinaryHeap<Expiration<K>>,
205}
206
207struct BucketEntry {
208 limiter: Arc<RateLimiter>,
209 last_used: tokio::time::Instant,
210}
211
212struct Expiration<K> {
213 at: tokio::time::Instant,
214 key: K,
215}
216
217impl<K> PartialEq for Expiration<K> {
218 fn eq(&self, other: &Self) -> bool {
219 self.at == other.at
220 }
221}
222
223impl<K> Eq for Expiration<K> {}
224
225impl<K> PartialOrd for Expiration<K> {
226 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
227 Some(self.cmp(other))
228 }
229}
230
231impl<K> Ord for Expiration<K> {
232 fn cmp(&self, other: &Self) -> Ordering {
233 other.at.cmp(&self.at)
235 }
236}
237
238impl<K> BucketCache<K>
239where
240 K: super::RateKey,
241{
242 fn new(max_keys: u64, idle_timeout: Duration, rate: Rate, burst: u64) -> Self {
243 Self {
244 state: Mutex::new(BucketCacheState {
245 entries: HashMap::new(),
246 expirations: BinaryHeap::new(),
247 }),
248 max_keys,
249 idle_timeout,
250 rate,
251 burst,
252 }
253 }
254
255 fn get_or_insert(&self, key: K) -> Result<Arc<RateLimiter>, RateKeyCapacityReached> {
256 let now = tokio::time::Instant::now();
257 let mut state = self.state.lock();
258
259 if let Some(entry) = state.entries.get_mut(&key) {
260 entry.last_used = now;
261 return Ok(entry.limiter.clone());
262 }
263 self.expire_idle(&mut state, now);
267 if state.entries.len() as u64 >= self.max_keys {
268 return Err(RateKeyCapacityReached);
269 }
270
271 let limiter = Arc::new(RateLimiter::new(self.rate, self.burst));
272 state.entries.insert(
273 key.clone(),
274 BucketEntry {
275 limiter: limiter.clone(),
276 last_used: now,
277 },
278 );
279 state.expirations.push(Expiration {
280 at: expiration_at(now, self.idle_timeout),
281 key,
282 });
283 Ok(limiter)
284 }
285
286 fn expire_idle(&self, state: &mut BucketCacheState<K>, now: tokio::time::Instant) {
287 while state
288 .expirations
289 .peek()
290 .is_some_and(|expiry| expiry.at <= now)
291 {
292 let Some(expiry) = state.expirations.pop() else {
293 break;
294 };
295 let Some((last_used, in_use)) = state
296 .entries
297 .get(&expiry.key)
298 .map(|entry| (entry.last_used, Arc::strong_count(&entry.limiter) > 1))
299 else {
300 continue;
301 };
302 if in_use {
303 state.expirations.push(Expiration {
304 at: expiration_at(now, self.idle_timeout),
305 key: expiry.key,
306 });
307 continue;
308 }
309 match expiration_at(last_used, self.idle_timeout) {
310 at if at > now => state.expirations.push(Expiration {
311 at,
312 key: expiry.key,
313 }),
314 _ => {
315 state.entries.remove(&expiry.key);
316 }
317 }
318 }
319 }
320
321 #[cfg(test)]
322 fn len(&self) -> usize {
323 self.state.lock().entries.len()
324 }
325}
326
327fn expiration_at(now: tokio::time::Instant, mut timeout: Duration) -> tokio::time::Instant {
330 loop {
331 if let Some(at) = now.checked_add(timeout) {
332 return at;
333 }
334 timeout /= 2;
335 }
336}
337
338fn full_refill_time(rate: Rate, burst: u64) -> Duration {
339 let nanos = u128::from(burst)
340 .saturating_mul(rate.per().as_nanos())
341 .div_ceil(u128::from(rate.units()))
342 .min(Duration::MAX.as_nanos());
343 Duration::new(
344 (nanos / 1_000_000_000) as u64,
345 (nanos % 1_000_000_000) as u32,
346 )
347}
348
349rama_utils::macros::error::static_str_error! {
350 #[doc = "serve aborted: no rate key could be derived for input"]
351 pub struct MissingRateKey;
352}
353
354rama_utils::macros::error::static_str_error! {
355 #[doc = "serve aborted: the keyed rate-limit capacity is occupied by active keys"]
356 pub struct RateKeyCapacityReached;
357}
358
359impl<X, K, Input> Policy<Input> for KeyedRatePolicy<X, K>
360where
361 X: InputToRateKey<Input, Key = K>,
362 K: super::RateKey,
363 Input: Send + 'static,
364{
365 type Guard = ();
366 type Error = BoxError;
367
368 async fn check(&self, input: Input) -> PolicyResult<Input, Self::Guard, Self::Error> {
369 let key = match self.extractor.rate_key(&input) {
370 Ok(Some(key)) => key,
371 Ok(None) => {
372 let output = if self.missing_key_allowed {
373 PolicyOutput::Ready(())
374 } else {
375 PolicyOutput::Abort(MissingRateKey.into())
376 };
377 return PolicyResult { input, output };
378 }
379 Err(err) => {
380 return PolicyResult {
381 input,
382 output: PolicyOutput::Abort(err.context("derive rate key")),
383 };
384 }
385 };
386
387 let limiter = match self.limiter(key) {
388 Ok(limiter) => limiter,
389 Err(err) => {
390 return PolicyResult {
391 input,
392 output: PolicyOutput::Abort(err.into()),
393 };
394 }
395 };
396 let output = match self.mode {
397 Mode::Wait => {
398 limiter.acquire(1).await;
399 PolicyOutput::Ready(())
400 }
401 Mode::Abort => match limiter.try_acquire(1) {
402 Acquire::Granted => PolicyOutput::Ready(()),
403 Acquire::RetryAt(at) => {
404 let retry_after = limiter
405 .deadline(at)
406 .saturating_duration_since(tokio::time::Instant::now());
407 PolicyOutput::Abort(RateLimitReached::new(retry_after).into())
408 }
409 Acquire::Never => {
410 debug_assert!(false, "single unit reported Acquire::Never");
412 PolicyOutput::Abort(RateLimitReached::new(Duration::ZERO).into())
413 }
414 },
415 };
416 PolicyResult { input, output }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use rama_core::extensions::{Extensions, ExtensionsRef};
424 use std::net::{IpAddr, Ipv4Addr};
425
426 use crate::stream::SocketInfo;
427
428 fn input_for_ip(ip: [u8; 4]) -> Extensions {
429 let ext = Extensions::new();
430 ext.insert(SocketInfo::new(
431 None,
432 (IpAddr::V4(Ipv4Addr::from(ip)), 40_000).into(),
433 ));
434 ext
435 }
436
437 fn assert_ready<R, G, E>(result: PolicyResult<R, G, E>) {
438 assert!(
439 matches!(result.output, PolicyOutput::Ready(_)),
440 "unexpected output, expected ready"
441 );
442 drop(result);
443 }
444
445 fn assert_abort<R, G>(result: PolicyResult<R, G, BoxError>) -> BoxError {
446 match result.output {
447 PolicyOutput::Abort(err) => err,
448 PolicyOutput::Ready(_) | PolicyOutput::Retry => {
449 panic!("unexpected output, expected abort")
450 }
451 }
452 }
453
454 #[expect(
455 clippy::trivially_copy_pass_by_ref,
456 reason = "InputToRateKey extractors receive a reference"
457 )]
458 fn u64_key(value: &u64) -> Result<Option<u64>, BoxError> {
459 Ok(Some(*value))
460 }
461
462 #[tokio::test(start_paused = true)]
463 async fn per_key_budgets_are_independent() {
464 let policy = KeyedRatePolicy::abort(super::super::ClientIpRateKey::new(), Rate::per_sec(1));
465
466 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
467 let err = assert_abort(policy.check(input_for_ip([10, 0, 0, 1])).await);
469 assert!(err.downcast_ref::<RateLimitReached>().is_some());
470
471 assert_ready(policy.check(input_for_ip([10, 0, 0, 2])).await);
473 }
474
475 #[tokio::test(start_paused = true)]
476 async fn refills_per_key() {
477 let policy = KeyedRatePolicy::abort(super::super::ClientIpRateKey::new(), Rate::per_sec(2));
478
479 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
480 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
481 assert_abort(policy.check(input_for_ip([10, 0, 0, 1])).await);
482
483 tokio::time::advance(Duration::from_millis(500)).await;
484 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
485 }
486
487 #[tokio::test(start_paused = true)]
488 async fn missing_key_modes() {
489 let allowing =
490 KeyedRatePolicy::abort(super::super::ClientIpRateKey::new(), Rate::per_sec(1));
491 assert_ready(allowing.check(Extensions::new()).await);
493 assert_ready(allowing.check(Extensions::new()).await);
494
495 let strict = KeyedRatePolicy::abort(super::super::ClientIpRateKey::new(), Rate::per_sec(1))
496 .with_missing_key_allowed(false);
497 let err = assert_abort(strict.check(Extensions::new()).await);
498 assert!(err.downcast_ref::<MissingRateKey>().is_some());
499 }
500
501 #[tokio::test(start_paused = true)]
502 async fn closure_extractor() {
503 let policy = KeyedRatePolicy::abort(
504 |input: &Extensions| {
505 Ok(input
506 .extensions()
507 .get_ref::<SocketInfo>()
508 .map(|info| u64::from(info.peer_addr().port)))
509 },
510 Rate::per_sec(1),
511 );
512
513 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
514 assert_abort(policy.check(input_for_ip([10, 0, 0, 1])).await);
515 }
516
517 #[tokio::test(start_paused = true)]
518 async fn wait_mode_paces_per_key() {
519 let policy = KeyedRatePolicy::wait(super::super::ClientIpRateKey::new(), Rate::per_sec(10));
520
521 let start = tokio::time::Instant::now();
522 for _ in 0..10 {
523 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
524 }
525 assert_eq!(start.elapsed(), Duration::ZERO);
526
527 assert_ready(policy.check(input_for_ip([10, 0, 0, 2])).await);
529 assert_eq!(start.elapsed(), Duration::ZERO);
530
531 assert_ready(policy.check(input_for_ip([10, 0, 0, 1])).await);
532 assert_eq!(start.elapsed(), Duration::from_millis(100));
533 }
534
535 #[test]
536 fn idle_timeout_covers_a_full_burst_refill() {
537 assert_eq!(
538 full_refill_time(Rate::per_sec(2), 5),
539 Duration::from_millis(2_500)
540 );
541 assert_eq!(
542 full_refill_time(Rate::new(3, Duration::from_millis(10)), 1),
543 Duration::from_nanos(3_333_334)
544 );
545 }
546
547 #[test]
548 fn unrepresentable_idle_timeout_stays_in_the_expiration_index() {
549 let buckets = BucketCache::<u64>::new(1, Duration::MAX, Rate::per_sec(1), 1);
550 let _limiter = buckets.get_or_insert(1).expect("insert bucket");
551 assert_eq!(buckets.state.lock().expirations.len(), 1);
552 }
553
554 #[test]
555 #[should_panic(expected = "burst must be non-zero")]
556 fn zero_burst_is_rejected_at_configuration_time() {
557 drop(
558 KeyedRatePolicy::<_, IpAddr>::abort(
559 super::super::ClientIpRateKey::new(),
560 Rate::per_sec(1),
561 )
562 .with_burst(0),
563 );
564 }
565
566 #[test]
567 #[should_panic(expected = "max_keys must be non-zero")]
568 fn zero_max_keys_is_rejected_at_configuration_time() {
569 drop(
570 KeyedRatePolicy::<_, IpAddr>::abort(
571 super::super::ClientIpRateKey::new(),
572 Rate::per_sec(1),
573 )
574 .with_max_keys(0),
575 );
576 }
577
578 #[tokio::test]
579 async fn capacity_rejects_new_keys_without_resetting_live_buckets() {
580 let policy = KeyedRatePolicy::abort(u64_key, Rate::per_sec(1)).with_max_keys(1);
581
582 assert_ready(policy.check(1).await);
583 assert!(
584 assert_abort(policy.check(1).await)
585 .downcast_ref::<RateLimitReached>()
586 .is_some()
587 );
588
589 let err = assert_abort(policy.check(2).await);
590 assert!(err.downcast_ref::<RateKeyCapacityReached>().is_some());
591
592 assert!(
593 assert_abort(policy.check(1).await)
594 .downcast_ref::<RateLimitReached>()
595 .is_some(),
596 "refusing key 2 must not reset key 1",
597 );
598 assert_eq!(policy.buckets.len(), 1);
599 }
600
601 #[tokio::test(start_paused = true)]
602 async fn fully_refilled_idle_bucket_makes_room_for_a_new_key() {
603 let policy = KeyedRatePolicy::abort(u64_key, Rate::per_sec(1))
604 .with_max_keys(1)
605 .with_idle_timeout(Duration::ZERO);
606
607 assert_ready(policy.check(1).await);
608 assert!(
609 assert_abort(policy.check(2).await)
610 .downcast_ref::<RateKeyCapacityReached>()
611 .is_some()
612 );
613
614 tokio::time::advance(Duration::from_secs(1)).await;
615 assert_ready(policy.check(2).await);
616 assert_eq!(policy.buckets.len(), 1);
617 }
618
619 #[tokio::test(start_paused = true)]
620 async fn a_waiting_acquisition_keeps_its_bucket_live() {
621 let policy = Arc::new(
622 KeyedRatePolicy::wait(u64_key, Rate::per_sec(1))
623 .with_max_keys(1)
624 .with_idle_timeout(Duration::ZERO),
625 );
626
627 assert_ready(policy.check(1).await);
628 let first = {
629 let policy = policy.clone();
630 tokio::spawn(async move { policy.check(1).await })
631 };
632 let second = {
633 let policy = policy.clone();
634 tokio::spawn(async move { policy.check(1).await })
635 };
636 tokio::task::yield_now().await;
637
638 tokio::time::advance(Duration::from_secs(1)).await;
639 tokio::task::yield_now().await;
640 assert!(
641 assert_abort(policy.check(2).await)
642 .downcast_ref::<RateKeyCapacityReached>()
643 .is_some(),
644 "a bucket with an active waiter must not be evicted",
645 );
646
647 tokio::time::advance(Duration::from_secs(1)).await;
648 assert_ready(first.await.unwrap());
649 assert_ready(second.await.unwrap());
650 tokio::time::advance(Duration::from_secs(1)).await;
651 assert_ready(policy.check(2).await);
652 }
653}