Skip to main content

rate_net/
eviction.rs

1//! How the per-key store bounds its memory.
2
3use core::time::Duration;
4
5/// The default key-capacity cap, applied unless overridden.
6///
7/// Roughly a million keys. Generous for legitimate traffic, but a hard ceiling
8/// so a flood of unique keys can never grow the store without limit. Pick a
9/// value that matches your real key cardinality with [`Eviction::capacity`].
10pub const DEFAULT_MAX_KEYS: usize = 1 << 20;
11
12/// How the limiter bounds the memory its per-key state can occupy.
13///
14/// A rate limiter that tracks state per key is a denial-of-service vector
15/// against itself if that state can grow without limit: a flood of unique keys
16/// would exhaust memory. `Eviction` is the defense. Two independent bounds
17/// compose:
18///
19/// - **Capacity** — a hard ceiling on the number of live keys. When inserting a
20///   new key would exceed it, the least-recently-seen key is evicted. This is
21///   what caps a unique-key flood.
22/// - **Idle TTL** — keys not seen for longer than the TTL become evictable, so
23///   long-idle state is reclaimed rather than held forever.
24///
25/// Eviction is **lazy and incremental**: it happens while inserting a new key,
26/// touches only the one shard being written, and never sweeps the whole store or
27/// blocks the steady-state check path. The capacity is enforced approximately
28/// per shard, so the live-key count stays within a small factor of the cap.
29///
30/// The [`Default`] is safe: a [`DEFAULT_MAX_KEYS`] capacity cap and no TTL —
31/// memory is bounded out of the box. Opt into unbounded growth explicitly with
32/// [`unbounded`](Self::unbounded), understanding the risk.
33///
34/// # Examples
35///
36/// ```
37/// use rate_net::Eviction;
38/// use std::time::Duration;
39///
40/// // Cap at 100k keys and reclaim anything idle for five minutes.
41/// let policy = Eviction::capacity(100_000).with_idle(Duration::from_secs(300));
42/// assert_eq!(policy.max_keys(), Some(100_000));
43/// assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(300)));
44/// ```
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct Eviction {
47    max_keys: Option<usize>,
48    idle_ttl: Option<Duration>,
49}
50
51impl Eviction {
52    /// A hard cap of `max_keys` live keys, with no idle expiry.
53    ///
54    /// A `max_keys` of `0` is treated as `1` (the store always holds at least
55    /// one key per shard).
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use rate_net::Eviction;
61    ///
62    /// let policy = Eviction::capacity(50_000);
63    /// assert_eq!(policy.max_keys(), Some(50_000));
64    /// assert_eq!(policy.idle_ttl(), None);
65    /// ```
66    #[must_use]
67    pub const fn capacity(max_keys: usize) -> Self {
68        Self {
69            max_keys: Some(max_keys),
70            idle_ttl: None,
71        }
72    }
73
74    /// Reclaim keys idle for longer than `ttl`, keeping the default capacity cap.
75    ///
76    /// Idle expiry alone does not bound a unique-key flood — flooded keys are not
77    /// idle — so this keeps the [`DEFAULT_MAX_KEYS`] cap as the flood defense and
78    /// layers the TTL on top. Use [`capacity`](Self::capacity) plus
79    /// [`with_idle`](Self::with_idle) to choose both bounds explicitly.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use rate_net::{Eviction, DEFAULT_MAX_KEYS};
85    /// use std::time::Duration;
86    ///
87    /// let policy = Eviction::idle(Duration::from_secs(300));
88    /// assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(300)));
89    /// assert_eq!(policy.max_keys(), Some(DEFAULT_MAX_KEYS));
90    /// ```
91    #[must_use]
92    pub const fn idle(ttl: Duration) -> Self {
93        Self {
94            max_keys: Some(DEFAULT_MAX_KEYS),
95            idle_ttl: Some(ttl),
96        }
97    }
98
99    /// Both bounds at once: a `max_keys` cap and an idle `ttl`.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use rate_net::Eviction;
105    /// use std::time::Duration;
106    ///
107    /// let policy = Eviction::new(10_000, Duration::from_secs(60));
108    /// assert_eq!(policy.max_keys(), Some(10_000));
109    /// assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(60)));
110    /// ```
111    #[must_use]
112    pub const fn new(max_keys: usize, ttl: Duration) -> Self {
113        Self {
114            max_keys: Some(max_keys),
115            idle_ttl: Some(ttl),
116        }
117    }
118
119    /// No bounds at all — the store grows without limit.
120    ///
121    /// Only safe when the key space is intrinsically bounded (a fixed set of
122    /// tenants, say). Against untrusted keys this is a self-inflicted
123    /// denial-of-service: prefer [`capacity`](Self::capacity).
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use rate_net::Eviction;
129    ///
130    /// let policy = Eviction::unbounded();
131    /// assert_eq!(policy.max_keys(), None);
132    /// assert_eq!(policy.idle_ttl(), None);
133    /// ```
134    #[must_use]
135    pub const fn unbounded() -> Self {
136        Self {
137            max_keys: None,
138            idle_ttl: None,
139        }
140    }
141
142    /// Returns a copy with the capacity cap set to `max_keys`.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use rate_net::Eviction;
148    /// use std::time::Duration;
149    ///
150    /// let policy = Eviction::idle(Duration::from_secs(30)).with_capacity(1_000);
151    /// assert_eq!(policy.max_keys(), Some(1_000));
152    /// ```
153    #[must_use]
154    pub const fn with_capacity(mut self, max_keys: usize) -> Self {
155        self.max_keys = Some(max_keys);
156        self
157    }
158
159    /// Returns a copy with idle expiry set to `ttl`.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use rate_net::Eviction;
165    /// use std::time::Duration;
166    ///
167    /// let policy = Eviction::capacity(1_000).with_idle(Duration::from_secs(30));
168    /// assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(30)));
169    /// ```
170    #[must_use]
171    pub const fn with_idle(mut self, ttl: Duration) -> Self {
172        self.idle_ttl = Some(ttl);
173        self
174    }
175
176    /// Returns a copy with the capacity cap removed (unbounded key count).
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use rate_net::Eviction;
182    ///
183    /// let policy = Eviction::default().without_capacity();
184    /// assert_eq!(policy.max_keys(), None);
185    /// ```
186    #[must_use]
187    pub const fn without_capacity(mut self) -> Self {
188        self.max_keys = None;
189        self
190    }
191
192    /// The configured capacity cap, if any.
193    #[must_use]
194    pub const fn max_keys(&self) -> Option<usize> {
195        self.max_keys
196    }
197
198    /// The configured idle TTL, if any.
199    #[must_use]
200    pub const fn idle_ttl(&self) -> Option<Duration> {
201        self.idle_ttl
202    }
203}
204
205impl Default for Eviction {
206    /// A [`DEFAULT_MAX_KEYS`] capacity cap and no idle TTL — bounded memory out
207    /// of the box.
208    fn default() -> Self {
209        Self::capacity(DEFAULT_MAX_KEYS)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::{DEFAULT_MAX_KEYS, Eviction};
216    use core::time::Duration;
217
218    #[test]
219    fn test_default_is_bounded() {
220        let policy = Eviction::default();
221        assert_eq!(policy.max_keys(), Some(DEFAULT_MAX_KEYS));
222        assert_eq!(policy.idle_ttl(), None);
223    }
224
225    #[test]
226    fn test_idle_keeps_default_cap() {
227        let policy = Eviction::idle(Duration::from_secs(5));
228        assert_eq!(policy.max_keys(), Some(DEFAULT_MAX_KEYS));
229        assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(5)));
230    }
231
232    #[test]
233    fn test_unbounded_has_no_bounds() {
234        let policy = Eviction::unbounded();
235        assert_eq!(policy.max_keys(), None);
236        assert_eq!(policy.idle_ttl(), None);
237    }
238
239    #[test]
240    fn test_builders_compose() {
241        let policy = Eviction::capacity(10).with_idle(Duration::from_secs(1));
242        assert_eq!(policy.max_keys(), Some(10));
243        assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(1)));
244
245        let dropped = policy.without_capacity();
246        assert_eq!(dropped.max_keys(), None);
247        assert_eq!(dropped.idle_ttl(), Some(Duration::from_secs(1)));
248    }
249}