Skip to main content

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}