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