Skip to main content

moqtap_proxy/shape/
bucket.rs

1//! The token bucket — configuration, state, and the one pure function
2//! that decides whether a unit may go now.
3//!
4//! [`charge`] takes `now` as a parameter and reads no clock, following
5//! `release_timer::plan(now, next, slice)`. That is the whole reason the
6//! exact rate claim is provable: the arithmetic is a function of
7//! fabricated `Instant`s, so `the_bucket_never_grants_above_rate_plus_burst`
8//! is a +-0 % assertion over ten thousand steps rather than a measurement
9//! that machine load can move.
10//!
11//! # Units
12//!
13//! `rate_bps` is **bytes per second**, paired with `burst_bytes` and with
14//! the `bytes` argument to [`charge`]. Everything the scheduler counts is a
15//! byte count, and mixing a bit rate into a byte accounting would put a
16//! factor of eight between the configuration and every statistic that
17//! reports against it. A bit rate is `rate_bps * 8`.
18//!
19//! # Why the arithmetic is scaled
20//!
21//! Tokens are held in *nano-bytes* (`u128`), one byte being a thousand
22//! million nano-bytes, and refills are computed from elapsed nanoseconds.
23//! An unscaled byte counter would round every sub-byte refill to zero and
24//! a bucket paced faster than one byte per nanosecond would drift low
25//! without any test noticing; `u128` removes the overflow question
26//! entirely rather than documenting a ceiling nobody would check.
27
28use std::time::{Duration, Instant};
29
30/// Nano-bytes per byte, and equally nanoseconds per second — the two are
31/// the same constant because a bucket accumulating `rate` bytes per second
32/// accumulates exactly `rate` nano-bytes per nanosecond.
33const NANO: u128 = 1_000_000_000;
34
35/// A named token bucket. One per class, or shared by several.
36///
37/// `#[non_exhaustive]` *with* a [`Default`], exactly as
38/// [`EgressConfig`](crate::action::EgressConfig) is. Without the `Default`
39/// an integration-test crate could not construct one at all, because
40/// struct-expression and functional-update syntax are both illegal outside
41/// the defining crate — and note that `..BucketConfig::default()` is one of
42/// the two illegal forms (`E0639`), so an outside caller assigns per field
43/// on a `::default()` binding. See the module doc on
44/// [`shape`](crate::shape) for the full statement. The default is an
45/// unnamed, unlimited bucket.
46///
47/// # The written form requires `name` and `burst_bytes`
48///
49/// The two rate fields default to absent, which means unlimited and is a
50/// sensible thing to leave out. The other two are required, and
51/// `burst_bytes` is the interesting one: its Rust default is `0`, a depth
52/// that can cover no object at all, and a bucket that inherits it delivers
53/// every unit at its `max_hold` clamp at a throughput bearing no relation to
54/// the rate beside it. That failure is quiet — the run looks rate-limited,
55/// because it is, just not by the number in the file — so the written form
56/// makes the author state the depth rather than inherit the one value that
57/// cannot work. See [`BucketConfig::burst_bytes`].
58#[derive(Debug, Clone, PartialEq, Eq, Default)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
61#[non_exhaustive]
62pub struct BucketConfig {
63    /// The name a [`ClassRule`](super::ClassRule) refers to. Must be unique
64    /// across the profile's buckets, and a class naming a bucket that is
65    /// not present is rejected by
66    /// [`ShapeProfile::try_new`](super::ShapeProfile::try_new).
67    pub name: String,
68    /// Sustained rate in **bytes per second**.
69    ///
70    /// `None` means unlimited — an explicitly unshaped class, which is not
71    /// the same as no class: it still has a name, its own statistics, and
72    /// its own place in the discipline. [`charge`] answers
73    /// [`Grant::Now`] for every unit, whatever its size, and touches no
74    /// state.
75    ///
76    /// `Some(0)` is **legal and distinct**: the bucket never refills, so
77    /// after the initial `burst_bytes` are spent no unit is ever grantable
78    /// from tokens. [`charge`] answers [`Grant::Never`] rather than a
79    /// refill instant, because there is no refill instant to compute and
80    /// arming a release timer at an unreachable one would arm a timer that
81    /// never fires. The caller supplies the deadline instead — the unit's
82    /// `max_hold` clamp — so under the default
83    /// [`Expiry::Deliver`](super::Expiry) a 0-bps class **does** deliver,
84    /// at `max_hold`, clamped, reporting `Impairment{HoldClamped}`.
85    /// A zero rate is checked **before** the burst, so a deliberately stopped
86    /// class answers [`Grant::Never`] and never [`Grant::LargerThanBurst`]. The
87    /// distinction is the whole value of the second variant: *I asked for no
88    /// traffic* is a configuration working, and *I asked for a rate and my
89    /// burst cannot cover one object* is a configuration that silently is not.
90    /// The consequence is binding on every fixture: **a 0-bps class delivers
91    /// zero bytes* is a statement about a sampling window, not a property.* A
92    /// test that relies on starvation must pin
93    /// [`QueueConfig::max_hold`](super::QueueConfig::max_hold) explicitly so
94    /// the margin between the assertion and the delivery is visible in the
95    /// fixture rather than inherited from a default the test never names.
96    #[cfg_attr(feature = "serde", serde(default))]
97    pub rate_bps: Option<u64>,
98    /// Bytes the bucket may accumulate while idle, and therefore the
99    /// largest unit it can ever grant from tokens: a unit larger than
100    /// `burst_bytes` can never be covered, however long the caller waits.
101    ///
102    /// **Set this to at least one object.** The default is `0`, which
103    /// cannot cover anything, and the failure it produces is quiet: with no
104    /// unit ever grantable, every object leaves at its `max_hold` clamp
105    /// instead of at `rate_bps`, so the measured throughput is
106    /// `depth / max_hold` and bears no relation to the rate that was
107    /// written down. Measured, at `rate_bps: Some(1_000_000)` with
108    /// `burst_bytes: 100` and 1000-byte objects: delivery landed on the
109    /// clamp exactly, at a rate the configuration never names.
110    ///
111    /// [`charge`] answers [`Grant::LargerThanBurst`] for that case rather
112    /// than folding it into [`Grant::Never`], so the caller can report it as
113    /// the misconfiguration it is instead of as the ordinary rate limiting
114    /// it is indistinguishable from. It cannot be rejected when the profile
115    /// is built: [`ShapeProfile::try_new`](super::ShapeProfile::try_new) has
116    /// the burst but not the object sizes, and the sizes are what decide.
117    pub burst_bytes: u64,
118    /// Optional ceiling for borrowing above `rate_bps`.
119    ///
120    /// **Reserved.** The scheduler accepts this field and never borrows, with no
121    /// diagnostic — the frozen `ShapeError` has no variant it could land
122    /// in. A profile setting `ceil_bps` above `rate_bps` measures a flat
123    /// `rate_bps`; this sentence is the whole of the warning.
124    #[cfg_attr(feature = "serde", serde(default))]
125    pub ceil_bps: Option<u64>,
126}
127
128/// The mutable half of a token bucket: what it holds and when it was last
129/// refilled.
130///
131/// Session-scoped and shared by every stream of a class, so it is the
132/// scheduler's state and not a stream's. Kept separate from
133/// [`BucketConfig`] so the configuration stays comparable and cloneable
134/// while the state stays exactly one owner's.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct BucketState {
137    /// Held tokens in nano-bytes. Never exceeds `burst_bytes * NANO` and
138    /// never goes negative — the type forbids it, and so does the rate
139    /// bound the bucket exists to hold.
140    tokens: u128,
141    /// The instant `tokens` was last brought up to date.
142    last: Instant,
143}
144
145impl BucketState {
146    /// A bucket that starts full at `burst_bytes`, as of `now`.
147    ///
148    /// Starting full is what makes the first unit of a session go
149    /// immediately; starting empty would put a `burst_bytes / rate_bps`
150    /// delay in front of every stream and read as a broken proxy.
151    pub fn new(burst_bytes: u64, now: Instant) -> Self {
152        Self { tokens: u128::from(burst_bytes) * NANO, last: now }
153    }
154
155    /// Whole bytes currently held, as of the last [`charge`].
156    ///
157    /// Does not refill: this is a read of recorded state, not a clock
158    /// reading, so it stays usable from a test that fabricates its own
159    /// instants.
160    pub fn available_bytes(&self) -> u64 {
161        u64::try_from(self.tokens / NANO).unwrap_or(u64::MAX)
162    }
163}
164
165/// What [`charge`] decided about one unit.
166///
167/// `#[non_exhaustive]` with no `Default`: there is no safe default answer.
168/// Defaulting to `Now` would silently unshape a bucket; defaulting to
169/// `Never` would silently stall one.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171#[non_exhaustive]
172pub enum Grant {
173    /// The unit may go now. The tokens have already been debited.
174    Now,
175    /// Not now. The bucket holds enough at this instant and not before, so
176    /// this is the deadline to arm.
177    ///
178    /// The instant is *earliest*, not *exact*: another stream sharing the
179    /// bucket may drain it again before this one wakes, in which case the
180    /// next [`charge`] answers `Later` again with a new deadline.
181    Later(Instant),
182    /// Not now, and no refill instant exists, because the rate is `Some(0)`
183    /// and the bucket therefore never refills.
184    ///
185    /// The caller must supply its own deadline, which is the unit's
186    /// `max_hold` clamp. Returning a fabricated far-future instant instead
187    /// was rejected: it would arm a release timer that never fires, and
188    /// nothing downstream could tell that deadline from a real one.
189    Never,
190    /// Not now, and not ever from tokens: the unit is larger than the whole
191    /// bucket, so no amount of refilling reaches it.
192    ///
193    /// **A separate answer from [`Self::Never`], and the separation is the
194    /// point.** Both leave the caller to fall back on the `max_hold` clamp,
195    /// so the two are identical in what they *do*; they are opposite in what
196    /// they mean. `Never` is a rate of zero doing exactly what it was asked
197    /// to. This is a rate that was asked for and cannot be delivered, because
198    /// the bucket cannot hold one object's worth of tokens — every unit
199    /// leaves at the clamp and the configured rate never binds at all. Folded
200    /// into one variant, the second is invisible: it produces the same clamp,
201    /// the same `tokens_exhausted_episodes` and the same `HoldClamped` report
202    /// a working rate limit produces.
203    ///
204    /// Reachable only with a non-zero rate — a zero rate is answered first —
205    /// so a caller reporting this is always reporting a burst that is too
206    /// small and never a class that was configured to stop.
207    LargerThanBurst {
208        /// The bucket's cap, which is [`BucketConfig::burst_bytes`].
209        burst_bytes: u64,
210        /// The unit that did not fit, so the report can quote both numbers
211        /// rather than leaving the reader to find one of them.
212        unit_bytes: u64,
213    },
214}
215
216/// Charge `bytes` against a bucket and say whether the unit may go.
217///
218/// **Pure with respect to the clock** — `now` is a parameter and no clock
219/// is read, following `release_timer::plan`. `state` is refilled to `now`
220/// before the decision and debited only on [`Grant::Now`]; every refusal
221/// costs nothing, so re-charging the same unit after its deadline is correct
222/// rather than double-billing.
223///
224/// The bound **this function** upholds, exactly: over any interval, the
225/// bytes it grants never exceed `rate_bps * elapsed + burst_bytes`. That is
226/// what makes the rate claim provable arithmetic rather than a measurement,
227/// and `the_bucket_never_grants_above_rate_plus_burst` proves it over
228/// 10 001 fabricated steps.
229///
230/// # It is not the bound the *shaper* upholds
231///
232/// Stated here because the difference is a factor of thousands and a reader
233/// of this line will otherwise assume the wrong one. `PendingQueue`'s
234/// release seam checks the unit's `max_hold` clamp **before** it calls
235/// `acquire`, and under the default [`Expiry::Deliver`](super::Expiry) a
236/// clamped unit goes out without a bucket being consulted at all — that is
237/// what "a 0-bps class still delivers, at `max_hold`" means. So the
238/// end-to-end ceiling is
239///
240/// ```text
241/// min(rate_bps * elapsed + burst_bytes,   <- this function
242///     depth / max_hold * elapsed)         <- the clamp, per stream
243/// ```
244///
245/// Measured, with `rate_bps: Some(0)`, `depth_bytes: 4096` and `max_hold:
246/// 300 ms`: a bucket configured at **0 bytes/s** sustained ~25 kB/s. The
247/// clamp is the deliberate non-destructive default — a starved unit is
248/// delivered late rather than dropped — and not a defect, but a fixture that
249/// means to observe *this* function's bound has to pin
250/// `max_hold` far enough out that the clamp cannot bind inside its sampling
251/// window — which is why every starvation fixture is required to name it.
252///
253/// `now` is expected to be monotonic. A `now` earlier than the last one
254/// refills nothing and does not rewind the bucket's clock, so an
255/// out-of-order caller under-grants rather than manufacturing tokens.
256pub fn charge(
257    state: &mut BucketState,
258    rate_bps: Option<u64>,
259    burst_bytes: u64,
260    bytes: u64,
261    now: Instant,
262) -> Grant {
263    // Unlimited: no accounting at all, so an unshaped class costs nothing
264    // per unit and arms no deadline. `None` is emphatically not `Some(0)`.
265    let Some(rate) = rate_bps else {
266        return Grant::Now;
267    };
268
269    let cap = u128::from(burst_bytes) * NANO;
270    if now > state.last {
271        let elapsed = (now - state.last).as_nanos();
272        state.tokens = (state.tokens + elapsed * u128::from(rate)).min(cap);
273        state.last = now;
274    } else {
275        // Still clamp: `burst_bytes` may have shrunk under a reconfigure.
276        state.tokens = state.tokens.min(cap);
277    }
278
279    let need = u128::from(bytes) * NANO;
280    if state.tokens >= need {
281        state.tokens -= need;
282        return Grant::Now;
283    }
284
285    // Two unreachable answers, deliberately not one. A zero rate is asked
286    // for first, so a class configured to stop is never reported as a class
287    // whose burst is mis-sized; what is left is a rate the caller does want
288    // and a bucket that cannot hold one unit of it, which is the case that
289    // has no other symptom.
290    if rate == 0 {
291        return Grant::Never;
292    }
293    if need > cap {
294        return Grant::LargerThanBurst { burst_bytes, unit_bytes: bytes };
295    }
296
297    let deficit = need - state.tokens;
298    let wait_nanos = deficit.div_ceil(u128::from(rate));
299    let wait = u64::try_from(wait_nanos).map(Duration::from_nanos);
300    match wait.ok().and_then(|d| state.last.checked_add(d)) {
301        Some(at) => Grant::Later(at),
302        // A wait that does not fit in a `Duration`, or an `Instant` past
303        // the platform's representable range, is not a deadline anyone can
304        // arm. Say so rather than saturate into a lie.
305        None => Grant::Never,
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    /// The exact rate bound, over fabricated instants.
314    ///
315    /// Demand is twice the rate, so the bucket — not the offered load — is
316    /// what limits the total. With demand at or below the rate the bucket
317    /// never binds and the assertion would hold however wrong the
318    /// arithmetic was.
319    #[test]
320    fn the_bucket_never_grants_above_rate_plus_burst() {
321        const RATE: u64 = 1_000_000; // bytes/s
322        const BURST: u64 = 10_000; // bytes
323        const STEPS: u64 = 10_000;
324        const STEP: Duration = Duration::from_micros(100); // 100 bytes of refill
325        const UNIT: u64 = 200; // 2x the rate
326
327        let base = Instant::now();
328        let mut state = BucketState::new(BURST, base);
329        let mut granted: u128 = 0;
330        let mut grants = 0u64;
331
332        // `0..=STEPS`: the first charge happens at `base`, before any
333        // refill, so the full bucket is never clamped away and the ceiling
334        // below is reached exactly rather than approached.
335        for i in 0..=STEPS {
336            let now = base + STEP * u32::try_from(i).expect("step index fits u32");
337            match charge(&mut state, Some(RATE), BURST, UNIT, now) {
338                Grant::Now => {
339                    granted += u128::from(UNIT);
340                    grants += 1;
341                }
342                Grant::Later(at) => assert!(at > now, "a Later deadline must be in the future"),
343                other => panic!("a {UNIT}-byte unit fits in a {BURST}-byte burst: {other:?}"),
344            }
345        }
346
347        let elapsed_nanos = (STEP * u32::try_from(STEPS).expect("step count fits u32")).as_nanos();
348        let ceiling = u128::from(RATE) * elapsed_nanos / NANO + u128::from(BURST);
349
350        // The claim.
351        assert!(
352            granted <= ceiling,
353            "granted {granted} bytes over {elapsed_nanos} ns; ceiling is rate*dt + burst = {ceiling}"
354        );
355        // ...and it is not passing by granting nothing: a bucket that is
356        // offered twice its rate must still deliver its rate.
357        assert!(
358            granted >= u128::from(RATE) * elapsed_nanos / NANO,
359            "granted {granted} bytes, below the sustained rate"
360        );
361        // The residue is strictly less than one unit, so `ceiling` is a
362        // tight bound and not a decade of slack. Here it is zero: the
363        // numbers are chosen so `(burst + rate*dt)` divides by `UNIT`.
364        assert!(ceiling - granted < u128::from(UNIT), "bound is not tight: {ceiling} vs {granted}");
365        assert_eq!(grants, 5050, "grant count is deterministic: (burst + rate*dt) / unit");
366    }
367
368    /// An unlimited bucket always grants, and arms no deadline.
369    #[test]
370    fn an_unlimited_bucket_always_grants() {
371        let base = Instant::now();
372        let mut state = BucketState::new(0, base);
373
374        // No time passes at all, and the burst is zero: only `None` meaning
375        // *unlimited* rather than *zero* can carry this.
376        for i in 0..1_000u64 {
377            let g = charge(&mut state, None, 0, u64::MAX, base);
378            assert_eq!(g, Grant::Now, "unlimited bucket refused unit {i}");
379        }
380
381        // Contrast, in the same test, so "always grants" is not vacuous:
382        // `Some(0)` with the same burst never grants and never names an
383        // instant to wait for.
384        let mut zero = BucketState::new(0, base);
385        assert_eq!(charge(&mut zero, Some(0), 0, 1, base), Grant::Never);
386        assert_eq!(
387            charge(&mut zero, Some(0), 0, 1, base + Duration::from_secs(3600)),
388            Grant::Never,
389            "a zero-rate bucket does not refill, however long it waits"
390        );
391    }
392
393    /// The `Later` deadline is the earliest instant the unit fits, and
394    /// waiting exactly that long makes it fit.
395    #[test]
396    fn a_later_deadline_is_when_the_unit_fits() {
397        let base = Instant::now();
398        // 1000 bytes/s, empty burst: 500 bytes needs exactly 500 ms.
399        let mut state = BucketState::new(0, base);
400        let at = match charge(&mut state, Some(1_000), 10_000, 500, base) {
401            Grant::Later(at) => at,
402            other => panic!("expected Later, got {other:?}"),
403        };
404        assert_eq!(at, base + Duration::from_millis(500));
405
406        // One nanosecond early is still not enough; the deadline itself is.
407        assert!(matches!(
408            charge(&mut state, Some(1_000), 10_000, 500, at - Duration::from_nanos(1)),
409            Grant::Later(_)
410        ));
411        assert_eq!(charge(&mut state, Some(1_000), 10_000, 500, at), Grant::Now);
412        assert_eq!(state.available_bytes(), 0);
413    }
414
415    /// A unit larger than the burst is refused with **its own answer** —
416    /// not a deadline that would come round again and again with the bucket
417    /// capped below it, and not the answer a zero-rate bucket gives.
418    ///
419    /// The two refusals are driven in one body against the same unit size,
420    /// because the claim is a difference and a difference needs both sides.
421    /// A rate of zero is a class that was asked to stop; a rate of 1000 with
422    /// a burst of 100 is a class that was asked for 1000 bytes a second and
423    /// will never see one byte of it, and only the second is worth a report.
424    ///
425    /// *Ablation:* restore the single `if rate == 0 || need > cap { Never }`
426    /// — the first assertion reddens with
427    /// `left: Never / right: LargerThanBurst { burst_bytes: 100, unit_bytes: 101 }`,
428    /// which is precisely the collapse that made a mis-sized burst
429    /// unreportable.
430    #[test]
431    fn a_unit_larger_than_the_burst_is_refused_as_a_burst_problem() {
432        let base = Instant::now();
433        let mut state = BucketState::new(100, base);
434        let too_big = Grant::LargerThanBurst { burst_bytes: 100, unit_bytes: 101 };
435        assert_eq!(charge(&mut state, Some(1_000), 100, 101, base), too_big);
436        // A century of refill does not change it: the cap, not the wait, is
437        // what refuses.
438        let much_later = base + Duration::from_secs(60 * 60 * 24 * 365);
439        assert_eq!(charge(&mut state, Some(1_000), 100, 101, much_later), too_big);
440        // And the bucket is still capped at the burst, not accumulating.
441        assert_eq!(state.available_bytes(), 100);
442
443        // The other side of the difference: the same over-sized unit against
444        // a bucket whose rate is zero is `Never`, because a class configured
445        // to stop is doing what it was told and there is nothing to report.
446        let mut stopped = BucketState::new(100, base);
447        assert_eq!(charge(&mut stopped, Some(0), 100, 101, base), Grant::Never);
448
449        // ...and one that *does* fit is granted, so neither refusal above is
450        // the bucket refusing everything.
451        assert_eq!(charge(&mut state, Some(1_000), 100, 100, much_later), Grant::Now);
452    }
453
454    /// A `now` that goes backwards under-grants; it never manufactures
455    /// tokens and never rewinds the bucket's own clock.
456    #[test]
457    fn a_backwards_now_does_not_manufacture_tokens() {
458        let base = Instant::now();
459        let mut state = BucketState::new(0, base);
460        let later = base + Duration::from_secs(1);
461        assert_eq!(charge(&mut state, Some(1_000), 10_000, 1_000, later), Grant::Now);
462
463        // Rewind a second: no refill, and the state's clock stays at
464        // `later`, so the deadline is measured from `later` and the next
465        // forward call does not re-credit the gap.
466        assert_eq!(
467            charge(&mut state, Some(1_000), 10_000, 1, base),
468            Grant::Later(later + Duration::from_millis(1)),
469        );
470        assert_eq!(
471            charge(&mut state, Some(1_000), 10_000, 1_000, later + Duration::from_secs(1)),
472            Grant::Now,
473        );
474    }
475}