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.
108    ///
109    /// [`charge`] answers [`Grant::LargerThanBurst`] for that case rather
110    /// than folding it into [`Grant::Never`], so the caller can report it as
111    /// the misconfiguration it is instead of as the ordinary rate limiting
112    /// it is indistinguishable from. It cannot be rejected when the profile
113    /// is built: [`ShapeProfile::try_new`](super::ShapeProfile::try_new) has
114    /// the burst but not the object sizes, and the sizes are what decide.
115    pub burst_bytes: u64,
116    /// Optional ceiling for borrowing above `rate_bps`.
117    ///
118    /// **Reserved.** The scheduler accepts this field and never borrows, with no
119    /// diagnostic — the frozen `ShapeError` has no variant it could land
120    /// in. A profile setting `ceil_bps` above `rate_bps` measures a flat
121    /// `rate_bps`; this sentence is the whole of the warning.
122    #[cfg_attr(feature = "serde", serde(default))]
123    pub ceil_bps: Option<u64>,
124}
125
126/// The mutable half of a token bucket: what it holds and when it was last
127/// refilled.
128///
129/// Session-scoped and shared by every stream of a class, so it is the
130/// scheduler's state and not a stream's. Kept separate from
131/// [`BucketConfig`] so the configuration stays comparable and cloneable
132/// while the state stays exactly one owner's.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct BucketState {
135    /// Held tokens in nano-bytes. Never exceeds `burst_bytes * NANO` and
136    /// never goes negative — the type forbids it, and so does the rate
137    /// bound the bucket exists to hold.
138    tokens: u128,
139    /// The instant `tokens` was last brought up to date.
140    last: Instant,
141}
142
143impl BucketState {
144    /// A bucket that starts full at `burst_bytes`, as of `now`.
145    ///
146    /// Starting full is what makes the first unit of a session go
147    /// immediately; starting empty would put a `burst_bytes / rate_bps`
148    /// delay in front of every stream and read as a broken proxy.
149    pub fn new(burst_bytes: u64, now: Instant) -> Self {
150        Self { tokens: u128::from(burst_bytes) * NANO, last: now }
151    }
152
153    /// Whole bytes currently held, as of the last [`charge`].
154    ///
155    /// Does not refill: this is a read of recorded state, not a clock
156    /// reading, so it stays usable from a test that fabricates its own
157    /// instants.
158    pub fn available_bytes(&self) -> u64 {
159        u64::try_from(self.tokens / NANO).unwrap_or(u64::MAX)
160    }
161}
162
163/// What [`charge`] decided about one unit.
164///
165/// `#[non_exhaustive]` with no `Default`: there is no safe default answer.
166/// Defaulting to `Now` would silently unshape a bucket; defaulting to
167/// `Never` would silently stall one.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169#[non_exhaustive]
170pub enum Grant {
171    /// The unit may go now. The tokens have already been debited.
172    Now,
173    /// Not now. The bucket holds enough at this instant and not before, so
174    /// this is the deadline to arm.
175    ///
176    /// The instant is *earliest*, not *exact*: another stream sharing the
177    /// bucket may drain it again before this one wakes, in which case the
178    /// next [`charge`] answers `Later` again with a new deadline.
179    Later(Instant),
180    /// Not now, and no refill instant exists, because the rate is `Some(0)`
181    /// and the bucket therefore never refills.
182    ///
183    /// The caller must supply its own deadline, which is the unit's
184    /// `max_hold` clamp. Returning a fabricated far-future instant instead
185    /// was rejected: it would arm a release timer that never fires, and
186    /// nothing downstream could tell that deadline from a real one.
187    Never,
188    /// Not now, and not ever from tokens: the unit is larger than the whole
189    /// bucket, so no amount of refilling reaches it.
190    ///
191    /// **A separate answer from [`Self::Never`], and the separation is the
192    /// point.** Both leave the caller to fall back on the `max_hold` clamp,
193    /// so the two are identical in what they *do*; they are opposite in what
194    /// they mean. `Never` is a rate of zero doing exactly what it was asked
195    /// to. This is a rate that was asked for and cannot be delivered, because
196    /// the bucket cannot hold one object's worth of tokens — every unit
197    /// leaves at the clamp and the configured rate never binds at all. Folded
198    /// into one variant, the second is invisible: it produces the same clamp,
199    /// the same `tokens_exhausted_episodes` and the same `HoldClamped` report
200    /// a working rate limit produces.
201    ///
202    /// Reachable only with a non-zero rate — a zero rate is answered first —
203    /// so a caller reporting this is always reporting a burst that is too
204    /// small and never a class that was configured to stop.
205    LargerThanBurst {
206        /// The bucket's cap, which is [`BucketConfig::burst_bytes`].
207        burst_bytes: u64,
208        /// The unit that did not fit, so the report can quote both numbers
209        /// rather than leaving the reader to find one of them.
210        unit_bytes: u64,
211    },
212}
213
214/// Charge `bytes` against a bucket and say whether the unit may go.
215///
216/// **Pure with respect to the clock** — `now` is a parameter and no clock
217/// is read, following `release_timer::plan`. `state` is refilled to `now`
218/// before the decision and debited only on [`Grant::Now`]; every refusal
219/// costs nothing, so re-charging the same unit after its deadline is correct
220/// rather than double-billing.
221///
222/// The bound **this function** upholds, exactly: over any interval, the
223/// bytes it grants never exceed `rate_bps * elapsed + burst_bytes`. That is
224/// what makes the rate claim provable arithmetic rather than a measurement,
225/// and `the_bucket_never_grants_above_rate_plus_burst` proves it over
226/// 10 001 fabricated steps.
227///
228/// # It is not the bound the *shaper* upholds
229///
230/// Stated here because the difference is a factor of thousands and a reader
231/// of this line will otherwise assume the wrong one. `PendingQueue`'s
232/// release seam checks the unit's `max_hold` clamp **before** it calls
233/// `acquire`, and under the default [`Expiry::Deliver`](super::Expiry) a
234/// clamped unit goes out without a bucket being consulted at all — that is
235/// what "a 0-bps class still delivers, at `max_hold`" means. So the
236/// end-to-end ceiling is
237///
238/// ```text
239/// min(rate_bps * elapsed + burst_bytes,   <- this function
240///     depth / max_hold * elapsed)         <- the clamp, per stream
241/// ```
242///
243/// The clamp is the deliberate non-destructive default — a starved unit is
244/// delivered late rather than dropped — and not a defect, but a fixture that
245/// means to observe *this* function's bound has to pin
246/// `max_hold` far enough out that the clamp cannot bind inside its sampling
247/// window — which is why every starvation fixture is required to name it.
248///
249/// `now` is expected to be monotonic. A `now` earlier than the last one
250/// refills nothing and does not rewind the bucket's clock, so an
251/// out-of-order caller under-grants rather than manufacturing tokens.
252pub fn charge(
253    state: &mut BucketState,
254    rate_bps: Option<u64>,
255    burst_bytes: u64,
256    bytes: u64,
257    now: Instant,
258) -> Grant {
259    // Unlimited: no accounting at all, so an unshaped class costs nothing
260    // per unit and arms no deadline. `None` is emphatically not `Some(0)`.
261    let Some(rate) = rate_bps else {
262        return Grant::Now;
263    };
264
265    let cap = u128::from(burst_bytes) * NANO;
266    if now > state.last {
267        let elapsed = (now - state.last).as_nanos();
268        state.tokens = (state.tokens + elapsed * u128::from(rate)).min(cap);
269        state.last = now;
270    } else {
271        // Still clamp: `burst_bytes` may have shrunk under a reconfigure.
272        state.tokens = state.tokens.min(cap);
273    }
274
275    let need = u128::from(bytes) * NANO;
276    if state.tokens >= need {
277        state.tokens -= need;
278        return Grant::Now;
279    }
280
281    // Two unreachable answers, deliberately not one. A zero rate is asked
282    // for first, so a class configured to stop is never reported as a class
283    // whose burst is mis-sized; what is left is a rate the caller does want
284    // and a bucket that cannot hold one unit of it, which is the case that
285    // has no other symptom.
286    if rate == 0 {
287        return Grant::Never;
288    }
289    if need > cap {
290        return Grant::LargerThanBurst { burst_bytes, unit_bytes: bytes };
291    }
292
293    let deficit = need - state.tokens;
294    let wait_nanos = deficit.div_ceil(u128::from(rate));
295    let wait = u64::try_from(wait_nanos).map(Duration::from_nanos);
296    match wait.ok().and_then(|d| state.last.checked_add(d)) {
297        Some(at) => Grant::Later(at),
298        // A wait that does not fit in a `Duration`, or an `Instant` past
299        // the platform's representable range, is not a deadline anyone can
300        // arm. Say so rather than saturate into a lie.
301        None => Grant::Never,
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    /// The exact rate bound, over fabricated instants.
310    ///
311    /// Demand is twice the rate, so the bucket — not the offered load — is
312    /// what limits the total. With demand at or below the rate the bucket
313    /// never binds and the assertion would hold however wrong the
314    /// arithmetic was.
315    #[test]
316    fn the_bucket_never_grants_above_rate_plus_burst() {
317        const RATE: u64 = 1_000_000; // bytes/s
318        const BURST: u64 = 10_000; // bytes
319        const STEPS: u64 = 10_000;
320        const STEP: Duration = Duration::from_micros(100); // 100 bytes of refill
321        const UNIT: u64 = 200; // 2x the rate
322
323        let base = Instant::now();
324        let mut state = BucketState::new(BURST, base);
325        let mut granted: u128 = 0;
326        let mut grants = 0u64;
327
328        // `0..=STEPS`: the first charge happens at `base`, before any
329        // refill, so the full bucket is never clamped away and the ceiling
330        // below is reached exactly rather than approached.
331        for i in 0..=STEPS {
332            let now = base + STEP * u32::try_from(i).expect("step index fits u32");
333            match charge(&mut state, Some(RATE), BURST, UNIT, now) {
334                Grant::Now => {
335                    granted += u128::from(UNIT);
336                    grants += 1;
337                }
338                Grant::Later(at) => assert!(at > now, "a Later deadline must be in the future"),
339                other => panic!("a {UNIT}-byte unit fits in a {BURST}-byte burst: {other:?}"),
340            }
341        }
342
343        let elapsed_nanos = (STEP * u32::try_from(STEPS).expect("step count fits u32")).as_nanos();
344        let ceiling = u128::from(RATE) * elapsed_nanos / NANO + u128::from(BURST);
345
346        // The claim.
347        assert!(
348            granted <= ceiling,
349            "granted {granted} bytes over {elapsed_nanos} ns; ceiling is rate*dt + burst = {ceiling}"
350        );
351        // ...and it is not passing by granting nothing: a bucket that is
352        // offered twice its rate must still deliver its rate.
353        assert!(
354            granted >= u128::from(RATE) * elapsed_nanos / NANO,
355            "granted {granted} bytes, below the sustained rate"
356        );
357        // The residue is strictly less than one unit, so `ceiling` is a
358        // tight bound and not a decade of slack. Here it is zero: the
359        // numbers are chosen so `(burst + rate*dt)` divides by `UNIT`.
360        assert!(ceiling - granted < u128::from(UNIT), "bound is not tight: {ceiling} vs {granted}");
361        assert_eq!(grants, 5050, "grant count is deterministic: (burst + rate*dt) / unit");
362    }
363
364    /// An unlimited bucket always grants, and arms no deadline.
365    #[test]
366    fn an_unlimited_bucket_always_grants() {
367        let base = Instant::now();
368        let mut state = BucketState::new(0, base);
369
370        // No time passes at all, and the burst is zero: only `None` meaning
371        // *unlimited* rather than *zero* can carry this.
372        for i in 0..1_000u64 {
373            let g = charge(&mut state, None, 0, u64::MAX, base);
374            assert_eq!(g, Grant::Now, "unlimited bucket refused unit {i}");
375        }
376
377        // Contrast, in the same test, so "always grants" is not vacuous:
378        // `Some(0)` with the same burst never grants and never names an
379        // instant to wait for.
380        let mut zero = BucketState::new(0, base);
381        assert_eq!(charge(&mut zero, Some(0), 0, 1, base), Grant::Never);
382        assert_eq!(
383            charge(&mut zero, Some(0), 0, 1, base + Duration::from_secs(3600)),
384            Grant::Never,
385            "a zero-rate bucket does not refill, however long it waits"
386        );
387    }
388
389    /// The `Later` deadline is the earliest instant the unit fits, and
390    /// waiting exactly that long makes it fit.
391    #[test]
392    fn a_later_deadline_is_when_the_unit_fits() {
393        let base = Instant::now();
394        // 1000 bytes/s, empty burst: 500 bytes needs exactly 500 ms.
395        let mut state = BucketState::new(0, base);
396        let at = match charge(&mut state, Some(1_000), 10_000, 500, base) {
397            Grant::Later(at) => at,
398            other => panic!("expected Later, got {other:?}"),
399        };
400        assert_eq!(at, base + Duration::from_millis(500));
401
402        // One nanosecond early is still not enough; the deadline itself is.
403        assert!(matches!(
404            charge(&mut state, Some(1_000), 10_000, 500, at - Duration::from_nanos(1)),
405            Grant::Later(_)
406        ));
407        assert_eq!(charge(&mut state, Some(1_000), 10_000, 500, at), Grant::Now);
408        assert_eq!(state.available_bytes(), 0);
409    }
410
411    /// A unit larger than the burst is refused with **its own answer** —
412    /// not a deadline that would come round again and again with the bucket
413    /// capped below it, and not the answer a zero-rate bucket gives.
414    ///
415    /// The two refusals are driven in one body against the same unit size,
416    /// because the claim is a difference and a difference needs both sides.
417    /// A rate of zero is a class that was asked to stop; a rate of 1000 with
418    /// a burst of 100 is a class that was asked for 1000 bytes a second and
419    /// will never see one byte of it, and only the second is worth a report.
420    #[test]
421    fn a_unit_larger_than_the_burst_is_refused_as_a_burst_problem() {
422        let base = Instant::now();
423        let mut state = BucketState::new(100, base);
424        let too_big = Grant::LargerThanBurst { burst_bytes: 100, unit_bytes: 101 };
425        assert_eq!(charge(&mut state, Some(1_000), 100, 101, base), too_big);
426        // A century of refill does not change it: the cap, not the wait, is
427        // what refuses.
428        let much_later = base + Duration::from_secs(60 * 60 * 24 * 365);
429        assert_eq!(charge(&mut state, Some(1_000), 100, 101, much_later), too_big);
430        // And the bucket is still capped at the burst, not accumulating.
431        assert_eq!(state.available_bytes(), 100);
432
433        // The other side of the difference: the same over-sized unit against
434        // a bucket whose rate is zero is `Never`, because a class configured
435        // to stop is doing what it was told and there is nothing to report.
436        let mut stopped = BucketState::new(100, base);
437        assert_eq!(charge(&mut stopped, Some(0), 100, 101, base), Grant::Never);
438
439        // ...and one that *does* fit is granted, so neither refusal above is
440        // the bucket refusing everything.
441        assert_eq!(charge(&mut state, Some(1_000), 100, 100, much_later), Grant::Now);
442    }
443
444    /// A `now` that goes backwards under-grants; it never manufactures
445    /// tokens and never rewinds the bucket's own clock.
446    #[test]
447    fn a_backwards_now_does_not_manufacture_tokens() {
448        let base = Instant::now();
449        let mut state = BucketState::new(0, base);
450        let later = base + Duration::from_secs(1);
451        assert_eq!(charge(&mut state, Some(1_000), 10_000, 1_000, later), Grant::Now);
452
453        // Rewind a second: no refill, and the state's clock stays at
454        // `later`, so the deadline is measured from `later` and the next
455        // forward call does not re-credit the gap.
456        assert_eq!(
457            charge(&mut state, Some(1_000), 10_000, 1, base),
458            Grant::Later(later + Duration::from_millis(1)),
459        );
460        assert_eq!(
461            charge(&mut state, Some(1_000), 10_000, 1_000, later + Duration::from_secs(1)),
462            Grant::Now,
463        );
464    }
465}