Skip to main content

ImpairmentKind

Enum ImpairmentKind 

Source
#[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
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

FramerBypass

The framer stopped parsing a stream, so no object on it is addressable. Emitted exactly once per stream.

Fields

§stream_id: u64

The source stream.

§draft: DraftVersion

The draft it was parsed as.

§reason: BypassReason

Why parsing stopped.

§

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.

Fields

§stream_id: u64

The source stream.

§total: u64

Unaddressable objects on this stream so far.

§

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: u64

The 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.

§total: u64

Frames refused on this direction when the report went out.

§

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.

Fields

§stream_id: u64

The source stream.

§

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

§requested: Option<Duration>

What was asked for, or None when the wait was unbounded.

§applied: Duration

What was applied: the max_hold ceiling.

§

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:

  1. a non-reset read failure on the source half, which is the last read that stream will ever do;
  2. 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.

Fields

§error: String

The failure that caused it.

§

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

§stream_id: u64

The source stream.

§bytes: usize

How many bytes could not be confirmed delivered.

§

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.

Fields

§error: String

The transport’s error.

§

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: TimerBackend

The backend the release thread resolved to.

§detail: Option<String>

Reserved for a future backend that can fail to initialise. Always None in 0.4.0: the only coarse backend is the one MOQTAP_RELEASE_TIMER forces, and nothing failed for it to describe.

§

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

§stream_id: u64

The source stream.

§reason: BypassReason

Why the framer gave up.

§code: u64

The code the destination was reset with (0x0).

§

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.

Fields

§key: StreamKey

The stream that asked to be serialized.

§target: StreamKey

The target it named.

§

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: String

The ClassRule::name that cannot fire.

§field: MatcherField

The key it named that the wire did not carry.

§draft: DraftVersion

The 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: String

The ClassRule::name whose rate is not being applied.

§burst_bytes: u64

The bucket cap, as configured.

§unit_bytes: u64

The wire size of the unit it could not cover — the other half of the comparison, so the report is actionable without a second measurement.

§

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: String

The ClassRule::name this stream’s classified units are charged to, or an empty string when no rule has claimed one yet.

§stream_id: u64

The source stream.

§bytes: u64

Wire bytes of the unpaced chunk that triggered the report.

§

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.

Fields

§key: StreamKey

Session-local identity of the destination stream carrying both classes. Unique even on the WebTransport arm.

§stream_id: u64

Transport stream id, for correlation with the other events in this enum. 0 for every WebTransport stream; key is what identifies.

Trait Implementations§

Source§

impl Clone for ImpairmentKind

Source§

fn clone(&self) -> ImpairmentKind

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ImpairmentKind

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for ImpairmentKind

Source§

impl PartialEq for ImpairmentKind

Source§

fn eq(&self, other: &ImpairmentKind) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for ImpairmentKind

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more