Skip to main content

postrust_proxy/ratelimit/
token_bucket.rs

1//! Token bucket algorithm for rate limiting.
2
3use parking_lot::Mutex;
4use std::time::Instant;
5
6/// Token bucket rate limiter.
7///
8/// Implements the token bucket algorithm where tokens are added at a fixed rate
9/// up to a maximum capacity. Each request consumes one token.
10///
11/// The rate is a fraction of a token per second, not a whole number. A limit
12/// of 3 requests a minute is 0.05 tokens a second; when the rate was carried
13/// as an integer that rounded to 0, the bucket never refilled, and a client
14/// that spent its burst was locked out until the process restarted.
15///
16/// The count and the time it was last brought up to date live under one lock.
17/// They used to be two atomics updated by separate compare-and-swaps, which
18/// let two concurrent requests both credit the same elapsed time.
19pub struct TokenBucket {
20    /// Maximum number of tokens (burst capacity)
21    capacity: f64,
22    /// Tokens added per second
23    refill_rate: f64,
24    state: Mutex<BucketState>,
25}
26
27struct BucketState {
28    tokens: f64,
29    last_refill: Instant,
30}
31
32impl TokenBucket {
33    /// Create a new token bucket, full.
34    ///
35    /// # Arguments
36    /// * `capacity` - Maximum tokens (burst size)
37    /// * `refill_rate` - Tokens added per second; may be fractional
38    pub fn new(capacity: u64, refill_rate: f64) -> Self {
39        let capacity = capacity as f64;
40        Self {
41            capacity,
42            // A negative or NaN rate would drain the bucket or poison the
43            // arithmetic; neither is a rate anyone meant.
44            refill_rate: if refill_rate.is_finite() && refill_rate > 0.0 {
45                refill_rate
46            } else {
47                0.0
48            },
49            state: Mutex::new(BucketState {
50                tokens: capacity,
51                last_refill: Instant::now(),
52            }),
53        }
54    }
55
56    /// Try to consume a token.
57    ///
58    /// Returns `true` if a token was available and consumed, `false` otherwise.
59    pub fn try_acquire(&self) -> bool {
60        self.try_acquire_n(1)
61    }
62
63    /// Try to consume N tokens.
64    ///
65    /// Returns `true` if N tokens were available and consumed, `false` otherwise.
66    pub fn try_acquire_n(&self, n: u64) -> bool {
67        self.try_acquire_n_at(n, Instant::now())
68    }
69
70    /// [`try_acquire_n`](Self::try_acquire_n) as of `now`, so tests can step
71    /// time rather than sleep through it.
72    pub(crate) fn try_acquire_n_at(&self, n: u64, now: Instant) -> bool {
73        let mut state = self.state.lock();
74        self.refill(&mut state, now);
75        let cost = n as f64;
76        if state.tokens >= cost {
77            state.tokens -= cost;
78            true
79        } else {
80            false
81        }
82    }
83
84    /// Get current available tokens (approximate).
85    pub fn available(&self) -> u64 {
86        let mut state = self.state.lock();
87        self.refill(&mut state, Instant::now());
88        state.tokens.floor() as u64
89    }
90
91    /// Whether the bucket has refilled completely.
92    ///
93    /// A full bucket is indistinguishable from a fresh one, so the limiter can
94    /// forget it without changing any future answer.
95    pub fn is_full(&self) -> bool {
96        self.is_full_at(Instant::now())
97    }
98
99    pub(crate) fn is_full_at(&self, now: Instant) -> bool {
100        let mut state = self.state.lock();
101        self.refill(&mut state, now);
102        state.tokens >= self.capacity
103    }
104
105    fn refill(&self, state: &mut BucketState, now: Instant) {
106        let elapsed = now.saturating_duration_since(state.last_refill);
107        state.tokens = (state.tokens + elapsed.as_secs_f64() * self.refill_rate).min(self.capacity);
108        // Only move forward: an `Instant` captured before the lock was taken
109        // can be older than the one another request has already stored.
110        if now > state.last_refill {
111            state.last_refill = now;
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use std::time::Duration;
120
121    #[test]
122    fn test_token_bucket_basic() {
123        let bucket = TokenBucket::new(10, 1.0); // 10 capacity, 1 token/sec
124
125        // Should be able to acquire up to capacity
126        for _ in 0..10 {
127            assert!(bucket.try_acquire());
128        }
129
130        // Should fail after exhausting tokens
131        assert!(!bucket.try_acquire());
132    }
133
134    #[test]
135    fn test_token_bucket_burst() {
136        let bucket = TokenBucket::new(5, 10.0); // 5 burst, 10 tokens/sec
137
138        // Burst of 5 should succeed
139        assert!(bucket.try_acquire_n(5));
140
141        // Next request should fail
142        assert!(!bucket.try_acquire());
143    }
144
145    #[test]
146    fn a_rate_below_one_per_second_still_refills() {
147        // 3 requests a minute. As an integer rate this was 3 / 60 = 0, and the
148        // bucket never refilled once the burst was spent.
149        let bucket = TokenBucket::new(3, 3.0 / 60.0);
150        // After the bucket's own clock starts, so the steps below are at
151        // least as long as they say.
152        let start = Instant::now();
153        for _ in 0..3 {
154            assert!(bucket.try_acquire_n_at(1, start));
155        }
156        assert!(!bucket.try_acquire_n_at(1, start));
157
158        // One token every 20 seconds.
159        assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(19)));
160        assert!(bucket.try_acquire_n_at(1, start + Duration::from_secs(20)));
161        assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(21)));
162
163        // And it refills to the burst, no further.
164        let later = start + Duration::from_secs(3600);
165        assert!(bucket.is_full_at(later));
166        for _ in 0..3 {
167            assert!(bucket.try_acquire_n_at(1, later));
168        }
169        assert!(!bucket.try_acquire_n_at(1, later));
170    }
171
172    #[test]
173    fn a_zero_or_nonsense_rate_does_not_refill_or_panic() {
174        let start = Instant::now();
175        for rate in [0.0, -1.0, f64::NAN] {
176            let bucket = TokenBucket::new(1, rate);
177            assert!(bucket.try_acquire_n_at(1, start));
178            assert!(!bucket.try_acquire_n_at(1, start + Duration::from_secs(3600)));
179        }
180    }
181}