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#![no_std]
39#![warn(missing_docs)]
40
41use core::cmp::min;
42
43/// A token-bucket rate limiter.
44///
45/// Construct one with [`RateLimiter::new`], then gate work with
46/// [`try_acquire`](Self::try_acquire) / [`try_acquire_one`](Self::try_acquire_one).
47/// The bucket starts full (a burst of up to `capacity` is allowed immediately).
48///
49/// Time is a plain `u64` in any monotonic unit you choose (commonly
50/// milliseconds); `refill_interval` uses that same unit. The limiter never reads
51/// the clock itself — pass `now` to each method.
52///
53/// `RateLimiter` is not internally synchronized. Share one across threads by
54/// wrapping it in your own `Mutex`/lock.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct RateLimiter {
57    capacity: u64,
58    refill_amount: u64,
59    refill_interval: u64,
60    tokens: u64,
61    last_refill: u64,
62}
63
64impl RateLimiter {
65    /// Creates a limiter that holds up to `capacity` tokens and adds
66    /// `refill_amount` tokens every `refill_interval` time units.
67    ///
68    /// The bucket starts full. `capacity`, `refill_amount`, and
69    /// `refill_interval` are each clamped to a minimum of `1` (a zero interval
70    /// would never refill and would divide by zero).
71    pub const fn new(capacity: u64, refill_amount: u64, refill_interval: u64) -> Self {
72        let capacity = if capacity == 0 { 1 } else { capacity };
73        Self {
74            capacity,
75            refill_amount: if refill_amount == 0 { 1 } else { refill_amount },
76            refill_interval: if refill_interval == 0 {
77                1
78            } else {
79                refill_interval
80            },
81            tokens: capacity,
82            last_refill: 0,
83        }
84    }
85
86    /// Returns the bucket capacity (the maximum burst).
87    pub const fn capacity(&self) -> u64 {
88        self.capacity
89    }
90
91    /// Returns how many tokens are added on each refill.
92    pub const fn refill_amount(&self) -> u64 {
93        self.refill_amount
94    }
95
96    /// Returns the refill interval, in the caller's time unit.
97    pub const fn refill_interval(&self) -> u64 {
98        self.refill_interval
99    }
100
101    /// Refills the bucket for any whole intervals elapsed since the last refill.
102    fn refill(&mut self, now: u64) {
103        let elapsed = now.saturating_sub(self.last_refill);
104        let batches = elapsed / self.refill_interval;
105        if batches == 0 {
106            return;
107        }
108        let added = batches.saturating_mul(self.refill_amount);
109        self.tokens = min(self.capacity, self.tokens.saturating_add(added));
110        self.last_refill = self
111            .last_refill
112            .saturating_add(batches.saturating_mul(self.refill_interval));
113    }
114
115    /// Returns the number of tokens available at `now`, after refilling.
116    pub fn available(&mut self, now: u64) -> u64 {
117        self.refill(now);
118        self.tokens
119    }
120
121    /// Tries to take `tokens` tokens at `now`. Returns `true` and consumes them
122    /// if enough are available, otherwise returns `false` and consumes nothing.
123    ///
124    /// A request for more than `capacity` tokens can never succeed.
125    pub fn try_acquire(&mut self, now: u64, tokens: u64) -> bool {
126        self.refill(now);
127        if self.tokens >= tokens {
128            self.tokens -= tokens;
129            true
130        } else {
131            false
132        }
133    }
134
135    /// Tries to take a single token at `now`.
136    pub fn try_acquire_one(&mut self, now: u64) -> bool {
137        self.try_acquire(now, 1)
138    }
139
140    /// Returns how long to wait, from `now`, until `tokens` tokens are available.
141    ///
142    /// Returns `Some(0)` if they are available now, and `None` if `tokens`
143    /// exceeds `capacity` (the bucket can never hold that many). Useful for a
144    /// `Retry-After` hint. This refills the bucket but does not consume anything.
145    pub fn retry_after(&mut self, now: u64, tokens: u64) -> Option<u64> {
146        if tokens > self.capacity {
147            return None;
148        }
149        self.refill(now);
150        if self.tokens >= tokens {
151            return Some(0);
152        }
153        let deficit = tokens - self.tokens;
154        let batches = deficit.div_ceil(self.refill_amount);
155        let into_interval = now.saturating_sub(self.last_refill);
156        let time_to_first = self.refill_interval.saturating_sub(into_interval);
157        Some(
158            time_to_first.saturating_add(
159                batches
160                    .saturating_sub(1)
161                    .saturating_mul(self.refill_interval),
162            ),
163        )
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn starts_full() {
173        let mut rl = RateLimiter::new(10, 1, 100);
174        assert_eq!(rl.available(0), 10);
175        assert_eq!(rl.capacity(), 10);
176    }
177
178    #[test]
179    fn acquire_consumes_tokens() {
180        let mut rl = RateLimiter::new(10, 1, 100);
181        assert!(rl.try_acquire(0, 4));
182        assert_eq!(rl.available(0), 6);
183    }
184
185    #[test]
186    fn burst_then_denied() {
187        let mut rl = RateLimiter::new(10, 1, 100);
188        for _ in 0..10 {
189            assert!(rl.try_acquire_one(0));
190        }
191        assert!(!rl.try_acquire_one(0));
192    }
193
194    #[test]
195    fn refills_over_time_capped_at_capacity() {
196        let mut rl = RateLimiter::new(10, 1, 100);
197        assert!(rl.try_acquire(0, 10));
198        assert_eq!(rl.available(0), 0);
199        assert_eq!(rl.available(250), 2); // two whole 100-unit intervals
200        assert_eq!(rl.available(10_000), 10); // capped at capacity, not 100
201    }
202
203    #[test]
204    fn partial_interval_does_not_refill_and_keeps_remainder() {
205        let mut rl = RateLimiter::new(10, 1, 100);
206        assert!(rl.try_acquire(0, 10));
207        assert_eq!(rl.available(50), 0); // 50 < 100, no token yet
208        assert_eq!(rl.available(100), 1); // the 50 remainder is preserved
209    }
210
211    #[test]
212    fn refill_amount_greater_than_one() {
213        let mut rl = RateLimiter::new(100, 5, 10);
214        assert!(rl.try_acquire(0, 100));
215        assert_eq!(rl.available(30), 15); // 3 intervals * 5
216    }
217
218    #[test]
219    fn acquire_more_than_capacity_always_fails() {
220        let mut rl = RateLimiter::new(5, 1, 100);
221        assert!(!rl.try_acquire(0, 6));
222        assert_eq!(rl.available(0), 5); // nothing consumed
223    }
224
225    #[test]
226    fn retry_after_zero_when_available() {
227        let mut rl = RateLimiter::new(10, 1, 100);
228        assert_eq!(rl.retry_after(0, 5), Some(0));
229    }
230
231    #[test]
232    fn retry_after_none_when_above_capacity() {
233        let mut rl = RateLimiter::new(10, 1, 100);
234        assert_eq!(rl.retry_after(0, 11), None);
235    }
236
237    #[test]
238    fn retry_after_full_interval_when_empty() {
239        let mut rl = RateLimiter::new(10, 1, 100);
240        assert!(rl.try_acquire(0, 10));
241        assert_eq!(rl.retry_after(0, 1), Some(100));
242        assert_eq!(rl.retry_after(0, 3), Some(300));
243    }
244
245    #[test]
246    fn retry_after_accounts_for_elapsed_partial_interval() {
247        let mut rl = RateLimiter::new(10, 1, 100);
248        assert!(rl.try_acquire(0, 10));
249        // 60 units into the first interval: one token arrives in 40 more.
250        assert_eq!(rl.retry_after(60, 1), Some(40));
251        assert_eq!(rl.retry_after(60, 2), Some(140));
252    }
253
254    #[test]
255    fn backwards_clock_does_not_panic_or_refill() {
256        let mut rl = RateLimiter::new(10, 1, 100);
257        assert!(rl.try_acquire(10_000, 10));
258        assert!(!rl.try_acquire(5_000, 1)); // earlier time: no refill, still empty
259        assert_eq!(rl.available(5_000), 0);
260    }
261
262    #[test]
263    fn zero_config_is_clamped() {
264        let rl = RateLimiter::new(0, 0, 0);
265        assert_eq!(rl.capacity(), 1);
266        assert_eq!(rl.refill_amount(), 1);
267        assert_eq!(rl.refill_interval(), 1);
268    }
269}