Skip to main content

Module shape

Module shape 

Source
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. ProxyStatsLegStats, 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 ClassRule claims.
scheduler 🔒
The session-scoped shaper: classification, admission and release.
stats 🔒
Per-class shaping statistics: the atomic storage and its snapshot.

Structs§

BucketConfig
A named token bucket. One per class, or shared by several.
BucketState
The mutable half of a token bucket: what it holds and when it was last refilled.
ClassRule
A matcher plus what to do with what it matches.
ClassStats
Per-class shaping statistics, one entry in ShapeStats.
DirectionStats
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 ClassRule claims.
ProxyStats
Shaping statistics for a whole proxy: every session it has accepted, including the ones that have already ended.
QueueConfig
The per-stream queue policy: how deep, how long, and what happens at each limit.
RangeSet
A sorted, coalesced set of inclusive u64 ranges.
SessionStats
What every session this proxy has run did, added up — the flat form of ProxyStats.
ShapeProfile
A complete egress shaping configuration for one session.
ShapeStats
Shaping statistics for one session.
StreamKey
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 charge decided about one unit.
MatchKind
What a Matcher can be aimed at.
MatcherField
One Matcher key, named so a report about it is typed rather than a string.
Overflow
What happens when a per-stream queue is full.
ShapeError
Why a ShapeProfile could not be built.

Functions§

charge
Charge bytes against a bucket and say whether the unit may go.