Skip to main content

rama_utils/rate/
bucket.rs

1use super::{Acquire, Rate};
2
3/// A sans-IO [token bucket]: spend units at a bounded [`Rate`] with a
4/// configurable burst capacity.
5///
6/// The bucket never reads a clock: the caller provides `now` as
7/// monotonic nanoseconds from any epoch, as long as it is consistent
8/// across calls. This keeps the bucket `no_std`, deterministic to test
9/// and embeddable in other sans-IO state machines.
10///
11/// Internally the level is stored scaled by the period (in nanoseconds),
12/// so refills are exact integer arithmetic with zero drift.
13///
14/// [token bucket]: https://en.wikipedia.org/wiki/Token_bucket
15///
16/// # Examples
17///
18/// ```
19/// use rama_utils::rate::{Acquire, Rate, TokenBucket};
20///
21/// let mut bucket = TokenBucket::from_rate(Rate::per_sec(2));
22/// assert_eq!(bucket.try_acquire(0, 1), Acquire::Granted);
23/// assert_eq!(bucket.try_acquire(0, 1), Acquire::Granted);
24/// // bucket is empty: the next unit refills after half a period
25/// assert_eq!(bucket.try_acquire(0, 1), Acquire::RetryAt(500_000_000));
26/// assert_eq!(bucket.try_acquire(500_000_000, 1), Acquire::Granted);
27/// ```
28#[derive(Debug, Clone)]
29pub struct TokenBucket {
30    rate: Rate,
31    burst: u64,
32    /// current level, scaled: 1 unit == `rate.per_nanos()` scaled units
33    level_scaled: u128,
34    last_refill: u64,
35}
36
37impl TokenBucket {
38    /// Create a new [`TokenBucket`] with the given [`Rate`] and
39    /// burst capacity (the maximum units spendable at once).
40    ///
41    /// The bucket starts full.
42    ///
43    /// # Panics
44    ///
45    /// Panics if `burst` is zero.
46    #[must_use]
47    pub fn new(rate: Rate, burst: u64) -> Self {
48        assert!(burst > 0, "TokenBucket: burst must be non-zero");
49        Self {
50            rate,
51            burst,
52            level_scaled: Self::capacity_scaled_for(rate, burst),
53            last_refill: 0,
54        }
55    }
56
57    /// Create a new [`TokenBucket`] with a burst capacity of one
58    /// period worth of units (`rate.units()`): a plain smooth rate.
59    #[must_use]
60    pub fn from_rate(rate: Rate) -> Self {
61        Self::new(rate, rate.units())
62    }
63
64    /// The configured [`Rate`].
65    #[must_use]
66    pub const fn rate(&self) -> Rate {
67        self.rate
68    }
69
70    /// The configured burst capacity.
71    #[must_use]
72    pub const fn burst(&self) -> u64 {
73        self.burst
74    }
75
76    /// Try to spend `n` units at monotonic instant `now` (nanoseconds,
77    /// caller's clock).
78    ///
79    /// - [`Acquire::Granted`]: the units were spent.
80    /// - [`Acquire::RetryAt`]: nothing was spent; retrying with the
81    ///   returned instant (and an otherwise untouched bucket) is
82    ///   guaranteed to be granted.
83    /// - [`Acquire::Never`]: `n` exceeds the burst capacity and can
84    ///   never be granted as a single acquisition.
85    ///
86    /// `n == 0` is always granted. A `now` earlier than a previously
87    /// provided instant is treated as no time having passed.
88    pub fn try_acquire(&mut self, now: u64, n: u64) -> Acquire {
89        if n > self.burst {
90            return Acquire::Never;
91        }
92        self.refill(now);
93
94        let need = n as u128 * self.rate.per_nanos() as u128;
95        if self.level_scaled >= need {
96            self.level_scaled -= need;
97            return Acquire::Granted;
98        }
99
100        let deficit = need - self.level_scaled;
101        let wait_nanos = deficit.div_ceil(self.rate.units() as u128);
102        let retry_at = now.saturating_add(u64::try_from(wait_nanos).unwrap_or(u64::MAX));
103        Acquire::RetryAt(retry_at)
104    }
105
106    /// Give back up to `n` previously spent units, saturating at the
107    /// burst capacity.
108    ///
109    /// Intended for callers that reserve budget upfront and end up
110    /// using less (e.g. a short write).
111    pub fn refund(&mut self, n: u64) {
112        let scaled = n as u128 * self.rate.per_nanos() as u128;
113        self.level_scaled = self
114            .level_scaled
115            .saturating_add(scaled)
116            .min(self.capacity_scaled());
117    }
118
119    /// The number of whole units available at monotonic instant `now`
120    /// (nanoseconds, caller's clock).
121    pub fn available(&mut self, now: u64) -> u64 {
122        self.refill(now);
123        // level <= capacity_scaled, so this always fits u64
124        (self.level_scaled / self.rate.per_nanos() as u128) as u64
125    }
126
127    fn refill(&mut self, now: u64) {
128        let elapsed = now.saturating_sub(self.last_refill);
129        if elapsed == 0 {
130            return;
131        }
132        self.level_scaled = self
133            .level_scaled
134            .saturating_add(elapsed as u128 * self.rate.units() as u128)
135            .min(self.capacity_scaled());
136        self.last_refill = now;
137    }
138
139    fn capacity_scaled(&self) -> u128 {
140        Self::capacity_scaled_for(self.rate, self.burst)
141    }
142
143    fn capacity_scaled_for(rate: Rate, burst: u64) -> u128 {
144        burst as u128 * rate.per_nanos() as u128
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use core::time::Duration;
152
153    const SEC: u64 = 1_000_000_000;
154
155    #[test]
156    fn starts_full_and_drains() {
157        let mut bucket = TokenBucket::from_rate(Rate::per_sec(10));
158        assert_eq!(bucket.burst(), 10);
159        for _ in 0..10 {
160            assert_eq!(bucket.try_acquire(0, 1), Acquire::Granted);
161        }
162        assert_eq!(bucket.try_acquire(0, 1), Acquire::RetryAt(SEC / 10));
163    }
164
165    #[test]
166    fn retry_at_is_exact() {
167        let mut bucket = TokenBucket::from_rate(Rate::per_sec(10));
168        assert_eq!(bucket.try_acquire(0, 10), Acquire::Granted);
169
170        let at = match bucket.try_acquire(0, 3) {
171            Acquire::RetryAt(at) => at,
172            other => panic!("expected retry-at, got {other:?}"),
173        };
174        assert_eq!(at, 3 * SEC / 10);
175        // one nano earlier: still denied
176        assert!(matches!(
177            bucket.try_acquire(at - 1, 3),
178            Acquire::RetryAt(again) if again == at
179        ));
180        // at the advertised instant: granted
181        assert_eq!(bucket.try_acquire(at, 3), Acquire::Granted);
182    }
183
184    #[test]
185    fn burst_capacity_is_a_hard_cap() {
186        let mut bucket = TokenBucket::new(Rate::per_sec(10), 4);
187        assert_eq!(bucket.try_acquire(0, 5), Acquire::Never);
188        // even after any amount of time
189        assert_eq!(bucket.try_acquire(100 * SEC, 5), Acquire::Never);
190        assert_eq!(bucket.try_acquire(100 * SEC, 4), Acquire::Granted);
191    }
192
193    #[test]
194    fn zero_units_always_granted() {
195        let mut bucket = TokenBucket::new(Rate::per_sec(1), 1);
196        assert_eq!(bucket.try_acquire(0, 1), Acquire::Granted);
197        assert_eq!(bucket.try_acquire(0, 0), Acquire::Granted);
198    }
199
200    #[test]
201    fn available_tracks_partial_refill() {
202        let mut bucket = TokenBucket::from_rate(Rate::per_sec(10));
203        assert_eq!(bucket.available(0), 10);
204        assert_eq!(bucket.try_acquire(0, 10), Acquire::Granted);
205        assert_eq!(bucket.available(0), 0);
206        assert_eq!(bucket.available(550_000_000), 5);
207        assert_eq!(bucket.available(SEC), 10);
208        // never above burst
209        assert_eq!(bucket.available(100 * SEC), 10);
210    }
211
212    #[test]
213    fn refund_saturates_at_capacity() {
214        let mut bucket = TokenBucket::from_rate(Rate::per_sec(10));
215        assert_eq!(bucket.try_acquire(0, 3), Acquire::Granted);
216        assert_eq!(bucket.available(0), 7);
217        bucket.refund(2);
218        assert_eq!(bucket.available(0), 9);
219        bucket.refund(100);
220        assert_eq!(bucket.available(0), 10);
221    }
222
223    #[test]
224    fn fractional_refill_has_no_drift() {
225        // 3 units per second: one unit refills every 333_333_333.33.. ns;
226        // over 3 units the total must be exactly one second, not 3 * ceil.
227        let mut bucket = TokenBucket::from_rate(Rate::per_sec(3));
228        assert_eq!(bucket.try_acquire(0, 3), Acquire::Granted);
229
230        let mut now = 0;
231        for i in 1..=3 {
232            let at = match bucket.try_acquire(now, 1) {
233                Acquire::RetryAt(at) => at,
234                other => panic!("expected retry-at, got {other:?}"),
235            };
236            assert_eq!(at, (i * SEC as u128).div_ceil(3) as u64);
237            assert_eq!(bucket.try_acquire(at, 1), Acquire::Granted);
238            now = at;
239        }
240        // after spending 3 units the bucket refills the last one at exactly 1s ...
241        assert_eq!(now, SEC);
242        // ... with zero accumulated drift for the next round
243        assert_eq!(
244            bucket.try_acquire(now, 1),
245            Acquire::RetryAt(now + SEC / 3 + 1)
246        );
247    }
248
249    #[test]
250    fn time_regression_is_ignored() {
251        let mut bucket = TokenBucket::from_rate(Rate::per_sec(10));
252        assert_eq!(bucket.try_acquire(SEC, 10), Acquire::Granted);
253        // clock goes backwards: no refill, no panic
254        assert_eq!(bucket.available(0), 0);
255        assert!(matches!(bucket.try_acquire(0, 1), Acquire::RetryAt(_)));
256    }
257
258    #[test]
259    fn sub_second_period() {
260        let mut bucket = TokenBucket::from_rate(Rate::new(5, Duration::from_millis(100)));
261        assert_eq!(bucket.try_acquire(0, 5), Acquire::Granted);
262        assert_eq!(bucket.try_acquire(0, 5), Acquire::RetryAt(100_000_000));
263        assert_eq!(bucket.try_acquire(100_000_000, 5), Acquire::Granted);
264    }
265
266    #[test]
267    #[should_panic(expected = "burst must be non-zero")]
268    fn zero_burst_panics() {
269        assert_eq!(TokenBucket::new(Rate::per_sec(1), 0).burst(), 0);
270    }
271
272    mod properties {
273        use super::*;
274        use quickcheck::{Arbitrary, Gen, quickcheck};
275
276        #[derive(Debug, Clone, Copy)]
277        struct Op {
278            advance_nanos: u32,
279            n: u16,
280        }
281
282        impl Arbitrary for Op {
283            fn arbitrary(g: &mut Gen) -> Self {
284                Self {
285                    advance_nanos: u32::arbitrary(g),
286                    n: u16::arbitrary(g),
287                }
288            }
289        }
290
291        quickcheck! {
292            /// total grants over any run never exceed burst + rate * elapsed
293            fn never_exceeds_rate(units: u64, per_millis: u16, burst: u16, ops: Vec<Op>) -> bool {
294                let units = units % 1_000 + 1;
295                let per = Duration::from_millis(u64::from(per_millis) + 1);
296                let burst = u64::from(burst) + 1;
297                let rate = Rate::new(units, per);
298                let mut bucket = TokenBucket::new(rate, burst);
299
300                let mut now = 0u64;
301                let mut granted = 0u128;
302                for op in ops {
303                    now = now.saturating_add(u64::from(op.advance_nanos));
304                    let n = u64::from(op.n) % (burst + 1);
305                    if bucket.try_acquire(now, n) == Acquire::Granted {
306                        granted += u128::from(n);
307                    }
308                }
309
310                let budget_scaled =
311                    u128::from(burst) * u128::from(rate.per_nanos())
312                    + u128::from(now) * u128::from(units);
313                granted * u128::from(rate.per_nanos()) <= budget_scaled
314            }
315
316            /// a denied acquire is granted exactly at the advertised instant,
317            /// and denied one nanosecond before it
318            fn retry_at_is_tight(units: u64, per_millis: u16, burst: u16, drain: u16, n: u16) -> bool {
319                let units = units % 1_000 + 1;
320                let per = Duration::from_millis(u64::from(per_millis) + 1);
321                let burst = u64::from(burst) + 1;
322                let mut bucket = TokenBucket::new(Rate::new(units, per), burst);
323
324                let _ = bucket.try_acquire(0, u64::from(drain) % (burst + 1));
325                let n = u64::from(n) % burst + 1;
326                match bucket.try_acquire(0, n) {
327                    Acquire::Granted | Acquire::Never => true,
328                    Acquire::RetryAt(at) => {
329                        let denied_before = at == 0
330                            || matches!(bucket.clone().try_acquire(at - 1, n), Acquire::RetryAt(_));
331                        let granted_at = bucket.try_acquire(at, n) == Acquire::Granted;
332                        denied_before && granted_at
333                    }
334                }
335            }
336
337            /// available() never exceeds burst, and refund() cannot lift it above
338            fn available_bounded_by_burst(units: u64, burst: u16, now: u64, refund: u64) -> bool {
339                let units = units % 1_000 + 1;
340                let burst = u64::from(burst) + 1;
341                let mut bucket = TokenBucket::new(Rate::per_sec(units), burst);
342                bucket.refund(refund);
343                bucket.available(now) <= burst
344            }
345        }
346    }
347}