Skip to main content

polyoxide_core/
capacity.rs

1//! [`CapacityBucket`]: a token bucket that holds a published burst, charges
2//! integer costs, and can be resized in place.
3
4use std::sync::{Arc, Mutex, MutexGuard};
5use std::time::Duration;
6
7use polyoxide_rt::Instant;
8use tokio::sync::Notify;
9
10use crate::hold::Hold;
11use crate::hooks::{LayerId, Refused};
12
13/// A token bucket for a venue that publishes a capacity and a refill rate,
14/// and charges each request a number of tokens.
15///
16/// It starts full, never holds more than `capacity`, and refills
17/// `refill_per_sec` tokens a second. [`acquire`](Self::acquire) waits until
18/// a request's tokens are there and takes them.
19///
20/// # Capacity is the published burst
21///
22/// This is the opposite of a window quota, and the two must not be made
23/// alike. A venue that publishes a bucket's capacity and rate implements a
24/// bucket, so holding the published capacity is a faithful model of it. A
25/// window quota publishes a count per window with no capacity term, and a
26/// bucket holding that count admits it twice in one window; see
27/// [`WindowQuotaTable`](crate::WindowQuotaTable).
28///
29/// # A cost it can never hold
30///
31/// A request costing more than `capacity` can never be satisfied, however
32/// long it waits. An exact cost is refused at once with [`Refused`], and
33/// nothing is sent. An inexact cost (a floor, such as a cancel-all whose
34/// cost the server counts) is charged at capacity and never refused: the
35/// server decides.
36///
37/// A bucket made [`provisional`](Self::provisional) is sized from a guess,
38/// before the venue has said what the account's capacity is. It never
39/// refuses: a cost above its capacity waits until [`confirm`](Self::confirm)
40/// or [`resize`](Self::resize), and is then charged or refused.
41///
42/// # The hold
43///
44/// [`acquire`](Self::acquire) waits out the bucket's [`Hold`] before
45/// charging, and again after any wait of its own, so a hold set while it
46/// waited for tokens is honoured. Every layer of one throttle shares one
47/// `Hold`, and [`resize`](Self::resize) keeps it.
48///
49/// The bucket keeps time on its hold's [`runtime`](Hold::runtime): its refill
50/// reads that clock and its waits for tokens sleep on it.
51///
52/// A clone shares the tokens and the hold.
53#[derive(Clone)]
54pub struct CapacityBucket {
55    inner: Arc<Inner>,
56}
57
58struct Inner {
59    layer: LayerId,
60    hold: Hold,
61    state: Mutex<State>,
62    /// Woken by `confirm` and `resize`, for costs waiting on a provisional
63    /// size.
64    sized: Notify,
65}
66
67struct State {
68    capacity: u32,
69    refill_per_sec: u32,
70    tokens: f64,
71    updated: Instant,
72    provisional: bool,
73}
74
75impl State {
76    /// Add what the refill has brought since the last update, up to capacity.
77    fn refill(&mut self, now: Instant) {
78        let elapsed = now.saturating_duration_since(self.updated).as_secs_f64();
79        self.tokens =
80            (self.tokens + elapsed * f64::from(self.refill_per_sec)).min(f64::from(self.capacity));
81        self.updated = now;
82    }
83}
84
85/// What one look at the bucket decided.
86enum Step {
87    Taken,
88    Refused(Refused),
89    WaitFor(Duration),
90    WaitForSize,
91}
92
93impl CapacityBucket {
94    /// A full bucket for `layer` holding at most `capacity` tokens and
95    /// refilling `refill_per_sec` a second, waiting out `hold`.
96    ///
97    /// # Panics
98    ///
99    /// When `refill_per_sec` is zero, since an emptied bucket would never
100    /// refill.
101    pub fn new(layer: LayerId, capacity: u32, refill_per_sec: u32, hold: Hold) -> Self {
102        Self::build(layer, capacity, refill_per_sec, hold, false)
103    }
104
105    /// As [`new`](Self::new), but sized provisionally: a cost above
106    /// `capacity` waits for [`confirm`](Self::confirm) or
107    /// [`resize`](Self::resize) instead of being refused.
108    ///
109    /// # Panics
110    ///
111    /// When `refill_per_sec` is zero.
112    pub fn provisional(layer: LayerId, capacity: u32, refill_per_sec: u32, hold: Hold) -> Self {
113        Self::build(layer, capacity, refill_per_sec, hold, true)
114    }
115
116    fn build(
117        layer: LayerId,
118        capacity: u32,
119        refill_per_sec: u32,
120        hold: Hold,
121        provisional: bool,
122    ) -> Self {
123        assert!(refill_per_sec > 0, "a bucket that never refills");
124        let updated = hold.runtime().now();
125        Self {
126            inner: Arc::new(Inner {
127                layer,
128                hold,
129                state: Mutex::new(State {
130                    capacity,
131                    refill_per_sec,
132                    tokens: f64::from(capacity),
133                    updated,
134                    provisional,
135                }),
136                sized: Notify::new(),
137            }),
138        }
139    }
140
141    /// The layer this bucket charges.
142    pub fn layer(&self) -> LayerId {
143        self.inner.layer
144    }
145
146    /// The most tokens the bucket holds.
147    pub fn capacity(&self) -> u32 {
148        self.lock().capacity
149    }
150
151    /// The hold this bucket waits out.
152    pub fn hold(&self) -> &Hold {
153        &self.inner.hold
154    }
155
156    /// Wait until `units` tokens are there, then take them.
157    ///
158    /// Waits out the hold first, and again after its own wait. An inexact
159    /// cost above capacity is charged at capacity.
160    ///
161    /// # Errors
162    ///
163    /// [`Refused`] at once, before any hold, when `exact` and `units` exceed
164    /// the capacity of a bucket that is not provisional. Nothing is taken.
165    pub async fn acquire(&self, units: u32, exact: bool) -> Result<(), Refused> {
166        // No wait can make room for it, so it is not made to wait out a hold.
167        if let Some(refused) = self.refuses(units, exact) {
168            return Err(refused);
169        }
170        self.inner.hold.wait().await;
171        loop {
172            // Made before the look, so a `confirm` between the look and the
173            // wait still wakes it.
174            let sized = self.inner.sized.notified();
175            match self.step(units, exact) {
176                Step::Taken => break,
177                Step::Refused(refused) => return Err(refused),
178                Step::WaitFor(wait) => self.inner.hold.runtime().sleep(wait).await,
179                Step::WaitForSize => sized.await,
180            }
181        }
182        // AD-23: a hold set while this call waited for tokens is honoured.
183        self.inner.hold.wait().await;
184        Ok(())
185    }
186
187    /// The refusal of an exact cost a confirmed bucket can never hold.
188    fn refuses(&self, units: u32, exact: bool) -> Option<Refused> {
189        let state = self.lock();
190        (exact && !state.provisional && units > state.capacity).then(|| Refused {
191            layer: self.inner.layer,
192            units,
193            capacity: state.capacity,
194        })
195    }
196
197    /// One look at the bucket: take the tokens, refuse, or say how long to
198    /// wait.
199    fn step(&self, units: u32, exact: bool) -> Step {
200        let mut state = self.lock();
201        state.refill(self.inner.hold.runtime().now());
202        let units = if units > state.capacity {
203            if state.provisional {
204                return Step::WaitForSize;
205            }
206            if exact {
207                return Step::Refused(Refused {
208                    layer: self.inner.layer,
209                    units,
210                    capacity: state.capacity,
211                });
212            }
213            state.capacity
214        } else {
215            units
216        };
217        let missing = f64::from(units) - state.tokens;
218        // A millionth of a token is rounding in the refill, not a wait: the
219        // timer could not sleep that short anyway.
220        if missing <= 1e-6 {
221            state.tokens = (state.tokens - f64::from(units)).max(0.0);
222            Step::Taken
223        } else {
224            Step::WaitFor(Duration::from_secs_f64(
225                missing / f64::from(state.refill_per_sec),
226            ))
227        }
228    }
229
230    /// Resize the bucket in place. The tokens it holds are kept, down to the
231    /// new capacity; the hold is kept; the sizing is confirmed.
232    ///
233    /// # Panics
234    ///
235    /// When `refill_per_sec` is zero.
236    pub fn resize(&self, capacity: u32, refill_per_sec: u32) {
237        assert!(refill_per_sec > 0, "a bucket that never refills");
238        {
239            let mut state = self.lock();
240            state.refill(self.inner.hold.runtime().now());
241            state.capacity = capacity;
242            state.refill_per_sec = refill_per_sec;
243            state.tokens = state.tokens.min(f64::from(capacity));
244            state.provisional = false;
245        }
246        self.inner.sized.notify_waiters();
247    }
248
249    /// Confirm a provisional size: a cost above capacity is refused from now
250    /// on, and those waiting are charged or refused.
251    pub fn confirm(&self) {
252        self.lock().provisional = false;
253        self.inner.sized.notify_waiters();
254    }
255
256    /// A poison-tolerant lock: a panic elsewhere must not make the bucket an
257    /// outage.
258    fn lock(&self) -> MutexGuard<'_, State> {
259        self.inner
260            .state
261            .lock()
262            .unwrap_or_else(|poisoned| poisoned.into_inner())
263    }
264}
265
266// `acquire` is `Send`, as every throttle's `acquire` has to be.
267const _: fn(&CapacityBucket) = |bucket| {
268    fn send<T: Send>(_: T) {}
269    send(bucket.acquire(1, true));
270};
271
272impl std::fmt::Debug for CapacityBucket {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        let state = self.lock();
275        f.debug_struct("CapacityBucket")
276            .field("layer", &self.inner.layer)
277            .field("capacity", &state.capacity)
278            .field("refill_per_sec", &state.refill_per_sec)
279            .field("provisional", &state.provisional)
280            .finish_non_exhaustive()
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use tokio::time::Instant;
287
288    use super::*;
289
290    const LAYER: LayerId = LayerId("test");
291
292    fn bucket(capacity: u32, refill_per_sec: u32) -> CapacityBucket {
293        CapacityBucket::new(LAYER, capacity, refill_per_sec, Hold::unbounded())
294    }
295
296    /// How long `bucket.acquire(units, true)` took, on the paused clock.
297    async fn timed(bucket: &CapacityBucket, units: u32) -> Duration {
298        let t = Instant::now();
299        bucket.acquire(units, true).await.unwrap();
300        t.elapsed()
301    }
302
303    #[tokio::test(start_paused = true)]
304    async fn it_starts_full() {
305        let bucket = bucket(10, 1);
306        assert_eq!(timed(&bucket, 10).await, Duration::ZERO);
307    }
308
309    #[tokio::test(start_paused = true)]
310    async fn it_never_holds_more_than_its_capacity() {
311        let bucket = bucket(5, 10);
312        tokio::time::sleep(Duration::from_secs(60)).await;
313        assert_eq!(timed(&bucket, 5).await, Duration::ZERO);
314        assert!(
315            timed(&bucket, 1).await >= Duration::from_millis(90),
316            "a minute idle filled the bucket past its capacity of 5"
317        );
318    }
319
320    #[tokio::test(start_paused = true)]
321    async fn it_paces_the_refill() {
322        let bucket = bucket(10, 10);
323        timed(&bucket, 10).await;
324        let waited = timed(&bucket, 5).await;
325        assert!(
326            (Duration::from_millis(500)..Duration::from_millis(510)).contains(&waited),
327            "5 tokens at 10 a second took {waited:?}"
328        );
329    }
330
331    #[tokio::test(start_paused = true)]
332    async fn an_exact_cost_above_capacity_is_refused_at_once() {
333        let bucket = bucket(10, 1);
334        let t = Instant::now();
335        assert_eq!(
336            bucket.acquire(11, true).await,
337            Err(Refused {
338                layer: LAYER,
339                units: 11,
340                capacity: 10
341            })
342        );
343        assert_eq!(t.elapsed(), Duration::ZERO);
344        assert_eq!(
345            timed(&bucket, 10).await,
346            Duration::ZERO,
347            "a refusal took tokens"
348        );
349    }
350
351    #[tokio::test(start_paused = true)]
352    async fn an_exact_cost_above_capacity_is_refused_without_waiting_out_a_hold() {
353        let bucket = bucket(10, 1);
354        bucket.hold().extend(Duration::from_secs(60));
355        let t = Instant::now();
356        assert_eq!(
357            bucket.acquire(11, true).await,
358            Err(Refused {
359                layer: LAYER,
360                units: 11,
361                capacity: 10
362            })
363        );
364        assert_eq!(
365            t.elapsed(),
366            Duration::ZERO,
367            "the refusal waited out the hold"
368        );
369    }
370
371    #[tokio::test(start_paused = true)]
372    async fn an_inexact_cost_above_capacity_is_charged_at_capacity() {
373        let bucket = bucket(10, 10);
374        let t = Instant::now();
375        bucket.acquire(50, false).await.unwrap();
376        assert_eq!(t.elapsed(), Duration::ZERO);
377        let waited = timed(&bucket, 1).await;
378        assert!(
379            (Duration::from_millis(100)..Duration::from_millis(110)).contains(&waited),
380            "the inexact cost did not drain exactly the capacity: {waited:?}"
381        );
382    }
383
384    #[tokio::test(start_paused = true)]
385    async fn a_provisional_bucket_waits_then_proceeds_after_a_resize() {
386        let bucket = CapacityBucket::provisional(LAYER, 10, 10, Hold::unbounded());
387        let waiting = tokio::spawn({
388            let bucket = bucket.clone();
389            async move { bucket.acquire(20, true).await }
390        });
391        tokio::time::sleep(Duration::from_secs(5)).await;
392        assert!(
393            !waiting.is_finished(),
394            "a provisional bucket refused or admitted 20 of 10"
395        );
396
397        bucket.resize(30, 10);
398        assert_eq!(waiting.await.unwrap(), Ok(()));
399    }
400
401    #[tokio::test(start_paused = true)]
402    async fn a_provisional_bucket_waits_then_refuses_after_confirm() {
403        let bucket = CapacityBucket::provisional(LAYER, 10, 10, Hold::unbounded());
404        let waiting = tokio::spawn({
405            let bucket = bucket.clone();
406            async move { bucket.acquire(20, true).await }
407        });
408        tokio::time::sleep(Duration::from_secs(5)).await;
409        assert!(!waiting.is_finished());
410
411        bucket.confirm();
412        assert_eq!(
413            waiting.await.unwrap(),
414            Err(Refused {
415                layer: LAYER,
416                units: 20,
417                capacity: 10
418            })
419        );
420    }
421
422    #[tokio::test(start_paused = true)]
423    async fn resize_keeps_the_tokens_clamped_and_the_hold() {
424        // Growing keeps the 6 tokens left, rather than filling to 100.
425        let grown = bucket(10, 1);
426        timed(&grown, 4).await;
427        grown.resize(100, 1);
428        assert_eq!(grown.capacity(), 100);
429        assert_eq!(timed(&grown, 6).await, Duration::ZERO);
430        assert!(timed(&grown, 1).await >= Duration::from_millis(990));
431
432        // Shrinking clamps them to the new capacity.
433        let shrunk = bucket(10, 1);
434        shrunk.resize(3, 1);
435        assert_eq!(timed(&shrunk, 3).await, Duration::ZERO);
436        assert!(timed(&shrunk, 1).await >= Duration::from_millis(990));
437
438        // The hold set before the resize still holds.
439        let held = bucket(10, 1);
440        held.hold().extend(Duration::from_secs(5));
441        held.resize(20, 2);
442        assert!(timed(&held, 1).await >= Duration::from_secs(5));
443    }
444
445    #[tokio::test(start_paused = true)]
446    async fn concurrent_acquirers_never_over_admit() {
447        // 5 tokens to start and 10 a second: the k-th of 25 single-token
448        // requests cannot go before (k - 5) / 10 seconds.
449        let bucket = bucket(5, 10);
450        let start = Instant::now();
451        let tasks: Vec<_> = (0..25)
452            .map(|_| {
453                let bucket = bucket.clone();
454                tokio::spawn(async move {
455                    bucket.acquire(1, true).await.unwrap();
456                    start.elapsed()
457                })
458            })
459            .collect();
460        let mut done = Vec::new();
461        for task in tasks {
462            done.push(task.await.unwrap());
463        }
464        done.sort();
465        for (k, at) in done.iter().enumerate() {
466            let earliest = Duration::from_millis(100) * (k as u32 + 1).saturating_sub(5);
467            assert!(
468                *at + Duration::from_micros(1) >= earliest,
469                "request {} went at {at:?}, before the {earliest:?} the bucket allows",
470                k + 1
471            );
472        }
473    }
474
475    #[tokio::test(start_paused = true)]
476    async fn a_hold_set_during_a_wait_for_tokens_is_honoured() {
477        let bucket = bucket(1, 1);
478        timed(&bucket, 1).await;
479        let holder = {
480            let hold = bucket.hold().clone();
481            tokio::spawn(async move {
482                tokio::time::sleep(Duration::from_millis(500)).await;
483                hold.extend(Duration::from_secs(5));
484            })
485        };
486        let waited = timed(&bucket, 1).await;
487        holder.await.unwrap();
488        assert!(
489            waited >= Duration::from_millis(5_500),
490            "the token came at 1s and the request went at {waited:?}, inside the hold"
491        );
492    }
493}