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}