Skip to main content

rama_net/rate/
keyed.rs

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
15/// A limit [`Policy`] that rate limits inputs *per key*: every key gets
16/// its own token bucket, lazily created on first use and stored in a
17/// bounded, idle-evicting cache.
18///
19/// The typical use is per-client fairness, keying on the client IP with
20/// [`ClientIpRateKey`](super::ClientIpRateKey); any
21/// [`InputToRateKey`] extractor (including plain closures) works.
22///
23/// Modes mirror [`RatePolicy`](rama_core::layer::limit::policy::RatePolicy):
24/// [`KeyedRatePolicy::abort`] rejects over-budget inputs with
25/// [`RateLimitReached`] (a 429 path), [`KeyedRatePolicy::wait`] paces them.
26/// Inputs without a derivable key are allowed through by default. This is
27/// convenient for stacks where the key is genuinely optional, but is
28/// fail-open when the extractor depends on missing metadata; security limits
29/// should set [`KeyedRatePolicy::set_missing_key_allowed`] to `false` and
30/// abort them with [`MissingRateKey`] instead.
31///
32/// Memory is bounded: at most [`KeyedRatePolicy::set_max_keys`] buckets
33/// are kept, and buckets idle longer than
34/// [`KeyedRatePolicy::set_idle_timeout`] are evicted. The idle timeout is
35/// clamped to the time it takes to refill the configured burst from empty,
36/// so a bucket evicted for *idleness* and recreated full cannot regain
37/// budget any faster than one that stayed cached.
38///
39/// A new key is rejected with [`RateKeyCapacityReached`] while `max_keys`
40/// non-idle buckets are live. Live buckets are never evicted to admit another
41/// key, because recreating an exhausted bucket full would let callers bypass
42/// the rate limit by cycling keys at the memory bound.
43///
44/// Size `max_keys` for the number of simultaneously active keys after
45/// aggregation. The default IPv6 `/64` aggregation means one routed `/48`
46/// can still fill the default 65 536-key capacity; deployments serving larger
47/// IPv6 populations can aggregate more broadly with
48/// [`ClientIpRateKey::set_ipv6_prefix`](super::ClientIpRateKey::set_ipv6_prefix)
49/// and/or raise this bound.
50pub 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    /// Create a new [`KeyedRatePolicy`] that paces inputs beyond the
89    /// given per-key [`Rate`]. A known key waits rather than failing when its
90    /// bucket is empty; a new key can still fail closed when the configured
91    /// key capacity is exhausted.
92    pub fn wait(extractor: X, rate: Rate) -> Self {
93        Self::new(extractor, rate, Mode::Wait)
94    }
95
96    /// Create a new [`KeyedRatePolicy`] that aborts inputs beyond the
97    /// given per-key [`Rate`] with [`RateLimitReached`].
98    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        /// Override the per-key burst capacity
123        /// (default: one period worth of units).
124        ///
125        /// # Panics
126        ///
127        /// Panics if `burst` is zero.
128        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        /// Allow (default) or abort — with [`MissingRateKey`] — inputs
138        /// for which no key can be derived.
139        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        /// Bound the number of tracked keys (default: 65 536). When all
147        /// tracked buckets are still active, a new key is rejected with
148        /// [`RateKeyCapacityReached`] instead of evicting a live bucket and
149        /// resetting its budget.
150        ///
151        /// This is an availability bound as well as a memory bound. With the
152        /// default IPv6 `/64` keys, one `/48` contains 65 536 distinct keys.
153        /// Aggregate more broadly or raise this value when that is a realistic
154        /// share of the expected active client population.
155        ///
156        /// # Panics
157        ///
158        /// Panics if `max_keys` is zero.
159        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        /// Evict buckets idle for this long (default: 1 minute), clamped
169        /// to at least the time required to refill the burst from empty.
170        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    /// (Re)build the bucket cache; changing storage config
178    /// drops all live buckets.
179    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        // Reverse chronological order makes BinaryHeap a min-heap.
234        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        // Cleanup is only relevant when a new key arrives. Keeping the hot
264        // existing-key path independent of the number of simultaneously
265        // expired entries avoids a periodic O(max_keys) latency spike.
266        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
327/// Add even a platform-unrepresentable timeout without dropping the cache
328/// entry from the expiration index. Only such extreme values are shortened.
329fn 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                    // defence-in-depth: cost is 1 and burst is non-zero
411                    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        // same client again: over budget, with a downcastable error
468        let err = assert_abort(policy.check(input_for_ip([10, 0, 0, 1])).await);
469        assert!(err.downcast_ref::<RateLimitReached>().is_some());
470
471        // a different client has its own bucket
472        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        // no SocketInfo extension: no key
492        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        // over budget for .1, but .2 is instant
528        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}