#[non_exhaustive]pub enum ImpairmentKind {
Show 15 variants
FramerBypass {
stream_id: u64,
draft: DraftVersion,
reason: BypassReason,
},
ObjectNotAddressable {
stream_id: u64,
total: u64,
},
ControlFrameNotDecodable {
type_id: u64,
total: u64,
},
EgressQueueFull {
stream_id: u64,
},
HoldClamped {
requested: Option<Duration>,
applied: Duration,
},
ControlStreamTruncated {
error: String,
},
QueuedBytesAtTeardown {
stream_id: u64,
bytes: usize,
},
DatagramNotSent {
error: String,
},
CoarseReleaseTimer {
backend: TimerBackend,
detail: Option<String>,
},
ElideFixupLost {
stream_id: u64,
reason: BypassReason,
code: u64,
},
SerializeTargetUnknown {
key: StreamKey,
target: StreamKey,
},
ShapeRuleUnmatchable {
class: String,
field: MatcherField,
draft: DraftVersion,
},
ShapeBurstBelowUnit {
class: String,
burst_bytes: u64,
unit_bytes: u64,
},
ShapeUnpacedObject {
class: String,
stream_id: u64,
bytes: u64,
},
ClassChangedMidStream {
key: StreamKey,
stream_id: u64,
},
}Expand description
A reduction in what the proxy can do or observe.
Every variant states its emission cardinality, because the counts
differ by an order of magnitude between them — some fire once per
session, some once per stream, and some once per unit — and a test
that assumes “exactly one” against a per-unit variant is a test that
goes red for the wrong reason. Where a variant is capped below its
natural rate, a counter on
crate::instrument::Counters carries the running total instead.
Like Effect, this derives PartialEq and Eq; the event that
carries it does not.
§Every variant here has a producer
None of these is reserved, aspirational or waiting for a call site: each
one is emitted by code in this crate, and the emission happens after
what it reports — see ProxyEvent::Impairment for why that ordering is
the whole value of the surface.
One is harder to reach than the rest and says so on itself:
Self::CoarseReleaseTimer needs MOQTAP_RELEASE_TIMER=condvar,
because every default release backend is high-resolution. That is a
diagnostic override rather than a fallback, and the variant exists so a
run made under it cannot quote delays it was unable to honour.
A variant added here is forced to answer one more question before it compiles: which of the proxy’s two connections it is about. The mapping that answers it matches exhaustively with no catch-all, so a new report stops the crate building rather than defaulting to a leg somebody else chose.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
FramerBypass
The framer stopped parsing a stream, so no object on it is addressable. Emitted exactly once per stream.
ObjectNotAddressable
An object exceeded the framer’s buffer cap and was streamed through without being addressable.
Emitted at most once per stream, on the first such object, and
carries the running count. Every other ImpairmentKind states its
cardinality; this one did not, and it fires per object — a stream
of large objects under a low max_buffered_object_bytes would
otherwise emit one event per object and swamp an observer written
against the once-per-stream cardinality every neighbouring variant
documents. The count keeps the information: total is the number
of unaddressable objects seen on this stream at the moment of
emission, i.e. 1, and
crate::instrument::Counters::objects_not_addressable is the
running total that stays accurate afterwards.
ControlFrameNotDecodable
A control frame’s body was refused by the decoder, so the proxy forwarded a message it could not read.
The frame’s declared length was intact — that is what let the parser find the frame behind it — and only the message inside it failed to decode. The bytes reach the peer regardless, in the position they held: on the observation-only control pipe they were forwarded before anything was parsed, and on the mutating pipe, where the parser owns the forwarding path, they are written verbatim without a hook being consulted. So what a refusal costs is this proxy’s account of a message, never the message.
That account is the whole product, which is why the loss is reported. Without this event a control message the proxy could not read is indistinguishable from one the peer never sent, and the two call for opposite conclusions. It is not a per-draft hazard: a Message Type the configured draft does not assign, a frame whose body does not match its declared length, and anything an extension adds all take the same path on all fourteen drafts.
§Cardinality: at most once per control stream direction
Emitted on the first refused frame, carrying the count as it stood
when the report went out. A peer repeating an unassigned Message
Type would otherwise emit one event per frame, against neighbouring
variants an observer has been told fire once per stream — the same
argument Self::ObjectNotAddressable makes for the same shape of
hazard.
The running figure that stays accurate afterwards is
crate::instrument::Counters::control_frames_not_decodable. That
counter and this event count different things on purpose: one frame
refused and forty refused produce one event each and differ by
thirty-nine there.
Fields
type_id: u64The Message Type varint the first refused frame declared.
The type is read from the frame header, which decoded; nothing inside the frame did, so this is the whole of what the refused message can still say about itself. A later refusal of a different type is counted and not named — see the cardinality note above.
EgressQueueFull
A stream’s pending queue reached
crate::action::EgressConfig::max_pending_bytes, so delay has
become backpressure. Emitted once per stream, on the transition
into backpressure — a queue that drains and fills again does not
report twice.
HoldClamped
A delay, or a shaped release, was clamped to
crate::action::EgressConfig::max_hold.
Emitted once per clamped unit, not once per stream: a hook
that returns an over-long Delay for every object emits one event
per object. It is a property of the decision, and the decision is
taken again for the next unit.
§requested: None is an unbounded wait, not a missing figure
A hook’s own Delay { by } names a duration, so it reports
Some(by). A shaped release frequently names none: a class whose
rate is zero, or whose burst is smaller than the unit at the head of
its queue, has no refill instant, and the clamp is the only
thing that will ever release that unit. None is that case, and it
is the honest report — there is no duration to quote and inventing a
finite one would be a fabrication.
The unbounded case was once reported as Duration::MAX, which
reaches a log as 18446744073709551615.999999999s. Two things went
wrong with that. A reader sees a 584-billion-year request beside a
2 s applied one and takes it for an encoding fault in whatever
rendered it, filing against the wrong component; and the natural
assertion that a clamp reduced the request — requested > applied
— cannot tell that sentinel from a real thirty-second request, so it
passes either way and certifies nothing.
Fields
ControlStreamTruncated
A control stream’s destination ended with a FIN part-way through a message. The proxy does not synthesize a reset there — that would be a session-level protocol violation — and it does not finish the message either, because completing it would mean inventing control-stream bytes neither peer wrote. On a data stream the equivalent failure synthesizes a reset, which is what makes the truncation visible to the peer; here only this report carries it.
Two things produce it, and error says which:
- a non-reset read failure on the source half, which is the last read that stream will ever do;
- the drain window of a requested session close expiring while a
message was part-written — see
ProxyControl::close_session.
Emitted at most once per control stream direction. The two
producers cannot both fire on one direction: the first returns from
the pipe, so the second is unreachable afterwards. A session with
both control directions affected emits two, one per side.
The second producer says nothing at all when the proxy cannot tell where the message boundaries are — a control stream whose framing could not be followed reports no truncation rather than a guessed one.
QueuedBytesAtTeardown
Queued bytes were still pending when the session tore down. They
were flushed best-effort; bytes may not have reached the peer.
Emitted at most once per stream, at teardown, and only for a stream that still had something queued when teardown began — a stream that drained at its release times reports nothing.
bytes counts everything the teardown flush could not vouch for:
what it could not write and what it wrote into a transport the
session was already closing. The second half is not pedantry — a
write_all into quinn returns Ok as soon as the bytes are
buffered, and Connection::close discards that buffer, so counting
only the residue reports zero for precisely the case that loses
data. A peer that did receive the bytes gets a spurious impairment;
that is the safe side to err on.
Fields
DatagramNotSent
A datagram could not be handed to the transport, and the session survived.
Emitted once per rejected datagram — this is a per-unit
variant, so a source steadily sending datagrams above the path MTU
produces one event each. Nothing caps it, because unlike
Self::ObjectNotAddressable there is no stream to attribute a
running total to.
Distinct from ProxyEvent::ActionFailed, which requires a site
and an action: this is the un-hooked path, where nobody acted
and there is nothing to name. Both exist because
forward_datagrams drops the ? on every send_datagram
call, not only the ones behind a hook.
CoarseReleaseTimer
The release wheel is not high-resolution on this host, so
crate::action::Action::Delay cannot resolve below the ~15.6 ms
system tick and every delay shorter than it is really a tick.
The default backend is high-resolution on every platform, so in
0.4.0 the only way to reach this is
MOQTAP_RELEASE_TIMER=condvar on Windows — the diagnostic
override, not a fallback. It is reported rather than assumed away
because a forced backend is still a run whose delays were not
honoured.
Emitted once per session, on that session’s first deferred
release. The measured shortfall is in
crate::instrument::Counters::release_errors; this event exists
so a report cannot quote “5 ms delay applied” while silently
having been unable to apply it.
Fields
backend: TimerBackendThe backend the release thread resolved to.
ElideFixupLost
The framer stopped parsing a stream while an elide fix-up was still owed, so the remainder of the stream cannot be renumbered.
The destination stream is reset rather than forwarded, because the alternative is delivering bytes that are known to decode to the wrong Object IDs. This is the residual case of the elide mechanism, and it is reachable: a mid-stream decode error or an unmeasurable oversized object can latch bypass at any point after an elide.
Emitted at most once per stream, and it is that stream’s last
event: the reset has already been asked for by the time this
arrives, and the forwarding task returns immediately afterwards.
The order is that way round on purpose — code names the value the
destination was reset with, so an event raised above the reset
would describe a wire change that had not been made, and this arm
has no later event to correct it with. It is the
fixup_owed == true half of the one FramerOut::Bypassed a
stream can produce — the half that reports Self::FramerBypass
is the other.
Fields
reason: BypassReasonWhy the framer gave up.
SerializeTargetUnknown
A StreamAction::SerializeAfter
named a stream there is nothing to wait for, so the stream it was
returned on proceeded immediately.
Three cases reach it and they are deliberately one report: the target
never existed, the target had already ended, or the target is this
stream itself. All three are a hook naming a stream that cannot end
later than now, and in all three the honest engine behaviour is to
proceed — a serialize that silently held forever would be a
max_hold stall attributed to the wrong thing.
Emitted once per stream. A stream takes at most one serialize decision at each of the two stream sites, and a decision that names a live target reports nothing at all.
Both fields are StreamKeys rather than transport ids, because
attribution is the whole point of the report and the transport id is
the constant 0 on every WebTransport stream.
ShapeRuleUnmatchable
A ClassRule keys on a field the wire
does not carry on this draft and stream kind, so it can never claim
a unit and everything it was aimed at falls to the default class.
This is the report that keeps the rule a key the wire does not carry
never matches from being a silent no-op. Discipline::StrictPriority
is specified on publisher_priority, which is None on drafts 15-19
under the default-priority bit — so without this, starve video while
audio flows is a silent no-op on the five newest drafts and the run
reports success. A rule aimed at
MatchKind::Datagram reports
through the same seam, from the datagram forwarder rather than from the
framer, and reports the one key a datagram can be missing: its priority,
which drafts 15 and later let a type byte leave off. The key no datagram
ever carries is refused before the run instead — see
Capabilities::admit_class.
Emitted once per session per (class, field), on the first unit
that reaches the rule. Not once per stream and not once per unit: it
is a statement about a profile against a draft, both of which are
fixed for the session. The falsifiable companion an author reads is
ShapeStats::default_class, which is where the units went.
A rule whose key is present but out of range reports nothing — that is a rule working.
Fields
class: StringThe ClassRule::name that
cannot fire.
field: MatcherFieldThe key it named that the wire did not carry.
draft: DraftVersionThe draft the session is running as, which is half of why the key is absent.
ShapeBurstBelowUnit
A class’s BucketConfig::burst_bytes
is smaller than the units it is being asked to pace, so its
configured rate never binds and every unit leaves at its max_hold
clamp instead.
A token bucket can never grant a unit larger than the whole bucket —
there is no amount of refilling that covers it — so a burst below one
object turns a rate into a metronome running at
queue depth / max_hold. Measured: rate_bps of 1 000 000 with
burst_bytes of 100 delivered 1000-byte objects at exactly the
clamp, a figure the configuration never mentions.
This is the report that makes that case distinguishable. Without
it the symptoms are HoldClamped on every unit and a rising
tokens_exhausted_episodes — which is also precisely what a class
that is genuinely rate-limited produces, so an author who wrote a rate
and left the burst at its default read a plausible-looking starved
class and no indication that their number had been ignored.
A class whose rate_bps is Some(0) never reaches this report. That
is a class configured to stop, doing what it was asked; only a rate
that was asked for and cannot be applied is a fault.
It cannot be rejected when the profile is built:
ShapeProfile::try_new has the
burst but not the object sizes, and the sizes are half the comparison.
Emitted once per session per class. The burst is a property of the
profile, so every stream carrying the class reproduces it and every
unit of it re-triggers it; the running figures beside this report are
the class’s own tokens_exhausted_episodes and one HoldClamped per
clamped unit.
Fields
class: StringThe ClassRule::name whose rate
is not being applied.
ShapeUnpacedObject
An object too large for the framer to buffer was forwarded without passing any token bucket, so the named class’s configured rate was exceeded by exactly that object.
Shaping is per unit and a unit is classified from its ObjectMeta.
An object beyond
FramerConfig::max_buffered_object_bytes
has none — the framer streams it through rather than measuring it —
so no rule can claim it, no bucket can charge it, and the release seam
grants it unconditionally. Measured: a 4 MiB object crossed in 800 ms
against a class holding a bucket configured at zero bytes per second.
This report is what keeps that from being a silent breach. class is
the class the stream’s classified units are charged to, which is the
rate the escaping object was nominally under — an empty string on a
stream that has not classified anything yet, matching the unnamed rows
on ShapeStats. The bytes themselves are
accounted on
ShapeStats::unshapeable, so
the conservation identity still closes; what was missing was anything
naming the class whose ceiling they went over.
Paired with, and deliberately distinct from,
ObjectNotAddressable: that one says
the hook cannot address the object, this one says the shaper did
not pace it. A session with no
ShapeProfile emits the first and never
the second, because there is no rate to exceed.
Emitted at most once per stream, on the first such object, for the
reason ObjectNotAddressable is capped the same way: a stream of
large objects would otherwise emit one event each.
Fields
class: StringThe ClassRule::name this
stream’s classified units are charged to, or an empty string when
no rule has claimed one yet.
ClassChangedMidStream
Two units on one destination stream resolved to different shaping classes, so that stream’s throughput is decided by whichever class is at its head rather than by any one class’s bucket.
This is not a defect and not a refusal — it is what per-unit classification over a single per-stream FIFO means. Reordering the queue by class is forbidden outright: object IDs are delta-encoded on the wire on drafts 14-19, and the framer’s only re-encoding primitive handles removal, not reordering. So the head gates everything behind it whatever class those units are, and this report is what keeps that from being mistaken for the shaping the author configured.
Emitted once per stream, on the first disagreement. The running
figures beside it are
ShapeStats::streams_with_mixed_classes
and, per class,
ClassStats::starved_behind_other_class
— which is deliberately not
ClassStats::tokens_exhausted_episodes:
two causes of waiting, two counters.
This report’s leg and the proxy-wide counter’s cell disagree by
exactly one leg, on purpose. This event answers the departing
connection, because the stream whose throughput is now shared is the
one being written to; the same occurrence is charged to the arrival
cell of
ProxyStats::per_leg, so it sits
beside the bytes_shaped that explains it. Correlating an event with
a cell means expecting the two labels to differ — see
DirectionStats::streams_with_mixed_classes,
which states it from the other side.
Keyed on StreamKey and not on stream_id, for the reason
SerializeTargetUnknown is: on the
WebTransport arm every transport stream id is the constant 0, so a
report identified by stream_id alone would make “once per stream”
read as “once per session” — and a test asserting one event per mixed
stream would pass on QUIC and be unwritable on WT. The transport id
rides along because it is what correlates this report with every
other event in this enum.
Trait Implementations§
Source§impl Clone for ImpairmentKind
impl Clone for ImpairmentKind
Source§fn clone(&self) -> ImpairmentKind
fn clone(&self) -> ImpairmentKind
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more