reliakit_ratelimit/lib.rs
1//! Clock-agnostic token-bucket rate limiter.
2//!
3//! A token bucket holds up to `capacity` tokens and refills `refill_amount`
4//! tokens every `refill_interval` time units. Each request takes one or more
5//! tokens; when the bucket is empty, requests are denied until it refills. The
6//! `capacity` sets the largest burst you will allow; the refill rate sets the
7//! sustained throughput.
8//!
9//! [`RateLimiter`] is a small, `Copy` value. It does **not** read the clock,
10//! sleep, or allocate — you pass the current time in on each call as a plain
11//! `u64` in whatever monotonic unit you choose (milliseconds is typical), and
12//! the intervals use that same unit. That keeps it usable from synchronous
13//! code, any async runtime, and `no_std` / embedded targets, and makes every
14//! decision deterministic and easy to test. All arithmetic is integer-only and
15//! saturating, so no call ever overflows or panics.
16//!
17//! # Example
18//!
19//! ```
20//! use reliakit_ratelimit::RateLimiter;
21//!
22//! // Allow bursts of up to 10, refilling 1 token every 100ms (~10/sec).
23//! let mut limiter = RateLimiter::new(10, 1, 100);
24//!
25//! // The bucket starts full, so a burst of 10 is allowed immediately.
26//! for _ in 0..10 {
27//! assert!(limiter.try_acquire_one(0));
28//! }
29//! // The 11th is denied until the bucket refills.
30//! assert!(!limiter.try_acquire_one(0));
31//! assert_eq!(limiter.retry_after(0, 1), Some(100)); // one token in 100ms
32//!
33//! // After 100ms exactly one token is back.
34//! assert!(limiter.try_acquire_one(100));
35//! assert!(!limiter.try_acquire_one(100));
36//! ```
37//!
38//! # Feature flags
39//!
40//! - `core` (off by default) adds `*_now(clock)` convenience methods on
41//! [`RateLimiter`] that read the time from a `reliakit_core::Clock`. It pulls
42//! in `reliakit-core` (`no_std`, zero third-party dependencies); the
43//! `now: u64` methods remain the primitive API.
44
45#![no_std]
46#![forbid(unsafe_code)]
47#![warn(missing_docs)]
48
49use core::cmp::min;
50
51/// A token-bucket rate limiter.
52///
53/// Construct one with [`RateLimiter::new`], then gate work with
54/// [`try_acquire`](Self::try_acquire) / [`try_acquire_one`](Self::try_acquire_one).
55/// The bucket starts full (a burst of up to `capacity` is allowed immediately).
56///
57/// Time is a plain `u64` in any monotonic unit you choose (commonly
58/// milliseconds); `refill_interval` uses that same unit. The limiter never reads
59/// the clock itself — pass `now` to each method.
60///
61/// `RateLimiter` is not internally synchronized. Share one across threads by
62/// wrapping it in your own `Mutex`/lock.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct RateLimiter {
65 capacity: u64,
66 refill_amount: u64,
67 refill_interval: u64,
68 tokens: u64,
69 last_refill: u64,
70}
71
72impl RateLimiter {
73 /// Creates a limiter that holds up to `capacity` tokens and adds
74 /// `refill_amount` tokens every `refill_interval` time units.
75 ///
76 /// The bucket starts full. `capacity`, `refill_amount`, and
77 /// `refill_interval` are each clamped to a minimum of `1` (a zero interval
78 /// would never refill and would divide by zero).
79 pub const fn new(capacity: u64, refill_amount: u64, refill_interval: u64) -> Self {
80 let capacity = if capacity == 0 { 1 } else { capacity };
81 Self {
82 capacity,
83 refill_amount: if refill_amount == 0 { 1 } else { refill_amount },
84 refill_interval: if refill_interval == 0 {
85 1
86 } else {
87 refill_interval
88 },
89 tokens: capacity,
90 last_refill: 0,
91 }
92 }
93
94 /// Returns the bucket capacity (the maximum burst).
95 pub const fn capacity(&self) -> u64 {
96 self.capacity
97 }
98
99 /// Returns how many tokens are added on each refill.
100 pub const fn refill_amount(&self) -> u64 {
101 self.refill_amount
102 }
103
104 /// Returns the refill interval, in the caller's time unit.
105 pub const fn refill_interval(&self) -> u64 {
106 self.refill_interval
107 }
108
109 /// Refills the bucket for any whole intervals elapsed since the last refill.
110 fn refill(&mut self, now: u64) {
111 let elapsed = now.saturating_sub(self.last_refill);
112 let batches = elapsed / self.refill_interval;
113 if batches == 0 {
114 return;
115 }
116 let added = batches.saturating_mul(self.refill_amount);
117 self.tokens = min(self.capacity, self.tokens.saturating_add(added));
118 self.last_refill = self
119 .last_refill
120 .saturating_add(batches.saturating_mul(self.refill_interval));
121 }
122
123 /// Returns the number of tokens available at `now`, after refilling.
124 pub fn available(&mut self, now: u64) -> u64 {
125 self.refill(now);
126 self.tokens
127 }
128
129 /// Tries to take `tokens` tokens at `now`. Returns `true` and consumes them
130 /// if enough are available, otherwise returns `false` and consumes nothing.
131 ///
132 /// A request for more than `capacity` tokens can never succeed.
133 pub fn try_acquire(&mut self, now: u64, tokens: u64) -> bool {
134 self.refill(now);
135 if self.tokens >= tokens {
136 self.tokens -= tokens;
137 true
138 } else {
139 false
140 }
141 }
142
143 /// Tries to take a single token at `now`.
144 pub fn try_acquire_one(&mut self, now: u64) -> bool {
145 self.try_acquire(now, 1)
146 }
147
148 /// Returns how long to wait, from `now`, until `tokens` tokens are available.
149 ///
150 /// Returns `Some(0)` if they are available now, and `None` if `tokens`
151 /// exceeds `capacity` (the bucket can never hold that many). Useful for a
152 /// `Retry-After` hint. This refills the bucket but does not consume anything.
153 pub fn retry_after(&mut self, now: u64, tokens: u64) -> Option<u64> {
154 if tokens > self.capacity {
155 return None;
156 }
157 self.refill(now);
158 if self.tokens >= tokens {
159 return Some(0);
160 }
161 let deficit = tokens - self.tokens;
162 let batches = deficit.div_ceil(self.refill_amount);
163 let into_interval = now.saturating_sub(self.last_refill);
164 let time_to_first = self.refill_interval.saturating_sub(into_interval);
165 Some(
166 time_to_first.saturating_add(
167 batches
168 .saturating_sub(1)
169 .saturating_mul(self.refill_interval),
170 ),
171 )
172 }
173}
174
175/// Convenience methods that read the current time from a
176/// [`Clock`](reliakit_core::Clock) instead of taking an explicit `now: u64`.
177///
178/// Available with the `core` feature. Each forwards to the matching `now`-taking
179/// method, which remains the primitive API.
180#[cfg(feature = "core")]
181impl RateLimiter {
182 /// Like [`available`](Self::available), reading the time from `clock`.
183 ///
184 /// ```
185 /// use reliakit_ratelimit::RateLimiter;
186 /// use reliakit_core::ManualClock;
187 ///
188 /// let clock = ManualClock::new(0);
189 /// let mut rl = RateLimiter::new(10, 1, 100);
190 /// assert!(rl.try_acquire_now(&clock, 10)); // drain the bucket
191 /// assert!(!rl.try_acquire_one_now(&clock)); // empty now
192 /// clock.set(100); // one refill interval later
193 /// assert!(rl.try_acquire_one_now(&clock)); // one token refilled
194 /// ```
195 pub fn available_now<C: reliakit_core::Clock>(&mut self, clock: &C) -> u64 {
196 self.available(clock.now())
197 }
198
199 /// Like [`try_acquire`](Self::try_acquire), reading the time from `clock`.
200 pub fn try_acquire_now<C: reliakit_core::Clock>(&mut self, clock: &C, tokens: u64) -> bool {
201 self.try_acquire(clock.now(), tokens)
202 }
203
204 /// Like [`try_acquire_one`](Self::try_acquire_one), reading the time from `clock`.
205 pub fn try_acquire_one_now<C: reliakit_core::Clock>(&mut self, clock: &C) -> bool {
206 self.try_acquire_one(clock.now())
207 }
208
209 /// Like [`retry_after`](Self::retry_after), reading the time from `clock`.
210 pub fn retry_after_now<C: reliakit_core::Clock>(
211 &mut self,
212 clock: &C,
213 tokens: u64,
214 ) -> Option<u64> {
215 self.retry_after(clock.now(), tokens)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn starts_full() {
225 let mut rl = RateLimiter::new(10, 1, 100);
226 assert_eq!(rl.available(0), 10);
227 assert_eq!(rl.capacity(), 10);
228 }
229
230 #[test]
231 fn acquire_consumes_tokens() {
232 let mut rl = RateLimiter::new(10, 1, 100);
233 assert!(rl.try_acquire(0, 4));
234 assert_eq!(rl.available(0), 6);
235 }
236
237 #[test]
238 fn burst_then_denied() {
239 let mut rl = RateLimiter::new(10, 1, 100);
240 for _ in 0..10 {
241 assert!(rl.try_acquire_one(0));
242 }
243 assert!(!rl.try_acquire_one(0));
244 }
245
246 #[test]
247 fn refills_over_time_capped_at_capacity() {
248 let mut rl = RateLimiter::new(10, 1, 100);
249 assert!(rl.try_acquire(0, 10));
250 assert_eq!(rl.available(0), 0);
251 assert_eq!(rl.available(250), 2); // two whole 100-unit intervals
252 assert_eq!(rl.available(10_000), 10); // capped at capacity, not 100
253 }
254
255 #[test]
256 fn partial_interval_does_not_refill_and_keeps_remainder() {
257 let mut rl = RateLimiter::new(10, 1, 100);
258 assert!(rl.try_acquire(0, 10));
259 assert_eq!(rl.available(50), 0); // 50 < 100, no token yet
260 assert_eq!(rl.available(100), 1); // the 50 remainder is preserved
261 }
262
263 #[test]
264 fn refill_amount_greater_than_one() {
265 let mut rl = RateLimiter::new(100, 5, 10);
266 assert!(rl.try_acquire(0, 100));
267 assert_eq!(rl.available(30), 15); // 3 intervals * 5
268 }
269
270 #[test]
271 fn acquire_more_than_capacity_always_fails() {
272 let mut rl = RateLimiter::new(5, 1, 100);
273 assert!(!rl.try_acquire(0, 6));
274 assert_eq!(rl.available(0), 5); // nothing consumed
275 }
276
277 #[test]
278 fn retry_after_zero_when_available() {
279 let mut rl = RateLimiter::new(10, 1, 100);
280 assert_eq!(rl.retry_after(0, 5), Some(0));
281 }
282
283 #[test]
284 fn retry_after_none_when_above_capacity() {
285 let mut rl = RateLimiter::new(10, 1, 100);
286 assert_eq!(rl.retry_after(0, 11), None);
287 }
288
289 #[test]
290 fn retry_after_full_interval_when_empty() {
291 let mut rl = RateLimiter::new(10, 1, 100);
292 assert!(rl.try_acquire(0, 10));
293 assert_eq!(rl.retry_after(0, 1), Some(100));
294 assert_eq!(rl.retry_after(0, 3), Some(300));
295 }
296
297 #[test]
298 fn retry_after_accounts_for_elapsed_partial_interval() {
299 let mut rl = RateLimiter::new(10, 1, 100);
300 assert!(rl.try_acquire(0, 10));
301 // 60 units into the first interval: one token arrives in 40 more.
302 assert_eq!(rl.retry_after(60, 1), Some(40));
303 assert_eq!(rl.retry_after(60, 2), Some(140));
304 }
305
306 #[test]
307 fn backwards_clock_does_not_panic_or_refill() {
308 let mut rl = RateLimiter::new(10, 1, 100);
309 assert!(rl.try_acquire(10_000, 10));
310 assert!(!rl.try_acquire(5_000, 1)); // earlier time: no refill, still empty
311 assert_eq!(rl.available(5_000), 0);
312 }
313
314 #[test]
315 fn zero_config_is_clamped() {
316 let rl = RateLimiter::new(0, 0, 0);
317 assert_eq!(rl.capacity(), 1);
318 assert_eq!(rl.refill_amount(), 1);
319 assert_eq!(rl.refill_interval(), 1);
320 }
321}
322
323#[cfg(all(test, feature = "core"))]
324mod core_tests {
325 use super::*;
326 use reliakit_core::ManualClock;
327
328 #[test]
329 fn now_methods_match_explicit_now() {
330 let clock = ManualClock::new(0);
331 let mut viaclock = RateLimiter::new(10, 2, 100);
332 let mut explicit = RateLimiter::new(10, 2, 100);
333
334 assert_eq!(viaclock.available_now(&clock), explicit.available(0));
335 assert_eq!(
336 viaclock.try_acquire_now(&clock, 7),
337 explicit.try_acquire(0, 7)
338 );
339 assert_eq!(
340 viaclock.retry_after_now(&clock, 5),
341 explicit.retry_after(0, 5)
342 );
343
344 clock.set(250);
345 assert_eq!(
346 viaclock.try_acquire_one_now(&clock),
347 explicit.try_acquire_one(250)
348 );
349 assert_eq!(viaclock.available_now(&clock), explicit.available(250));
350 }
351}