rate_net/builder.rs
1//! The Tier-2 builder.
2
3use std::time::Duration;
4
5use clock_lib::{Clock, SystemClock};
6
7use crate::algorithm::Algorithm;
8use crate::eviction::Eviction;
9use crate::limiter::{RateLimiter, default_shard_count};
10use crate::quota::Quota;
11
12/// A fluent builder for a [`RateLimiter`] when the Tier-1 constructors are not
13/// enough.
14///
15/// Start it with [`RateLimiter::builder`], chain the knobs you care about —
16/// algorithm, quota, burst, shard count, eviction policy, clock — and call
17/// [`build`](Self::build). Anything left unset keeps a sane default: the token
18/// bucket, default sharding, and the bounded-memory [`Eviction`] default. The
19/// quota defaults to a limit of `0` (which denies everything), so set it.
20///
21/// `build` is infallible: a zero limit produces a deny-everything limiter, and a
22/// zero period a degenerate one, rather than an error.
23///
24/// # Examples
25///
26/// ```
27/// use rate_net::{RateLimiter, Eviction};
28/// use std::time::Duration;
29///
30/// let limiter = RateLimiter::builder()
31/// .quota(1000, Duration::from_secs(60)) // 1000 / minute
32/// .burst(50) // allow short bursts of 50
33/// .shards(64) // tune for core count
34/// .eviction(Eviction::idle(Duration::from_secs(300)))
35/// .build();
36///
37/// assert_eq!(limiter.quota().limit(), 1000);
38/// assert_eq!(limiter.quota().burst(), 50);
39/// assert_eq!(limiter.shards(), 64);
40/// ```
41#[must_use = "a builder does nothing until `.build()` is called"]
42pub struct Builder<C: Clock + Clone = SystemClock> {
43 algorithm: Algorithm,
44 limit: u32,
45 period: Duration,
46 burst: Option<u32>,
47 shards: Option<usize>,
48 eviction: Eviction,
49 clock: C,
50}
51
52impl Builder<SystemClock> {
53 /// Creates a builder with default settings, driven by the OS monotonic
54 /// clock. Reached through [`RateLimiter::builder`].
55 pub(crate) fn new() -> Self {
56 Self {
57 algorithm: Algorithm::default(),
58 limit: 0,
59 period: Duration::from_secs(1),
60 burst: None,
61 shards: None,
62 eviction: Eviction::default(),
63 clock: SystemClock::new(),
64 }
65 }
66}
67
68impl<C: Clock + Clone> Builder<C> {
69 /// Selects the algorithm. Defaults to [`Algorithm::TokenBucket`]. The leaky
70 /// bucket and window algorithms require the `algorithms` feature.
71 ///
72 /// # Examples
73 ///
74 /// ```
75 /// # #[cfg(feature = "algorithms")] {
76 /// use rate_net::{RateLimiter, Algorithm};
77 /// use std::time::Duration;
78 ///
79 /// let limiter = RateLimiter::builder()
80 /// .algorithm(Algorithm::SlidingWindowCounter)
81 /// .quota(100, Duration::from_secs(1))
82 /// .build();
83 /// assert_eq!(limiter.algorithm(), Algorithm::SlidingWindowCounter);
84 /// # }
85 /// ```
86 pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
87 self.algorithm = algorithm;
88 self
89 }
90
91 /// Sets the quota: `limit` requests per `period`, per key.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// use rate_net::RateLimiter;
97 /// use std::time::Duration;
98 ///
99 /// let limiter = RateLimiter::builder().quota(5, Duration::from_millis(100)).build();
100 /// assert_eq!(limiter.quota().limit(), 5);
101 /// ```
102 pub fn quota(mut self, limit: u32, period: Duration) -> Self {
103 self.limit = limit;
104 self.period = period;
105 self
106 }
107
108 /// Sets the quota to `limit` requests per second.
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use rate_net::RateLimiter;
114 ///
115 /// let limiter = RateLimiter::builder().per_second(100).build();
116 /// assert_eq!(limiter.quota().limit(), 100);
117 /// ```
118 pub fn per_second(self, limit: u32) -> Self {
119 self.quota(limit, Duration::from_secs(1))
120 }
121
122 /// Sets the quota to `limit` requests per minute.
123 ///
124 /// # Examples
125 ///
126 /// ```
127 /// use rate_net::RateLimiter;
128 /// use std::time::Duration;
129 ///
130 /// let limiter = RateLimiter::builder().per_minute(600).build();
131 /// assert_eq!(limiter.quota().period(), Duration::from_secs(60));
132 /// ```
133 pub fn per_minute(self, limit: u32) -> Self {
134 self.quota(limit, Duration::from_secs(60))
135 }
136
137 /// Sets the burst ceiling. Defaults to the quota's limit. Applies to the
138 /// token and leaky buckets; window algorithms ignore it.
139 ///
140 /// # Examples
141 ///
142 /// ```
143 /// use rate_net::RateLimiter;
144 ///
145 /// let limiter = RateLimiter::builder().per_second(100).burst(250).build();
146 /// assert_eq!(limiter.quota().burst(), 250);
147 /// ```
148 pub fn burst(mut self, burst: u32) -> Self {
149 self.burst = Some(burst);
150 self
151 }
152
153 /// Sets the shard count (rounded up to a power of two). Defaults to a small
154 /// multiple of the core count.
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// use rate_net::RateLimiter;
160 ///
161 /// let limiter = RateLimiter::builder().per_second(1).shards(128).build();
162 /// assert_eq!(limiter.shards(), 128);
163 /// ```
164 pub fn shards(mut self, shards: usize) -> Self {
165 self.shards = Some(shards);
166 self
167 }
168
169 /// Sets the eviction policy. Defaults to a bounded-memory capacity cap.
170 ///
171 /// # Examples
172 ///
173 /// ```
174 /// use rate_net::{RateLimiter, Eviction};
175 ///
176 /// let limiter = RateLimiter::builder().per_second(1).eviction(Eviction::capacity(1_000)).build();
177 /// assert_eq!(limiter.eviction().max_keys(), Some(1_000));
178 /// ```
179 pub fn eviction(mut self, eviction: Eviction) -> Self {
180 self.eviction = eviction;
181 self
182 }
183
184 /// Sets the time source, for deterministic tests with a `ManualClock`.
185 ///
186 /// # Examples
187 ///
188 /// ```
189 /// use rate_net::RateLimiter;
190 /// use clock_lib::ManualClock;
191 /// use std::sync::Arc;
192 ///
193 /// let clock = Arc::new(ManualClock::new());
194 /// let limiter = RateLimiter::builder().per_second(5).clock(Arc::clone(&clock)).build();
195 /// assert!(limiter.check("k").is_allow());
196 /// ```
197 pub fn clock<C2: Clock + Clone>(self, clock: C2) -> Builder<C2> {
198 Builder {
199 algorithm: self.algorithm,
200 limit: self.limit,
201 period: self.period,
202 burst: self.burst,
203 shards: self.shards,
204 eviction: self.eviction,
205 clock,
206 }
207 }
208
209 /// Builds the configured limiter.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use rate_net::RateLimiter;
215 /// use std::time::Duration;
216 ///
217 /// let limiter = RateLimiter::builder().quota(10, Duration::from_secs(1)).build();
218 /// assert!(limiter.check("k").is_allow());
219 /// ```
220 #[must_use]
221 pub fn build(self) -> RateLimiter<C> {
222 let burst = self.burst.unwrap_or(self.limit);
223 let quota = Quota::from_parts(self.limit, self.period, burst);
224 let shards = self.shards.unwrap_or_else(default_shard_count);
225 RateLimiter::build(self.algorithm, quota, self.clock, shards, self.eviction)
226 }
227}
228
229#[cfg(all(test, not(loom)))]
230mod tests {
231 #![allow(clippy::unwrap_used)]
232
233 use std::sync::Arc;
234 use std::time::Duration;
235
236 use clock_lib::ManualClock;
237
238 use crate::algorithm::Algorithm;
239 use crate::eviction::Eviction;
240 use crate::limiter::RateLimiter;
241
242 #[test]
243 fn test_builder_defaults_to_token_bucket_and_set_quota() {
244 let limiter = RateLimiter::builder()
245 .quota(50, Duration::from_secs(1))
246 .build();
247 assert_eq!(limiter.algorithm(), Algorithm::TokenBucket);
248 assert_eq!(limiter.quota().limit(), 50);
249 assert_eq!(limiter.quota().burst(), 50);
250 }
251
252 #[test]
253 fn test_builder_covers_every_knob() {
254 let limiter = RateLimiter::builder()
255 .per_minute(1000)
256 .burst(50)
257 .shards(64)
258 .eviction(Eviction::capacity(100_000))
259 .build();
260 assert_eq!(limiter.quota().limit(), 1000);
261 assert_eq!(limiter.quota().period(), Duration::from_secs(60));
262 assert_eq!(limiter.quota().burst(), 50);
263 assert_eq!(limiter.shards(), 64);
264 assert_eq!(limiter.eviction().max_keys(), Some(100_000));
265 }
266
267 #[test]
268 fn test_builder_clock_injection() {
269 let clock = Arc::new(ManualClock::new());
270 let limiter = RateLimiter::builder()
271 .per_second(2)
272 .clock(Arc::clone(&clock))
273 .build();
274 assert!(limiter.check("k").is_allow());
275 assert!(limiter.check("k").is_allow());
276 assert!(limiter.check("k").is_deny());
277 clock.advance(Duration::from_secs(1));
278 assert!(limiter.check("k").is_allow());
279 }
280
281 #[test]
282 fn test_unset_quota_denies() {
283 let limiter = RateLimiter::builder().build();
284 assert!(limiter.check("k").is_deny());
285 }
286
287 #[cfg(feature = "algorithms")]
288 #[test]
289 fn test_builder_selects_algorithm() {
290 let limiter = RateLimiter::builder()
291 .algorithm(Algorithm::FixedWindow)
292 .per_second(10)
293 .build();
294 assert_eq!(limiter.algorithm(), Algorithm::FixedWindow);
295 }
296}