Skip to main content

rama_utils/rate/
limiter.rs

1use super::{Acquire, Rate, TokenBucket};
2
3use parking_lot::Mutex;
4use std::{
5    future::Future,
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9    time::Duration,
10};
11use tokio::sync::{Notify, futures::OwnedNotified};
12use tokio::time::Instant;
13
14/// A cheap-to-clone async handle around a shared [`TokenBucket`]:
15/// clones share the same budget.
16///
17/// Two ways to consume it:
18///
19/// - [`RateLimiter::acquire`] waits until the budget allows (pacing);
20/// - [`RateLimiter::try_acquire`] never waits and reports when to retry
21///   (rejecting, e.g. a 429 path).
22///
23/// Time is tracked against a per-limiter epoch taken at construction,
24/// using tokio's clock (so paused-time tests work as expected).
25///
26/// There is no FIFO fairness guarantee between concurrent waiters. Under a
27/// sustained stream of smaller acquisitions, a larger acquisition can be
28/// repeatedly beaten to newly refilled tokens and starve; callers that need
29/// strict waiter fairness must serialize acquisitions above this primitive.
30#[derive(Debug, Clone)]
31pub struct RateLimiter {
32    inner: Arc<Inner>,
33}
34
35/// A registered wait for the next [`RateLimiter::refund`].
36///
37/// Registration happens when this value is created, before it is first
38/// polled, so callers can safely create it before checking the bucket.
39#[derive(Debug)]
40#[must_use = "futures do nothing unless awaited or polled"]
41pub struct RefundWait(Pin<Box<OwnedNotified>>);
42
43impl RefundWait {
44    fn new(notify: Arc<Notify>) -> Self {
45        let mut notified = Box::pin(notify.notified_owned());
46        notified.as_mut().enable();
47        Self(notified)
48    }
49}
50
51impl Future for RefundWait {
52    type Output = ();
53
54    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
55        self.0.as_mut().poll(cx)
56    }
57}
58
59#[derive(Debug)]
60struct Inner {
61    bucket: Mutex<TokenBucket>,
62    epoch: Instant,
63    rate: Rate,
64    burst: u64,
65    refunds: Arc<Notify>,
66}
67
68impl RateLimiter {
69    /// Create a new [`RateLimiter`] with the given [`Rate`] and
70    /// burst capacity, starting full.
71    ///
72    /// # Panics
73    ///
74    /// Panics if `burst` is zero.
75    #[must_use]
76    pub fn new(rate: Rate, burst: u64) -> Self {
77        Self::from_bucket(TokenBucket::new(rate, burst))
78    }
79
80    /// Create a new [`RateLimiter`] with a burst capacity of one period
81    /// worth of units (`rate.units()`): a plain smooth rate.
82    #[must_use]
83    pub fn from_rate(rate: Rate) -> Self {
84        Self::from_bucket(TokenBucket::from_rate(rate))
85    }
86
87    fn from_bucket(bucket: TokenBucket) -> Self {
88        Self {
89            inner: Arc::new(Inner {
90                rate: bucket.rate(),
91                burst: bucket.burst(),
92                bucket: Mutex::new(bucket),
93                epoch: Instant::now(),
94                refunds: Arc::new(Notify::new()),
95            }),
96        }
97    }
98
99    /// The configured [`Rate`].
100    #[must_use]
101    pub fn rate(&self) -> Rate {
102        self.inner.rate
103    }
104
105    /// The configured burst capacity.
106    #[must_use]
107    pub fn burst(&self) -> u64 {
108        self.inner.burst
109    }
110
111    fn now_nanos(&self) -> u64 {
112        u64::try_from(
113            Instant::now()
114                .saturating_duration_since(self.inner.epoch)
115                .as_nanos(),
116        )
117        .unwrap_or(u64::MAX)
118    }
119
120    /// Try to spend `n` units without waiting.
121    ///
122    /// See [`TokenBucket::try_acquire`]; the [`Acquire::RetryAt`] instant
123    /// is in this limiter's clock and can be turned into a deadline
124    /// with [`RateLimiter::deadline`].
125    pub fn try_acquire(&self, n: u64) -> Acquire {
126        self.inner.bucket.lock().try_acquire(self.now_nanos(), n)
127    }
128
129    /// Spend `n` units, waiting until the budget allows it.
130    ///
131    /// Requests larger than the burst capacity are acquired in
132    /// burst-sized chunks, so this never fails: it paces.
133    /// Concurrent acquisitions are not queued fairly; see the type docs.
134    ///
135    /// Dropping the future while it waits for a chunk spends nothing for that
136    /// chunk. For a request larger than the burst, chunks already acquired
137    /// before cancellation stay spent. Refunding them could mint capacity when
138    /// concurrent users spent refill produced while those chunks were held.
139    pub async fn acquire(&self, n: u64) {
140        let mut remaining = n;
141        while remaining > 0 {
142            let want = remaining.min(self.inner.burst);
143            let refunded = self.inner.refunds.clone().notified_owned();
144            tokio::pin!(refunded);
145            refunded.as_mut().enable();
146            match self.try_acquire(want) {
147                Acquire::Granted => {
148                    remaining -= want;
149                }
150                Acquire::RetryAt(at) => {
151                    tokio::select! {
152                        () = tokio::time::sleep_until(self.deadline(at)) => {}
153                        () = refunded.as_mut() => {}
154                    }
155                }
156                Acquire::Never => {
157                    // defence-in-depth: chunks are clamped to the burst
158                    // capacity, so this cannot fire; bail rather than spin
159                    debug_assert!(false, "burst-clamped chunk reported Acquire::Never");
160                    break;
161                }
162            }
163        }
164    }
165
166    /// Give back up to `n` previously spent units, saturating at the
167    /// burst capacity.
168    pub fn refund(&self, n: u64) {
169        self.inner.bucket.lock().refund(n);
170        if n > 0 {
171            self.inner.refunds.notify_waiters();
172        }
173    }
174
175    /// Wait until this limiter is next refunded.
176    ///
177    /// Poll-based users should create this listener before calling
178    /// [`try_acquire`][Self::try_acquire], so a concurrent refund cannot land
179    /// between observing a deficit and registering for notification.
180    pub fn notified_on_refund(&self) -> RefundWait {
181        RefundWait::new(self.inner.refunds.clone())
182    }
183
184    /// Turn an [`Acquire::RetryAt`] instant into a timer deadline,
185    /// for poll-based callers.
186    #[must_use]
187    pub fn deadline(&self, retry_at_nanos: u64) -> Instant {
188        self.inner
189            .epoch
190            .checked_add(Duration::from_nanos(retry_at_nanos))
191            // saturated retry-at with an extreme rate config: far enough
192            .unwrap_or_else(|| {
193                Instant::now()
194                    .checked_add(Duration::from_hours(8_760))
195                    .unwrap_or_else(Instant::now)
196            })
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[tokio::test(start_paused = true)]
205    async fn acquire_paces_exactly() {
206        let limiter = RateLimiter::from_rate(Rate::per_sec(10));
207        limiter.acquire(10).await; // drain the initial burst
208
209        let start = Instant::now();
210        for i in 1..=5u64 {
211            limiter.acquire(1).await;
212            assert_eq!(start.elapsed(), Duration::from_millis(i * 100));
213        }
214    }
215
216    #[tokio::test(start_paused = true)]
217    async fn acquire_chunks_oversized_requests() {
218        let limiter = RateLimiter::new(Rate::per_sec(10), 10);
219
220        let start = Instant::now();
221        // 25 units at 10/s with a full 10-unit start:
222        // 10 now, 10 at +1s, 5 at +1.5s
223        limiter.acquire(25).await;
224        assert_eq!(start.elapsed(), Duration::from_millis(1_500));
225    }
226
227    #[tokio::test(start_paused = true)]
228    async fn try_acquire_never_waits() {
229        let limiter = RateLimiter::new(Rate::per_sec(2), 2);
230        assert_eq!(limiter.try_acquire(2), Acquire::Granted);
231
232        let retry_at = match limiter.try_acquire(1) {
233            Acquire::RetryAt(at) => at,
234            other => panic!("expected retry-at, got {other:?}"),
235        };
236        assert_eq!(
237            limiter.deadline(retry_at),
238            Instant::now() + Duration::from_millis(500)
239        );
240        assert_eq!(limiter.try_acquire(3), Acquire::Never);
241    }
242
243    #[tokio::test(start_paused = true)]
244    async fn clones_share_the_budget() {
245        let limiter = RateLimiter::new(Rate::per_sec(10), 2);
246        let clone = limiter.clone();
247
248        assert_eq!(limiter.rate(), Rate::per_sec(10));
249        assert_eq!(limiter.burst(), 2);
250
251        assert_eq!(limiter.try_acquire(1), Acquire::Granted);
252        assert_eq!(clone.try_acquire(1), Acquire::Granted);
253        assert!(matches!(clone.try_acquire(1), Acquire::RetryAt(_)));
254
255        limiter.refund(1);
256        assert_eq!(clone.try_acquire(1), Acquire::Granted);
257    }
258
259    #[tokio::test(start_paused = true)]
260    async fn a_refund_wakes_a_waiter_before_its_old_deadline() {
261        let limiter = RateLimiter::new(Rate::per_sec(10), 10);
262        limiter.acquire(10).await;
263
264        let waiting_limiter = limiter.clone();
265        let start = Instant::now();
266        let waiting = tokio::spawn(async move { waiting_limiter.acquire(10).await });
267        tokio::task::yield_now().await;
268
269        limiter.refund(10);
270        waiting.await.unwrap();
271        assert_eq!(start.elapsed(), Duration::ZERO);
272    }
273
274    #[tokio::test]
275    async fn refund_of_zero_does_not_wake_listeners() {
276        let limiter = RateLimiter::new(Rate::per_sec(10), 10);
277        let waiting_limiter = limiter.clone();
278        let waiting = tokio::spawn(async move { waiting_limiter.notified_on_refund().await });
279        tokio::task::yield_now().await;
280
281        limiter.refund(0);
282        tokio::task::yield_now().await;
283        assert!(!waiting.is_finished());
284
285        limiter.refund(1);
286        waiting.await.unwrap();
287    }
288
289    #[tokio::test(start_paused = true)]
290    async fn cancelling_oversized_acquire_does_not_mint_shared_budget() {
291        let limiter = RateLimiter::new(Rate::per_sec(10), 10);
292        limiter.acquire(10).await; // drain the initial burst
293
294        let acquire_limiter = limiter.clone();
295        let task = tokio::spawn(async move { acquire_limiter.acquire(20).await });
296        tokio::task::yield_now().await;
297
298        // The oversized acquire takes its first 10-unit chunk at +1s and is
299        // then waiting for the second. A concurrent user spends the five
300        // units that refill before +1.5s.
301        tokio::time::advance(Duration::from_secs(1)).await;
302        tokio::task::yield_now().await;
303        tokio::time::advance(Duration::from_millis(500)).await;
304        tokio::task::yield_now().await;
305        assert_eq!(limiter.try_acquire(5), Acquire::Granted);
306
307        task.abort();
308        let _cancelled = task.await;
309
310        // Refunding the first chunk here would restore a full burst even
311        // though the concurrent user already spent the intervening refill.
312        assert!(matches!(limiter.try_acquire(1), Acquire::RetryAt(_)));
313    }
314
315    #[tokio::test(start_paused = true)]
316    async fn acquire_within_burst_is_cancel_safe() {
317        let limiter = RateLimiter::new(Rate::per_sec(10), 10);
318        limiter.acquire(10).await; // drain the initial burst
319
320        // a single-chunk acquire only ever grants at the very end, so a
321        // cancel mid-wait spends nothing to begin with.
322        let cancelled = tokio::time::timeout(Duration::from_millis(500), limiter.acquire(10)).await;
323        cancelled.unwrap_err();
324        // one unit refilled after 100ms and none was consumed
325        assert_eq!(limiter.try_acquire(5), Acquire::Granted);
326    }
327
328    #[tokio::test(start_paused = true)]
329    async fn concurrent_waiters_all_proceed() {
330        let limiter = RateLimiter::from_rate(Rate::per_sec(10));
331        limiter.acquire(10).await;
332
333        let start = Instant::now();
334        let mut set = tokio::task::JoinSet::new();
335        for _ in 0..5 {
336            let limiter = limiter.clone();
337            set.spawn(async move { limiter.acquire(1).await });
338        }
339        while let Some(res) = set.join_next().await {
340            res.unwrap();
341        }
342        // 5 units at 10/s from an empty bucket
343        assert_eq!(start.elapsed(), Duration::from_millis(500));
344    }
345}