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}