Expand description
Egress shaping — the configuration a caller writes, and the pure primitives the scheduler is built from.
A ShapeProfile describes what one session’s media egress is
allowed to do: named token buckets, class rules that aim a Matcher
at a bucket, one bounded-queue policy, and a Discipline that
arbitrates between classes competing for the same bucket. Control
streams are never shaped — pacing SUBSCRIBE and ANNOUNCE behind a video
bucket would stall MoQT’s normal steady state and make an idle control
stream look like a dead session.
§Why this module is pub
Unlike the engine internals (egress, exec, release_timer), a
caller constructs these types, so they are public and every
item below carries a rustdoc comment.
§The two constructor shapes, and why they differ
ShapeProfile has private fields and a fallible constructor. A
mis-typed bucket name would otherwise be a silently inert class —
configuration that looks applied and does nothing, which is exactly the
failure this module exists to make impossible. ShapeProfile::try_new
rejects it, so an invalid profile cannot reach a session and there is no
runtime your config was rejected path to miss.
The four config structs it is built from — BucketConfig,
ClassRule, Matcher and QueueConfig — are the opposite:
all-public fields and #[non_exhaustive] with a Default,
exactly as EgressConfig is. The pairing
is load-bearing rather than stylistic: #[non_exhaustive] on its own
makes a struct unconstructible outside this crate, because
struct-expression and functional-update syntax are both illegal there
— the entire public configuration surface would be unreachable from an
integration-test crate and from every caller’s code.
Note precisely what the Default buys, because it is one step less than
it looks: ..Default::default() is also E0639 outside this crate,
so an outside caller writes let mut m = Matcher::default(); followed by
per-field assignment — which is what tests/actions_shaping.rs does at
every construction site. What the Default provides is a value to
start from, not a syntax. Inside this crate both forms compile, which is
why the unit tests below use the shorter one; a sentence claiming the
functional-update form works for an outside caller was measured false
(11 × E0639 out of tree, on all four structs).
§State of the module
The types, Matcher::matches, ShapeProfile::try_new’s validation
and the pure token bucket (charge) landed first, with their own unit
tests, so the scheduler that consumes them lands against arithmetic that
is already gated.
A configured ShapeProfile arms framing on its own
(ProxySessionConfig::shape), ShapeStats is recorded and readable
through
ProxySession::shape_stats,
admission runs — per-unit classification through Matcher, the
per-stream queue depth and all three Overflow policies — and so does
release: every shaped unit is queued rather than written inline, its
class’s token bucket is debited at PendingQueue::pop_next_due, the
configured Discipline arbitrates between classes sharing a bucket,
and Expiry decides what becomes of a unit that outlives max_hold.
The same figures are kept a second time for a whole proxy. ProxyStats
— LegStats, SessionStats and the class rows — is read through
ProxyControl::stats and covers
every session the proxy has accepted, including the ones that have already
ended, so it is cumulative where ProxySession::shape_stats is one
session’s own. It is charged by the same writers, forwarded from inside
each one, so no figure can reach a session’s rows and miss the proxy’s.
The one shape difference is worth knowing before reading a cell: a
session’s totals carry a direction only, while a proxy’s carry a leg and
a direction, because a proxy holds two connections and a byte crosses
both. LegStats states which cell each measurement lands in.
Two things are deliberately outside that: control streams, which install no scheduler at all, and teardown, which drains ignoring release times so a bucket can never gate a mirrored reset.
An object too large for the framer to buffer is outside it as well, and
says so. Such an object has no ObjectMeta, so no rule can name it, so no
bucket charges it and it is granted unconditionally — one object can
therefore cross a class’s rate whole. Measured: a 4 MiB object crossed in
800 ms against a class whose bucket was configured at zero bytes per second.
The bytes are accounted on ShapeStats::unshapeable and the session
reports Impairment{ShapeUnpacedObject}, once per stream, naming the class
the stream’s other units are charged to — because my 500 kbps cap was
breached by one large segment is otherwise a hole in the accounting with
nothing to attribute it to.
Datagrams are policed rather than paced, which is a different
operation and not a lesser one. forward_datagrams classifies each
datagram through Matcher::matches_datagram, asks its class’s bucket
for the bytes, and discards what the bucket refuses instead of
queuing it. Nothing on that path delays anything, and nothing should: a
FIFO would impose a delivery order the protocol does not have, and a
datagram has neither a successor written against it nor a stream whose
object IDs would move behind a hole — which is exactly what makes
dropping the arriving unit sound here and unsound for a queued stream
unit.
What follows from that shape, and is worth knowing before reading a
figure: QueueConfig is not consulted for a datagram. Neither
depth binds it and no Overflow policy decides it, because it is never
queued — the bucket is the whole of the decision. A profile that shapes
subgroup streams and polices datagrams reads its queue policy for the
first and not for the second.
This is settled rather than pending, and the sharp edge is worth stating outright: the bucket can answer not now, but at this instant — the same answer that defers a stream unit — and on the datagram path that answer is discarded like every other refusal. A datagram over a live rate is dropped where it arrived, not held until the instant its own bucket named.
If what is wanted is smoothing, reach for quinn-netem. It delays,
jitters and reorders at the socket, under the whole connection, which is
the scope a link-level queue has: a bottleneck queues by link, not by
track, and a router does not know which track a datagram belongs to.
Class-aware policing is a real box — an operator rate-limiter drops over
rate — while class-aware smoothing is a scheduler inside a router, which
is not a condition a player is ever placed in. So netem’s not being
class-aware is the right scope for it rather than a gap in it, and the
division is: a rate on one track is a class over a bucket, and belongs
here; a congested path is netem.
The framed sites keep their Delay and Hold because a stream has a
delivery order — holding object N and then N+1 preserves a guarantee the
protocol makes, where holding two datagrams would manufacture one.
There is no seam a datagram never reaches. There was one — a report a
Fetch-aimed class made on drafts 18 and 19, where the framer bypassed
every fetch stream before any ObjectMeta existed — and it went when
those streams became readable. Every unmatchable rule now reports from a
unit that arrived, through Scheduler::classify or its datagram sibling.
§What is still owed
BucketConfig::ceil_bps is accepted and never borrowed against: a
profile setting it above rate_bps measures a flat rate_bps.
Nothing else, and there used to be more: five reported fields here
snapshotted as a constant zero. Two of them counted what a hook does,
which needs no profile at all while every figure on this page is gated on
one, and they are
Counters::units_delayed
and
Counters::objects_truncated
now. The other three were Duration totals; ClassStats says why this
page carries no duration at all.
Separately, and not the same kind of zero: of the five figures a
DirectionStats carries, only objects_seen and bytes_shaped are
measured at both crossings, so the three event figures read zero in a
departure cell of ProxyStats::per_leg. LegStats says which
cell is which.
Modules§
- bucket 🔒
- The token bucket — configuration, state, and the one pure function that decides whether a unit may go now.
- matcher 🔒
- Class matching — what a
ClassRuleclaims. - scheduler 🔒
- The session-scoped shaper: classification, admission and release.
- stats 🔒
- Per-class shaping statistics: the atomic storage and its snapshot.
Structs§
- Bucket
Config - A named token bucket. One per class, or shared by several.
- Bucket
State - The mutable half of a token bucket: what it holds and when it was last refilled.
- Class
Rule - A matcher plus what to do with what it matches.
- Class
Stats - Per-class shaping statistics, one entry in
ShapeStats. - Direction
Stats - Five totals over the traffic travelling one way.
- LegStats
- One connection’s shaping statistics, split by which way the traffic was
going — one entry of
ProxyStats::per_leg. - Matcher
- Which units a
ClassRuleclaims. - Proxy
Stats - Shaping statistics for a whole proxy: every session it has accepted, including the ones that have already ended.
- Queue
Config - The per-stream queue policy: how deep, how long, and what happens at each limit.
- Range
Set - A sorted, coalesced set of inclusive
u64ranges. - Session
Stats - What every session this proxy has run did, added up — the flat form of
ProxyStats. - Shape
Profile - A complete egress shaping configuration for one session.
- Shape
Stats - Shaping statistics for one session.
- Stream
Key - Identifies one forwarded stream within a session.
Enums§
- Discipline
- How classes competing for the same bucket are arbitrated.
- Expiry
- What happens when a queued object outlives
max_hold. - Grant
- What
chargedecided about one unit. - Match
Kind - What a
Matchercan be aimed at. - Matcher
Field - One
Matcherkey, named so a report about it is typed rather than a string. - Overflow
- What happens when a per-stream queue is full.
- Shape
Error - Why a
ShapeProfilecould not be built.
Functions§
- charge
- Charge
bytesagainst a bucket and say whether the unit may go.