Skip to main content

netstack3_base/data_structures/
token_bucket.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use core::time::Duration;
6
7use crate::InstantContext;
8
9// TODO(https://github.com/rust-lang/rust/issues/57391): Replace this with Duration::SECOND.
10const SECOND: Duration = Duration::from_secs(1);
11
12/// Instead of actually storing the number of tokens, we store the number of
13/// fractions of `1 / TOKEN_MULTIPLIER`. If we stored the number of tokens, then
14/// under heavy load scenarios, the actual observed rate could be far off from
15/// the ideal rate due to integer rounding issues. Storing fractions instead
16/// limits the inaccuracy to at most `1 / TOKEN_MULTIPLIER` away from the ideal
17/// rate. See the comment in `try_take` for more details.
18///
19/// Note that the choice of 256 for `TOKEN_MULTIPLIER` provides us with good
20/// accuracy (only deviating from the ideal rate by 1/256) while still allowing
21/// for a maximum rate of 2^56 tokens per second.
22const TOKEN_MULTIPLIER: u64 = 256;
23
24/// A [token bucket] used for rate limiting.
25///
26/// `TokenBucket` implements rate limiting by "filling" a bucket with "tokens"
27/// at a constant rate, and allowing tokens to be consumed from the bucket until
28/// it is empty. This guarantees that a consumer may only maintain a rate of
29/// consumption faster than the rate of refilling for a bounded amount of time
30/// before they will catch up and find the bucket empty.
31///
32/// Note that the bucket has a maximum size beyond which no new tokens will be
33/// added. This prevents a long quiet period from building up a large backlog of
34/// tokens which can then be used in an intense and sustained burst.
35///
36/// This implementation does not require any background threads or timers to
37/// operate; it refills the bucket during calls to `try_take`, so no extra
38/// infrastructure is required to use it.
39///
40/// [token bucket]: https://en.wikipedia.org/wiki/Token_bucket
41#[derive(Debug)]
42pub struct TokenBucket<I> {
43    // The last time that the bucket was refilled, or `None` if the bucket has
44    // never been refilled.
45    last_refilled: Option<I>,
46    token_fractions: u64,
47    token_fractions_per_second: u64,
48}
49
50impl<I> TokenBucket<I> {
51    /// Constructs a new `TokenBucket` and initializes it with one second's
52    /// worth of tokens.
53    ///
54    /// # Panics
55    ///
56    /// `new` panics if `tokens_per_second` is greater than 2^56 - 1.
57    pub fn new(tokens_per_second: u64) -> TokenBucket<I> {
58        let token_fractions_per_second = tokens_per_second.checked_mul(TOKEN_MULTIPLIER).unwrap();
59        TokenBucket {
60            last_refilled: None,
61            // Initialize to 0 so that the first call to `try_take` will
62            // initialize the `last_refilled` time and fill the bucket. If we
63            // initialized this to a full bucket, then an immediate burst of
64            // calls to `try_take` would appear as though they'd happened over
65            // the course of a second, and the client would effectively get
66            // double the ideal rate until the second round of tokens expired.
67            token_fractions: 0,
68            token_fractions_per_second,
69        }
70    }
71}
72
73impl<I: crate::Instant> TokenBucket<I> {
74    /// Attempt to take a token from the bucket.
75    ///
76    /// `try_take` attempts to take a token from the bucket. If the bucket is
77    /// currently empty, then no token is available to be taken, and `try_take`
78    /// return false.
79    pub fn try_take<BC: InstantContext<Instant = I>>(&mut self, bindings_ctx: &BC) -> bool {
80        if self.token_fractions >= TOKEN_MULTIPLIER {
81            self.token_fractions -= TOKEN_MULTIPLIER;
82            return true;
83        }
84
85        // The algorithm implemented here is as follows: Whenever the bucket
86        // empties, refill it immediately. In order not to violate the
87        // requirement that tokens are added at a particular rate, we only add
88        // the number of tokens that "should have been" added since the last
89        // refill. We never add more than one second's worth of tokens at a time
90        // in order to guarantee that the bucket never has more than one
91        // second's worth of tokens in it.
92        //
93        // If tokens are being consumed at a rate slower than they are being
94        // added, then we will exhaust the bucket less often than once per
95        // second, and every refill will be a complete refill. If tokens are
96        // being consumed at a rate faster than they are being added, then the
97        // duration between refills will continuously decrease until every call
98        // to `try_take` adds 0 or t in [1, 2) tokens.
99        //
100        // Consider, for example, a production rate of 32 tokens per second and
101        // a consumption rate of 64 tokens per second:
102        // - First, there are 32 tokens in the bucket.
103        // - After 0.5 seconds, all 32 have been exhausted.
104        // - The call to `try_take` which exhausts the bucket refills the bucket
105        //   with 0.5 seconds' worth of tokens, or 16 tokens.
106        //
107        // This process repeats itself, halving the number of tokens added (and
108        // halving the amount of time to exhaust the bucket) until, after an
109        // amount of time which is linear in the rate of tokens being added, a
110        // call to `try_take` adds only 0 or t in [1, 2) tokens. In either case,
111        // the bucket is left with less than 1 token (if `try_take` adds >= 1
112        // token, it also consumes 1 token immediately).
113        //
114        // This has the potential downside of, under heavy load, executing a
115        // slightly more complex algorithm on every call to `try_take`, which
116        // includes querying for the current time. I (joshlf) speculate that
117        // this isn't an issue in practice, but it's worth calling out in case
118        // it becomes an issue in the future.
119
120        let now = bindings_ctx.now();
121        // The duration since the last refill, or 1 second, whichever is
122        // shorter. If this is the first fill, pretend that a full second has
123        // elapsed since the previous refill. In reality, there was no previous
124        // refill, which means it's fine to fill the bucket completely.
125        let dur_since_last_refilled = self.last_refilled.map_or(SECOND, |last_refilled| {
126            let dur = now.saturating_duration_since(last_refilled);
127            if dur > SECOND { SECOND } else { dur }
128        });
129
130        // Do math in u128 to avoid overflow. Be careful to multiply first and
131        // then divide to minimize integer division rounding error. The result
132        // of the calculation should always fit in a `u64` because the ratio
133        // `dur_since_last_refilled / SECOND` is guaranteed not to be greater
134        // than 1.
135        let added_token_fractions = u64::try_from(
136            (u128::from(self.token_fractions_per_second) * dur_since_last_refilled.as_nanos())
137                / SECOND.as_nanos(),
138        )
139        .unwrap();
140
141        // Only refill the bucket if we can add at least 1 token. This avoids
142        // two failure modes:
143        // - If we always blindly added however many token fractions are
144        //   available, then under heavy load, we might constantly add 0 token
145        //   fractions (because less time has elapsed since `last_refilled` than
146        //   is required to add a single token fraction) while still updating
147        //   `last_refilled` each time. This would drop the observed rate to 0
148        //   in the worst case.
149        // - If we always added >= 1 token fraction (as opposed to >= 1 full
150        //   token), then we would run into integer math inaccuracy issues. In
151        //   the worst case, `try_take` would be called after just less than the
152        //   amount of time required to add two token fractions. The actual
153        //   number of token fractions added would be rounded down to 1, and the
154        //   observed rate would be slightly more than 1/2 of the ideal rate.
155        //
156        // By always adding at least 1 token, we ensure that the worst case
157        // behavior is when `try_take` is called after just less than the amount
158        // of time required to add `TOKEN_MULTIPLIER + 1` token fractions has
159        // elapsed. In this case, the actual number of token fractions added is
160        // rounded down to 1, and the observed rate is within `1 /
161        // TOKEN_MULTIPLIER` of the ideal rate.
162        if let Some(new_token_fractions) =
163            (self.token_fractions + added_token_fractions).checked_sub(TOKEN_MULTIPLIER)
164        {
165            self.token_fractions = new_token_fractions;
166            self.last_refilled = Some(now);
167            true
168        } else {
169            return false;
170        }
171    }
172}
173
174#[cfg(test)]
175pub(crate) mod tests {
176    use super::*;
177
178    use crate::testutil::{FakeInstant, FakeInstantCtx};
179
180    impl<I: crate::Instant> TokenBucket<I> {
181        /// Call `try_take` `n` times, and assert that it succeeds every time.
182        fn assert_take_n<BC: InstantContext<Instant = I>>(&mut self, bindings_ctx: &BC, n: usize) {
183            for _ in 0..n {
184                assert!(self.try_take(bindings_ctx));
185            }
186        }
187    }
188
189    #[test]
190    fn test_token_bucket() {
191        /// Construct a `FakeInstantCtx` and a `TokenBucket` with a rate of 64
192        /// tokens per second, and pass them to `f`.
193        fn test<F: FnOnce(FakeInstantCtx, TokenBucket<FakeInstant>)>(f: F) {
194            f(FakeInstantCtx::default(), TokenBucket::new(64));
195        }
196
197        // Test that, if we consume all of the tokens in the bucket, but do not
198        // attempt to consume any more than that, the bucket will not be
199        // updated.
200        test(|mut ctx, mut bucket| {
201            let epoch = ctx.now();
202            assert!(bucket.try_take(&ctx));
203            assert_eq!(bucket.last_refilled.unwrap(), epoch);
204            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
205
206            // Sleep so that the current time will be different than the time at
207            // which the `last_refilled` time was initialized. That way, we can
208            // tell whether the `last_refilled` field was updated or not.
209            ctx.sleep(SECOND);
210            bucket.assert_take_n(&ctx, 63);
211            assert_eq!(bucket.last_refilled.unwrap(), epoch);
212            assert_eq!(bucket.token_fractions, 0);
213        });
214
215        // Test that, if we try to consume a token when the bucket is empty, it
216        // will get refilled.
217        test(|mut ctx, mut bucket| {
218            let epoch = ctx.now();
219            assert!(bucket.try_take(&ctx));
220            assert_eq!(bucket.last_refilled.unwrap(), epoch);
221            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
222
223            // Sleep for one second so that the bucket will be completely
224            // refilled.
225            ctx.sleep(SECOND);
226            bucket.assert_take_n(&ctx, 64);
227            assert_eq!(bucket.last_refilled.unwrap(), FakeInstant::from(SECOND));
228            // 1 token was consumed by the last call to `try_take`.
229            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
230        });
231
232        // Test that, if more than 1 second has elapsed since the previous
233        // refill, we still only fill with 1 second's worth of tokens.
234        test(|mut ctx, mut bucket| {
235            let epoch = ctx.now();
236            assert!(bucket.try_take(&ctx));
237            assert_eq!(bucket.last_refilled.unwrap(), epoch);
238            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
239
240            ctx.sleep(SECOND * 2);
241            bucket.assert_take_n(&ctx, 64);
242            assert_eq!(bucket.last_refilled.unwrap(), FakeInstant::from(SECOND * 2));
243            // 1 token was consumed by the last call to `try_take`.
244            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
245        });
246
247        // Test that, if we refill the bucket when less then a second has
248        // elapsed, a proportional amount of the bucket is refilled.
249        test(|mut ctx, mut bucket| {
250            let epoch = ctx.now();
251            assert!(bucket.try_take(&ctx));
252            assert_eq!(bucket.last_refilled.unwrap(), epoch);
253            assert_eq!(bucket.token_fractions, 63 * TOKEN_MULTIPLIER);
254
255            ctx.sleep(SECOND / 2);
256            bucket.assert_take_n(&ctx, 64);
257            assert_eq!(bucket.last_refilled.unwrap(), FakeInstant::from(SECOND / 2));
258            // Since only half a second had elapsed since the previous refill,
259            // only half of the tokens were refilled. 1 was consumed by the last
260            // call to `try_take`.
261            assert_eq!(bucket.token_fractions, 31 * TOKEN_MULTIPLIER);
262        });
263
264        // Test that, if we try to consume a token when the bucket is empty and
265        // not enough time has elapsed to allow for any tokens to be added,
266        // `try_take` will fail and the bucket will remain empty.
267        test(|mut ctx, mut bucket| {
268            // Allow 1/65 of a second to elapse so we know we're not just
269            // dealing with a consequence of no time having elapsed. The
270            // "correct" number of tokens to add after 1/65 of a second is
271            // 64/65, which will be rounded down to 0.
272            let epoch = ctx.now();
273            bucket.assert_take_n(&ctx, 64);
274            ctx.sleep(SECOND / 128);
275            assert!(!bucket.try_take(&ctx));
276            assert_eq!(bucket.last_refilled.unwrap(), epoch);
277            assert_eq!(bucket.token_fractions, 0);
278        });
279
280        // Test that, as long as we consume tokens at exactly the right rate, we
281        // never fail to consume a token.
282        test(|mut ctx, mut bucket| {
283            // Initialize the `last_refilled` time and then drain the bucket,
284            // leaving the `last_refilled` time at t=0 and the bucket empty.
285            bucket.assert_take_n(&ctx, 64);
286            for _ in 0..1_000 {
287                // `Duration`s store nanoseconds under the hood, and 64 divides
288                // 1e9 evenly, so this is lossless.
289                ctx.sleep(SECOND / 64);
290                assert!(bucket.try_take(&ctx));
291                assert_eq!(bucket.token_fractions, 0);
292                assert_eq!(bucket.last_refilled.unwrap(), ctx.now());
293            }
294        });
295
296        // Test that, if we consume tokens too quickly, we succeed in consuming
297        // tokens the correct proportion of the time.
298        //
299        // Test with rates close to 1 (2/1 through 5/4) and rates much larger
300        // than 1 (3/1 through 6/1).
301        for (numer, denom) in
302            [(2, 1), (3, 2), (4, 3), (5, 4), (3, 1), (4, 1), (5, 1), (6, 1)].iter()
303        {
304            test(|mut ctx, mut bucket| {
305                // Initialize the `last_refilled` time and then drain the
306                // bucket, leaving the `last_refilled` time at t=0 and the
307                // bucket empty.
308                bucket.assert_take_n(&ctx, 64);
309
310                const ATTEMPTS: u32 = 1_000;
311                let mut successes = 0;
312                for _ in 0..ATTEMPTS {
313                    // In order to speed up by a factor of numer/denom, we
314                    // multiply the duration between tries by its inverse,
315                    // denom/numer.
316                    ctx.sleep((SECOND * *denom) / (64 * *numer));
317                    if bucket.try_take(&ctx) {
318                        successes += 1;
319                        assert_eq!(bucket.last_refilled.unwrap(), ctx.now());
320                    }
321                }
322
323                // The observed rate can be up to 1/TOKEN_MULTIPLIER off in
324                // either direction.
325                let ideal_successes = (ATTEMPTS * denom) / numer;
326                let mult = u32::try_from(TOKEN_MULTIPLIER).unwrap();
327                assert!(successes <= (ideal_successes * (mult + 1)) / mult);
328                assert!(successes >= (ideal_successes * (mult - 1)) / mult);
329            });
330        }
331    }
332
333    #[test]
334    fn test_token_bucket_new() {
335        // Test that `new` doesn't panic if given 2^56 - 1.
336        let _: TokenBucket<()> = TokenBucket::<()>::new((1 << 56) - 1);
337    }
338
339    #[test]
340    #[should_panic]
341    fn test_token_bucket_new_panics() {
342        // Test that `new` panics if given 2^56
343        let _: TokenBucket<()> = TokenBucket::<()>::new(1 << 56);
344    }
345}
346
347#[cfg(any(test, benchmark))]
348pub(crate) mod benchmarks {
349    use super::*;
350
351    use crate::bench;
352    use crate::testutil::{Bencher, FakeInstantCtx};
353
354    fn bench_try_take<B: Bencher>(b: &mut B, enforced_rate: u64, try_rate: u32) {
355        let sleep = SECOND / try_rate;
356        let mut ctx = FakeInstantCtx::default();
357        let mut bucket = TokenBucket::new(enforced_rate);
358        b.iter(|| {
359            ctx.sleep(sleep);
360            let _: bool = B::black_box(bucket.try_take(B::black_box(&ctx)));
361        });
362    }
363
364    // These benchmarks measure the time taken to remove a token from the token
365    // bucket (using try_take) when tokens are being removed at various rates
366    // (relative to the rate at which they fill into the bucket).
367    // These benchmarks use the fastest possible `InstantContext`, and should be
368    // considered an upper bound on performance.
369
370    // Call `try_take` at 1/64 the enforced rate.
371    bench!(bench_try_take_slow, |b| bench_try_take(b, 64, 1));
372    // Call `try_take` at 1/2 the enforced rate.
373    bench!(bench_try_take_half_rate, |b| bench_try_take(b, 64, 32));
374    // Call `try_take` at the enforced rate.
375    bench!(bench_try_take_equal_rate, |b| bench_try_take(b, 64, 64));
376    // Call `try_take` at 65/64 the enforced rate.
377    bench!(bench_try_take_almost_equal_rate, |b| bench_try_take(b, 64, 65));
378    // Call `try_take` at 2x the enforced rate.
379    bench!(bench_try_take_double_rate, |b| bench_try_take(b, 64, 64 * 2));
380
381    #[cfg(benchmark)]
382    pub fn add_benches(
383        group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>,
384    ) {
385        let _ = group.bench_function("TokenBucket/TryTake/Slow", bench_try_take_slow);
386        let _ = group.bench_function("TokenBucket/TryTake/HalfRate", bench_try_take_half_rate);
387        let _ = group.bench_function("TokenBucket/TryTake/EqualRate", bench_try_take_equal_rate);
388        let _ = group.bench_function(
389            "TokenBucket/TryTake/AlmostEqualRate",
390            bench_try_take_almost_equal_rate,
391        );
392        let _ = group.bench_function("TokenBucket/TryTake/DoubleRate", bench_try_take_double_rate);
393    }
394}