Skip to main content

moqtap_proxy/
event.rs

1//! Proxy event types emitted by the inline parser.
2
3use std::net::SocketAddr;
4use std::time::{Duration, Instant};
5
6use moqtap_codec::dispatch::{
7    AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnyObjectHeader, AnySubgroupHeader,
8};
9use moqtap_codec::version::DraftVersion;
10
11use crate::capability::{ActionKind, Refusal, Site};
12use crate::shape::StreamKey;
13use crate::types::{Leg, ObjectMeta};
14
15pub use crate::types::ProxySide;
16
17/// Unique session identifier (monotonic counter assigned by the proxy).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct SessionId(pub u64);
20
21/// The kind of data stream header parsed from a unidirectional stream.
22#[derive(Debug, Clone)]
23pub enum DataStreamHeaderKind {
24    /// Subgroup stream header.
25    Subgroup(AnySubgroupHeader),
26    /// Fetch response stream header.
27    Fetch(AnyFetchHeader),
28}
29
30/// Events emitted by the proxy during stream forwarding.
31///
32/// Marked `#[non_exhaustive]`: observers must carry a catch-all arm, so
33/// that adding an event is not a breaking change.
34///
35/// # Asserting on events — destructure, do not compare
36///
37/// `ProxyEvent` derives **`Debug` and `Clone` only**. It carries decoded
38/// codec types ([`AnyControlMessage`], [`AnyDatagramHeader`], …) that do
39/// not implement `PartialEq`, so there is no `assert_eq!(event,
40/// ProxyEvent::Something { .. })` to write and there will not be one:
41/// deriving `PartialEq` here would require it on every draft's message
42/// enum.
43///
44/// Every assertion in this workspace therefore has the same two-step
45/// shape — **count with `matches!`, then destructure and compare the
46/// payload**:
47///
48/// ```
49/// use moqtap_proxy::event::{Effect, ProxyEvent};
50///
51/// fn exactly_one_replacement(events: &[ProxyEvent]) {
52///     // 1. count the variant with `matches!`
53///     let applied: Vec<&ProxyEvent> = events
54///         .iter()
55///         .filter(|e| matches!(e, ProxyEvent::ActionApplied { .. }))
56///         .collect();
57///     assert_eq!(applied.len(), 1);
58///
59///     // 2. destructure, then compare the payload with `assert_eq!`
60///     let ProxyEvent::ActionApplied { effect, .. } = applied[0] else {
61///         unreachable!("filtered above")
62///     };
63///     assert_eq!(*effect, Effect::Replaced { bytes: 4 });
64/// }
65/// # let _ = exactly_one_replacement;
66/// ```
67///
68/// Step 2 works because the *payload* types added in 0.4.0 — [`Effect`],
69/// [`ImpairmentKind`], [`Refusal`], [`Site`] and [`ActionKind`] — do
70/// derive `PartialEq` and `Eq`. The boundary is exactly at the event: the
71/// event is destructured, what comes out of it is compared.
72///
73/// `ProxyEvent` and every one of those payload enums is
74/// `#[non_exhaustive]`, so a `match` over any of them from outside this
75/// crate needs a catch-all arm; `matches!` supplies one for free, which
76/// is the other reason it is the recommended spelling.
77#[derive(Debug, Clone)]
78#[non_exhaustive]
79pub enum ProxyEvent {
80    /// A new client connected and a session was created.
81    SessionStarted {
82        /// The session identifier.
83        session_id: SessionId,
84        /// The client's remote address.
85        client_addr: SocketAddr,
86        /// The transport the client chose via ALPN — e.g. `"QUIC"` or
87        /// `"WebTransport"`. Observers use this to label per-client
88        /// sessions; the proxy itself accepts either simultaneously.
89        client_transport: String,
90    },
91
92    /// A setup message (CLIENT_SETUP or SERVER_SETUP) was observed.
93    SetupMessage {
94        /// The session identifier.
95        session_id: SessionId,
96        /// Which side sent the message.
97        side: ProxySide,
98        /// The decoded setup message.
99        message: AnyControlMessage,
100    },
101
102    /// A control message was parsed from the forwarded byte stream.
103    ControlMessage {
104        /// The session identifier.
105        session_id: SessionId,
106        /// Which side sent the message.
107        side: ProxySide,
108        /// The decoded control message.
109        message: AnyControlMessage,
110    },
111
112    /// A data stream header was parsed from a unidirectional stream.
113    DataStreamHeader {
114        /// The session identifier.
115        session_id: SessionId,
116        /// Which side opened the stream.
117        side: ProxySide,
118        /// The parsed header.
119        header: DataStreamHeaderKind,
120    },
121
122    /// An object header was parsed on a data stream.
123    ///
124    /// Never emitted: `AnyObjectHeader` has no variants past draft-13, so
125    /// this could only ever report objects on the oldest drafts, and even
126    /// there it reported a header without consuming the payload behind it.
127    /// [`ProxyEvent::Object`] replaces it and covers drafts 07-19.
128    #[deprecated(since = "0.3.0", note = "superseded by ProxyEvent::Object")]
129    ObjectHeader {
130        /// The session identifier.
131        session_id: SessionId,
132        /// Which side sent the object.
133        side: ProxySide,
134        /// The parsed object header.
135        header: AnyObjectHeader,
136    },
137
138    /// A complete object was framed on a data stream.
139    ///
140    /// Emitted once per object, in stream order, on every draft 07-19.
141    /// Objects the framer could not address individually — one larger than
142    /// its buffer cap, or a stream it stopped parsing — produce no event;
143    /// their bytes are still forwarded unchanged.
144    Object {
145        /// The session identifier.
146        session_id: SessionId,
147        /// Which side sent the object.
148        side: ProxySide,
149        /// The object's identity and framing, without its payload.
150        meta: ObjectMeta,
151    },
152
153    /// A datagram arrived and its header was parsed.
154    ///
155    /// An **observation of what came in**, emitted where the decode happens
156    /// — before any hook is consulted and before the datagram is handed to
157    /// the far transport. It is deliberately not a delivery receipt, and
158    /// calling it one would be wrong in two directions at once: a hook that
159    /// drops or replaces the datagram still produces this event, and the
160    /// forward itself can be refused, which is reported separately as
161    /// [`ImpairmentKind::DatagramNotSent`] or, when somebody acted on it,
162    /// as [`ProxyEvent::ActionFailed`].
163    ///
164    /// It sits with [`ProxyEvent::Object`] and
165    /// [`ProxyEvent::ControlMessage`] rather than with the action events:
166    /// all three say a unit was seen and understood, and none of them says
167    /// where it ended up. Withholding it until after the send would make an
168    /// undecodable-or-dropped datagram invisible, which is exactly the case
169    /// an observer is usually watching for.
170    ///
171    /// Emitted only when an observer is attached — the decode is skipped
172    /// entirely otherwise — and only when the header actually decoded. A
173    /// datagram whose header this crate cannot read produces no event and is
174    /// still forwarded.
175    Datagram {
176        /// The session identifier.
177        session_id: SessionId,
178        /// Which side sent the datagram.
179        side: ProxySide,
180        /// The parsed datagram header.
181        header: AnyDatagramHeader,
182        /// Size of the datagram payload in bytes.
183        payload_len: usize,
184    },
185
186    /// A bidirectional stream was opened or accepted.
187    BiStreamOpened {
188        /// The session identifier.
189        session_id: SessionId,
190        /// Which side opened the stream.
191        side: ProxySide,
192    },
193
194    /// A unidirectional stream was opened or accepted.
195    UniStreamOpened {
196        /// The session identifier.
197        session_id: SessionId,
198        /// Which side opened the stream.
199        side: ProxySide,
200    },
201
202    /// Inline parse failed (non-fatal — bytes are still forwarded).
203    ParseError {
204        /// The session identifier.
205        session_id: SessionId,
206        /// Which side the error occurred on.
207        side: ProxySide,
208        /// Description of the parse error.
209        error: String,
210    },
211
212    /// A stream direction ended cleanly — the peer sent a FIN and every
213    /// byte it wrote was forwarded.
214    ///
215    /// An abnormal end is reported as [`ProxyEvent::StreamReset`]
216    /// instead, never as this event.
217    StreamClosed {
218        /// The session identifier.
219        session_id: SessionId,
220        /// Which side closed.
221        side: ProxySide,
222    },
223
224    /// A peer tore a stream down abnormally — either `RESET_STREAM` from
225    /// the sender or `STOP_SENDING` from the receiver.
226    ///
227    /// This reports the *observation*. The proxy mirrors the teardown
228    /// onto the opposite stream with the same application error code;
229    /// when that stream has already gone away the mirror is a no-op, and
230    /// the event is still emitted so the teardown is never invisible.
231    ///
232    /// Distinct from [`ProxyEvent::StreamClosed`], which reports an
233    /// orderly FIN. An observer that sees this knows the stream was
234    /// abandoned and any data on it may be truncated.
235    ///
236    /// Not emitted for streams the proxy itself tears down at session
237    /// shutdown — those end with a FIN.
238    StreamReset {
239        /// The session identifier.
240        session_id: SessionId,
241        /// The side the teardown was observed on. For a `RESET_STREAM`
242        /// this is the ingress side the bytes were arriving on; for a
243        /// `STOP_SENDING` it is the egress side they were leaving on.
244        side: ProxySide,
245        /// The peer's application error code, forwarded verbatim.
246        code: u64,
247    },
248
249    /// The session ended.
250    SessionEnded {
251        /// The session identifier.
252        session_id: SessionId,
253        /// Reason for session termination.
254        reason: String,
255    },
256
257    // ── 0.4.0: the action engine ───────────────────────────────────────
258    /// An action was executed and the wire changed.
259    ///
260    /// Emitted once the engine has **admitted** the action and produced the
261    /// bytes or the plan for it: every guard has run, every refusal has
262    /// already been taken, and nothing between here and the wire can decline
263    /// it on this proxy's behalf. `effect` is what was decided, down to the
264    /// byte count.
265    ///
266    /// # It is not a delivery receipt, and one case makes that visible
267    ///
268    /// The engine decides; the forwarding task then hands the result to the
269    /// transport. That transport can still say no — a replacement datagram
270    /// above the path MTU is the case that exists — and when it does, this
271    /// event has already been emitted and is followed by
272    /// [`ProxyEvent::ActionFailed`] naming the same site and action. **Both
273    /// events are emitted for that attempt**, in that order, and the pair is
274    /// the whole truth: the action was admitted, and it did not reach the
275    /// peer.
276    ///
277    /// Folding the two into one report was rejected twice over. Withholding
278    /// this event until the write returned would mean an action that was
279    /// admitted, queued behind a `Delay`, and lost at teardown reported
280    /// nothing at all — and it is precisely the admitted-then-lost case that
281    /// [`ImpairmentKind::QueuedBytesAtTeardown`] is paired against. Reporting
282    /// only the failure would lose which action was taken, since a refusal
283    /// and a rejection name different things.
284    ///
285    /// So the reading is: this event means *the engine did it*. An observer
286    /// that needs *the peer got it* must also watch for `ActionFailed`, and
287    /// for the impairments that report queued bytes.
288    ///
289    /// A proxy-initiated reset appears here, **not** as
290    /// [`ProxyEvent::StreamReset`], which continues to mean an observed
291    /// peer teardown.
292    ///
293    /// # Cardinality under `Delay` and `Hold`
294    ///
295    /// A deferred action emits **two** `ActionApplied` events, not one,
296    /// and they are distinguishable by `action`:
297    ///
298    /// 1. at the decision, `{ action: Delay | Hold, effect: Queued {
299    ///    release_at } }` — the modifier was accepted and the unit is in
300    ///    the queue;
301    /// 2. at the release, `{ action: <the inner kind>, effect: <what the
302    ///    inner action did> }` — e.g. `{ action: Replace, effect:
303    ///    Replaced { bytes } }`.
304    ///
305    /// One event would force a choice between reporting the delay and reporting
306    /// the effect, and a queued unit that is later lost at teardown would have
307    /// reported a `Replaced` that never happened. Two events keep *the engine
308    /// accepted this* and "the wire changed" separately falsifiable, which is
309    /// what [`ImpairmentKind::QueuedBytesAtTeardown`] is paired against.
310    ///
311    /// So the count to assert is *exactly one `ActionApplied` per
312    /// (attempt, phase)*: one for a non-deferred action, two for a
313    /// deferred one.
314    ActionApplied {
315        /// The session identifier.
316        session_id: SessionId,
317        /// The side the unit arrived on.
318        side: ProxySide,
319        /// The source stream, when there is one.
320        stream_id: Option<u64>,
321        /// Where the decision was taken.
322        site: Site,
323        /// What was executed.
324        action: ActionKind,
325        /// What actually happened.
326        effect: Effect,
327    },
328
329    /// An action could not be executed. Emitted **per attempt**, and the
330    /// unit is forwarded unchanged.
331    ///
332    /// The engine declined *before* anything reached the transport: the
333    /// wire carries exactly what it would have carried with no hook at
334    /// all. This is the pre-admission half of the two failure reports —
335    /// [`ProxyEvent::ActionFailed`] is the post-admission one, where the
336    /// engine accepted the action and the transport rejected it.
337    ActionRefused {
338        /// The session identifier.
339        session_id: SessionId,
340        /// The side the unit arrived on.
341        side: ProxySide,
342        /// The source stream, when there is one.
343        stream_id: Option<u64>,
344        /// Where the decision was taken.
345        site: Site,
346        /// What was attempted.
347        action: ActionKind,
348        /// Why it was refused.
349        refusal: Refusal,
350    },
351
352    /// An action was admitted but the transport rejected it. The session
353    /// survives.
354    ///
355    /// Emitted **per failed attempt**. The unit is not forwarded — the
356    /// transport already declined it — and forwarding continues on
357    /// everything else.
358    ///
359    /// Reaching this means the capability table admitted the action and
360    /// the path disagreed; a replacement datagram above the path MTU is
361    /// the case that exists in 0.4.0. Neither
362    /// [`ProxyEvent::ActionApplied`] nor [`ProxyEvent::ActionRefused`]
363    /// can carry it on its own: the action was admitted, so refusing it
364    /// after the fact would be a lie, and it did not reach the peer, so
365    /// reporting only that it applied would be a bigger one.
366    ///
367    /// It is **paired with** `ActionApplied`, not exclusive of it. The
368    /// engine admits, emits `ActionApplied`, and hands the bytes on; the
369    /// transport then declines them and this follows. An attempt that
370    /// reaches here therefore contributes two events, in that order. It is
371    /// exclusive of [`ProxyEvent::ActionRefused`], which is the other
372    /// half of the same split: a refused unit is forwarded unchanged and a
373    /// transport failure on *those* bytes is
374    /// [`ImpairmentKind::DatagramNotSent`], because nobody's action was in
375    /// flight.
376    ///
377    /// Distinct from [`ImpairmentKind::DatagramNotSent`], which is the
378    /// same transport failure on a unit **nobody acted on** — there is no
379    /// `site` and no `action` to name there, and this variant requires
380    /// both.
381    ///
382    /// Connection-level errors (`ConnectionLost`, `Connection(_)`) are
383    /// **not** reported here: they still end the session.
384    ActionFailed {
385        /// The session identifier.
386        session_id: SessionId,
387        /// The side the unit arrived on.
388        side: ProxySide,
389        /// Where the decision was taken.
390        site: Site,
391        /// What was executed.
392        action: ActionKind,
393        /// The transport's error.
394        error: String,
395    },
396
397    /// Something reduced what the proxy can do, with no action involved.
398    ///
399    /// Every [`ImpairmentKind`] states its own emission cardinality;
400    /// read it there before asserting a count.
401    ///
402    /// # This is emitted after the thing it reports, never before
403    ///
404    /// Whatever the report describes has already happened by the time the
405    /// observer is called: the reset has been handed to the transport, the
406    /// datagram has been refused, the queue has been abandoned. Nothing in
407    /// this crate emits one of these on the way *into* an operation that
408    /// could still be declined.
409    ///
410    /// That ordering is what makes the event stream a record rather than an
411    /// intention. Reversed, an impairment raised before a step that a guard
412    /// then refuses is an observer told about a loss that did not occur —
413    /// and there is no later event that retracts it, because this variant
414    /// has no counterpart to [`ProxyEvent::ActionFailed`]. An observer may
415    /// therefore treat every one of these as a fact about the past.
416    ///
417    /// The one place the rule is visibly *not* the same is
418    /// [`ProxyEvent::ActionApplied`], which is emitted when the engine
419    /// admits an action and before the caller hands the bytes to the
420    /// transport; that pairing is covered in its own rustdoc and is why
421    /// `ActionFailed` exists.
422    Impairment {
423        /// The session identifier.
424        session_id: SessionId,
425        /// The side the reporting task was forwarding *from* — the
426        /// direction bytes were arriving on, not necessarily the direction
427        /// the impairment was felt in. Read `leg` for that.
428        side: ProxySide,
429        /// Which of the proxy's two connections the report is about, or
430        /// `None` when it is about neither.
431        ///
432        /// # Not derivable from `side`, which is why it is here
433        /// `side` names the direction the reporting task reads from, so it
434        /// answers *where did these bytes come from*. Most of what
435        /// [`ImpairmentKind`] reports is a failure to *write*: a queue that
436        /// could not be flushed, a datagram the far transport refused, a
437        /// destination stream that had to be reset. Those belong to the
438        /// **opposite** connection from the one the bytes arrived on, and a
439        /// reader who mapped `side` to a connection would attribute every one
440        /// of them to the wrong leg — silently, and in a way that looks
441        /// entirely plausible in a log.
442        ///
443        /// So the two are split, exactly as
444        /// [`ProxyStats`](crate::shape::ProxyStats) splits what a leg read
445        /// from what it wrote:
446        ///
447        /// * **the arriving connection** for
448        ///   [`ImpairmentKind::FramerBypass`],
449        ///   [`ImpairmentKind::ObjectNotAddressable`] and
450        ///   [`ImpairmentKind::ControlFrameNotDecodable`] — all three are a
451        ///   parser giving up on bytes that came *in*, and none of them
452        ///   says anything about what could be written;
453        /// * **the departing connection** for
454        ///   [`ImpairmentKind::EgressQueueFull`],
455        ///   [`ImpairmentKind::HoldClamped`],
456        ///   [`ImpairmentKind::QueuedBytesAtTeardown`],
457        ///   [`ImpairmentKind::DatagramNotSent`],
458        ///   [`ImpairmentKind::ControlStreamTruncated`],
459        ///   [`ImpairmentKind::ElideFixupLost`],
460        ///   [`ImpairmentKind::ShapeUnpacedObject`],
461        ///   [`ImpairmentKind::ClassChangedMidStream`] and
462        ///   [`ImpairmentKind::SerializeTargetUnknown`] — every one of them
463        ///   is about bytes the proxy was trying to place on the far side;
464        /// * **`None`** for
465        ///   [`ImpairmentKind::CoarseReleaseTimer`],
466        ///   [`ImpairmentKind::ShapeRuleUnmatchable`] and
467        ///   [`ImpairmentKind::ShapeBurstBelowUnit`]. These three compare a
468        ///   *profile* against the sizes it is asked to pace, or the
469        ///   process against its host. None of them is a property of a
470        ///   connection, and all three are equally true of both legs.
471        ///   `None` says that in the type instead of picking whichever leg
472        ///   the reporting task happened to be on.
473        ///
474        /// A caller that wants *which connection is unhealthy* reads this field
475        /// and skips the `None`s. A caller that wants *which direction was
476        /// being forwarded when this was noticed* reads `side`.
477        leg: Option<Leg>,
478        /// What happened.
479        kind: ImpairmentKind,
480    },
481
482    /// The egress shaper acted on a unit **as configured**.
483    ///
484    /// This is the product working, not an impairment: a configured drop is
485    /// the tool doing what it was told, where an
486    /// [`ImpairmentKind`] is the tool declining to. It is also the only
487    /// event that can carry a class label, because a class is a shaping
488    /// concept and nothing else in this enum has one.
489    ///
490    /// **Cardinality: once per stream per distinct `outcome`.** Running
491    /// totals live in [`ShapeStats`](crate::shape::ShapeStats) — a
492    /// per-object event would drown an observer at line rate, which is the
493    /// same reason [`ImpairmentKind::ObjectNotAddressable`] carries a total
494    /// instead of firing per object.
495    Shaped {
496        /// The session identifier.
497        session_id: SessionId,
498        /// The side the unit arrived on.
499        side: ProxySide,
500        /// Session-local identity. Unique even on the WebTransport arm,
501        /// where every transport stream id is the constant `0`.
502        key: StreamKey,
503        /// Transport stream id, for correlation with the other events in
504        /// this enum. **`0` for every WebTransport stream**; `key` is what
505        /// identifies.
506        stream_id: u64,
507        /// The class the unit resolved to, or an empty string for a unit
508        /// that matched no rule and for an outcome that is about the stream
509        /// rather than about a unit.
510        class: String,
511        /// What the shaper did.
512        outcome: ShapeOutcome,
513    },
514
515    /// The egress shaper discarded a **datagram** as configured.
516    ///
517    /// [`Self::Shaped`]'s datagram sibling, and separate from it because a
518    /// datagram belongs to no stream: `Shaped` carries a [`StreamKey`] and a
519    /// transport stream id, and there is nothing honest to put in either.
520    /// Merging the two by making those fields optional would put an
521    /// `Option` on the far commoner event to describe the rarer one.
522    ///
523    /// **Cardinality: once per forwarding direction per distinct
524    /// `outcome`**, which is the same rule [`Self::Shaped`] states per
525    /// stream — the direction is a datagram's whole scope. Running totals
526    /// live in [`ShapeStats`](crate::shape::ShapeStats).
527    ShapedDatagram {
528        /// The session identifier.
529        session_id: SessionId,
530        /// The side the datagram arrived on.
531        side: ProxySide,
532        /// The class the datagram resolved to, or an empty string for one
533        /// that matched no rule.
534        class: String,
535        /// What the shaper did.
536        outcome: ShapeOutcome,
537    },
538}
539
540/// What [`ProxyEvent::Shaped`] reports the shaper did.
541///
542/// `#[non_exhaustive]`; derives `PartialEq`/`Eq` like [`Effect`] and
543/// [`ImpairmentKind`], so a test destructures the event and compares this.
544#[derive(Debug, Clone, PartialEq, Eq)]
545#[non_exhaustive]
546pub enum ShapeOutcome {
547    /// A unit was discarded by
548    /// [`Overflow::DropTail`](crate::shape::Overflow::DropTail).
549    ///
550    /// The unit went through the framer's elide path, so absolute object
551    /// IDs on drafts 14-21 stay correct; a unit an elide guard refused was
552    /// admitted instead of dropped and is reported as a refusal, not here.
553    Dropped,
554    /// A queued unit outlived `max_hold` under
555    /// [`Expiry::ResetStream`](crate::shape::Expiry::ResetStream).
556    ///
557    /// Decided at release time rather than at admission: the queue notices
558    /// the overrun when it next looks at its head, replaces everything it
559    /// was holding with the reset the policy asked for, and reports this.
560    /// So the event says the whole destination stream was abandoned, not
561    /// that one unit was — which is why it carries an empty `class` for the
562    /// same reason [`Self::StreamReset`] does.
563    ///
564    /// **Emitted once per stream.** The queue is gone after the first one,
565    /// so there is nothing left to expire.
566    ///
567    /// This variant read "no producer yet" for as long as expiry was decided
568    /// nowhere; it now has one, and asserting on it is
569    /// `actions_shaping.rs`'s business rather than a promise waiting to be
570    /// kept.
571    Expired,
572    /// A datagram was discarded because its class's bucket had no tokens
573    /// for it.
574    ///
575    /// **Policing rather than shaping**, and the difference is that there is
576    /// no queue: a datagram arriving over its class's configured rate is
577    /// dropped at admission rather than held until the tokens arrive. That
578    /// is the only sound answer for this carrier — a queue would impose an
579    /// order the protocol does not have, and a datagram has no successor
580    /// whose framing depends on it, which is what makes discarding one
581    /// harmless where discarding a queued stream unit is not.
582    ///
583    /// Carried only by [`ProxyEvent::ShapedDatagram`]. [`Self::Dropped`] is
584    /// the stream carrier's answer and says something different: that an
585    /// overflow policy discarded a unit the queue had no room for.
586    Policed,
587    /// The destination stream was abandoned by an overflow or expiry
588    /// policy.
589    StreamReset {
590        /// The application error code it was reset with.
591        code: u64,
592    },
593}
594
595/// What an executed action actually did.
596///
597/// Unlike [`ProxyEvent`] this derives `PartialEq` and `Eq`: destructure
598/// the event, then `assert_eq!` on what comes out. See
599/// [`ProxyEvent`]'s own rustdoc for the shape.
600#[derive(Debug, Clone, PartialEq, Eq)]
601#[non_exhaustive]
602pub enum Effect {
603    /// The original bytes were forwarded.
604    ForwardedVerbatim,
605    /// Replacement bytes were forwarded.
606    Replaced {
607        /// How many bytes were written.
608        bytes: usize,
609    },
610    /// An object was removed. `renumbered_successor` is `true` when the next
611    /// object on the stream has to be re-encoded against the one now in
612    /// front of it: its leading ID varint rewritten on a subgroup stream of
613    /// drafts 14-21, its whole framing re-encoded on a fetch stream of
614    /// drafts 15-21.
615    ///
616    /// It says a fix-up is **owed**, not that the bytes moved. A survivor
617    /// that already stated everything it needed comes through unchanged, and
618    /// the debt is settled all the same.
619    Elided {
620        /// Whether a successor fix-up is now pending.
621        renumbered_successor: bool,
622    },
623    /// The unit was queued for later release.
624    Queued {
625        /// When it is due.
626        release_at: Instant,
627    },
628    /// A prefix was written and the stream reset.
629    ///
630    /// `forwarded` counts bytes **handed to the transport**, not bytes the
631    /// peer observed. quinn clears the peer's receive assembler when the
632    /// reset is processed, so the peer may observe fewer — never more.
633    Truncated {
634        /// Bytes of this unit written before the reset.
635        forwarded: usize,
636        /// The reset code, as the action stated it.
637        code: u64,
638        /// `false` when the draft defines no stream-reset code vocabulary
639        /// (drafts 07-10), so `code` is a choice rather than a claim.
640        code_defined: bool,
641    },
642    /// The destination stream was reset.
643    StreamReset {
644        /// The reset code.
645        code: u64,
646        /// `false` when the draft defines no stream-reset code vocabulary
647        /// (drafts 07-10), so `code` is a choice rather than a claim.
648        code_defined: bool,
649    },
650    /// The stream was never forwarded.
651    StreamRejected {
652        /// The code the source was stopped with.
653        code: u64,
654    },
655    /// A session close was requested.
656    SessionClosing {
657        /// The session termination code.
658        code: u32,
659    },
660    /// The unit was not forwarded.
661    Dropped,
662}
663
664/// A reduction in what the proxy can do or observe.
665///
666/// **Every variant states its emission cardinality**, because the counts
667/// differ by an order of magnitude between them — some fire once per
668/// session, some once per stream, and some once per unit — and a test
669/// that assumes "exactly one" against a per-unit variant is a test that
670/// goes red for the wrong reason. Where a variant is capped below its
671/// natural rate, a counter on
672/// [`crate::instrument::Counters`] carries the running total instead.
673///
674/// Like [`Effect`], this derives `PartialEq` and `Eq`; the event that
675/// carries it does not.
676///
677/// # Every variant here has a producer
678///
679/// None of these is reserved, aspirational or waiting for a call site: each
680/// one is emitted by code in this crate, and the emission happens **after**
681/// what it reports — see [`ProxyEvent::Impairment`] for why that ordering is
682/// the whole value of the surface.
683///
684/// One is harder to reach than the rest and says so on itself:
685/// [`Self::CoarseReleaseTimer`] needs `MOQTAP_RELEASE_TIMER=condvar`,
686/// because every default release backend is high-resolution. That is a
687/// diagnostic override rather than a fallback, and the variant exists so a
688/// run made under it cannot quote delays it was unable to honour.
689///
690/// A variant added here is forced to answer one more question before it
691/// compiles: which of the proxy's two connections it is about. The mapping
692/// that answers it matches exhaustively with no catch-all, so a new report
693/// stops the crate building rather than defaulting to a leg somebody else
694/// chose.
695#[derive(Debug, Clone, PartialEq, Eq)]
696#[non_exhaustive]
697pub enum ImpairmentKind {
698    /// The framer stopped parsing a stream, so no object on it is
699    /// addressable. Emitted exactly once per stream.
700    FramerBypass {
701        /// The source stream.
702        stream_id: u64,
703        /// The draft it was parsed as.
704        draft: DraftVersion,
705        /// Why parsing stopped.
706        reason: crate::types::BypassReason,
707    },
708    /// An object exceeded the framer's buffer cap and was streamed through
709    /// without being addressable.
710    ///
711    /// Emitted **at most once per stream**, on the first such object, and
712    /// carries the running count. Every other `ImpairmentKind` states its
713    /// cardinality; this one did not, and it fires per object — a stream
714    /// of large objects under a low `max_buffered_object_bytes` would
715    /// otherwise emit one event per object and swamp an observer written
716    /// against the once-per-stream cardinality every neighbouring variant
717    /// documents. The count keeps the information: `total` is the number
718    /// of unaddressable objects seen on this stream **at the moment of
719    /// emission**, i.e. `1`, and
720    /// [`crate::instrument::Counters::objects_not_addressable`] is the
721    /// running total that stays accurate afterwards.
722    ObjectNotAddressable {
723        /// The source stream.
724        stream_id: u64,
725        /// Unaddressable objects on this stream so far.
726        total: u64,
727    },
728    /// A control frame's body was refused by the decoder, so the proxy
729    /// forwarded a message it could not read.
730    ///
731    /// The frame's declared length was intact — that is what let the parser
732    /// find the frame behind it — and only the message inside it failed to
733    /// decode. The bytes reach the peer regardless, in the position they
734    /// held: on the observation-only control pipe they were forwarded
735    /// before anything was parsed, and on the mutating pipe, where the
736    /// parser owns the forwarding path, they are written verbatim without
737    /// a hook being consulted. So what a refusal costs is this proxy's
738    /// account of a message, never the message.
739    ///
740    /// That account is the whole product, which is why the loss is
741    /// reported. Without this event a control message the proxy could not
742    /// read is indistinguishable from one the peer never sent, and the two
743    /// call for opposite conclusions. It is not a per-draft hazard: a
744    /// Message Type the configured draft does not assign, a frame whose
745    /// body does not match its declared length, and anything an extension
746    /// adds all take the same path on all the drafts.
747    ///
748    /// # Cardinality: at most once per control stream direction
749    ///
750    /// Emitted on the first refused frame, carrying the count as it stood
751    /// when the report went out. A peer repeating an unassigned Message
752    /// Type would otherwise emit one event per frame, against neighbouring
753    /// variants an observer has been told fire once per stream — the same
754    /// argument [`Self::ObjectNotAddressable`] makes for the same shape of
755    /// hazard.
756    ///
757    /// The running figure that stays accurate afterwards is
758    /// [`crate::instrument::Counters::control_frames_not_decodable`]. That
759    /// counter and this event count different things on purpose: one frame
760    /// refused and forty refused produce one event each and differ by
761    /// thirty-nine there.
762    ControlFrameNotDecodable {
763        /// The Message Type varint the **first** refused frame declared.
764        ///
765        /// The type is read from the frame header, which decoded; nothing
766        /// inside the frame did, so this is the whole of what the refused
767        /// message can still say about itself. A later refusal of a
768        /// different type is counted and not named — see the cardinality
769        /// note above.
770        type_id: u64,
771        /// Frames refused on this direction when the report went out.
772        total: u64,
773    },
774    /// A stream's pending queue reached
775    /// [`crate::action::EgressConfig::max_pending_bytes`], so delay has
776    /// become backpressure. Emitted once per stream, on the transition
777    /// into backpressure — a queue that drains and fills again does not
778    /// report twice.
779    EgressQueueFull {
780        /// The source stream.
781        stream_id: u64,
782    },
783    /// A delay, or a shaped release, was clamped to
784    /// [`crate::action::EgressConfig::max_hold`].
785    ///
786    /// Emitted **once per clamped unit**, not once per stream: a hook
787    /// that returns an over-long `Delay` for every object emits one event
788    /// per object. It is a property of the decision, and the decision is
789    /// taken again for the next unit.
790    ///
791    /// # `requested: None` is an unbounded wait, not a missing figure
792    ///
793    /// A hook's own `Delay { by }` names a duration, so it reports
794    /// `Some(by)`. A shaped release frequently names none: a class whose
795    /// rate is zero, or whose burst is smaller than the unit at the head of
796    /// its queue, has **no** refill instant, and the clamp is the only
797    /// thing that will ever release that unit. `None` is that case, and it
798    /// is the honest report — there is no duration to quote and inventing a
799    /// finite one would be a fabrication.
800    ///
801    /// The unbounded case was once reported as `Duration::MAX`, which
802    /// reaches a log as `18446744073709551615.999999999s`. Two things went
803    /// wrong with that. A reader sees a 584-billion-year request beside a
804    /// 2 s applied one and takes it for an encoding fault in whatever
805    /// rendered it, filing against the wrong component; and the natural
806    /// assertion that a clamp *reduced* the request — `requested > applied`
807    /// — cannot tell that sentinel from a real thirty-second request, so it
808    /// passes either way and certifies nothing.
809    HoldClamped {
810        /// What was asked for, or `None` when the wait was unbounded.
811        requested: Option<Duration>,
812        /// What was applied: the `max_hold` ceiling.
813        applied: Duration,
814    },
815    /// A **control** stream's destination ended with a FIN part-way
816    /// through a message. The proxy does not synthesize a reset there —
817    /// that would be a session-level protocol violation — and it does not
818    /// finish the message either, because completing it would mean
819    /// inventing control-stream bytes neither peer wrote. On a data stream
820    /// the equivalent failure synthesizes a reset, which is what makes the
821    /// truncation visible to the peer; here only this report carries it.
822    ///
823    /// Two things produce it, and `error` says which:
824    ///
825    /// 1. a non-reset read failure on the source half, which is the last
826    ///    read that stream will ever do;
827    /// 2. the drain window of a requested session close expiring while a
828    ///    message was part-written — see
829    ///    [`ProxyControl::close_session`](crate::control::ProxyControl::close_session).
830    ///
831    /// Emitted **at most once per control stream direction**. The two
832    /// producers cannot both fire on one direction: the first returns from
833    /// the pipe, so the second is unreachable afterwards. A session with
834    /// both control directions affected emits two, one per `side`.
835    ///
836    /// The second producer says nothing at all when the proxy cannot tell
837    /// where the message boundaries are — a control stream whose framing
838    /// could not be followed reports no truncation rather than a guessed
839    /// one.
840    ControlStreamTruncated {
841        /// The failure that caused it.
842        error: String,
843    },
844    /// Queued bytes were still pending when the session tore down. They
845    /// were flushed best-effort; `bytes` may not have reached the peer.
846    ///
847    /// Emitted **at most once per stream**, at teardown, and only for a
848    /// stream that still had something queued when teardown began — a
849    /// stream that drained at its release times reports nothing.
850    ///
851    /// `bytes` counts everything the teardown flush could not vouch for:
852    /// what it could not write **and** what it wrote into a transport the
853    /// session was already closing. The second half is not pedantry — a
854    /// `write_all` into quinn returns `Ok` as soon as the bytes are
855    /// buffered, and `Connection::close` discards that buffer, so counting
856    /// only the residue reports zero for precisely the case that loses
857    /// data. A peer that did receive the bytes gets a spurious impairment;
858    /// that is the safe side to err on.
859    QueuedBytesAtTeardown {
860        /// The source stream.
861        stream_id: u64,
862        /// How many bytes could not be confirmed delivered.
863        bytes: usize,
864    },
865    /// A datagram could not be handed to the transport, and the session
866    /// survived.
867    ///
868    /// Emitted **once per rejected datagram** — this is a per-unit
869    /// variant, so a source steadily sending datagrams above the path MTU
870    /// produces one event each. Nothing caps it, because unlike
871    /// [`Self::ObjectNotAddressable`] there is no stream to attribute a
872    /// running total to.
873    ///
874    /// Distinct from [`ProxyEvent::ActionFailed`], which requires a `site`
875    /// and an `action`: this is the un-hooked path, where nobody acted
876    /// and there is nothing to name. Both exist because
877    /// `forward_datagrams` drops the `?` on **every** `send_datagram`
878    /// call, not only the ones behind a hook.
879    DatagramNotSent {
880        /// The transport's error.
881        error: String,
882    },
883    /// The release wheel is not high-resolution on this host, so
884    /// [`crate::action::Action::Delay`] cannot resolve below the ~15.6 ms
885    /// system tick and every delay shorter than it is really a tick.
886    ///
887    /// The default backend is high-resolution on every platform, so in
888    /// 0.4.0 the only way to reach this is
889    /// `MOQTAP_RELEASE_TIMER=condvar` on Windows — the diagnostic
890    /// override, not a fallback. It is reported rather than assumed away
891    /// because a forced backend is still a run whose delays were not
892    /// honoured.
893    ///
894    /// Emitted **once per session**, on that session's first deferred
895    /// release. The measured shortfall is in
896    /// [`crate::instrument::Counters::release_errors`]; this event exists
897    /// so a report cannot quote "5 ms delay applied" while silently
898    /// having been unable to apply it.
899    CoarseReleaseTimer {
900        /// The backend the release thread resolved to.
901        backend: crate::instrument::TimerBackend,
902        /// Reserved for a future backend that can fail to initialise.
903        /// **Always `None` in 0.4.0**: the only coarse backend is the one
904        /// `MOQTAP_RELEASE_TIMER` forces, and nothing failed for it to
905        /// describe.
906        detail: Option<String>,
907    },
908    /// The framer stopped parsing a stream while an elide fix-up was
909    /// still owed, so the remainder of the stream cannot be renumbered.
910    ///
911    /// The destination stream is **reset** rather than forwarded, because
912    /// the alternative is delivering bytes that are known to decode to
913    /// the wrong Object IDs. This is the residual case of the elide
914    /// mechanism, and it is reachable: a mid-stream decode error or an
915    /// unmeasurable oversized object can latch bypass at any point after
916    /// an elide.
917    ///
918    /// Emitted **at most once per stream**, and it is that stream's last
919    /// event: the reset has already been asked for by the time this
920    /// arrives, and the forwarding task returns immediately afterwards.
921    /// The order is that way round on purpose — `code` names the value the
922    /// destination *was* reset with, so an event raised above the reset
923    /// would describe a wire change that had not been made, and this arm
924    /// has no later event to correct it with. It is the
925    /// `fixup_owed == true` half of the one `FramerOut::Bypassed` a
926    /// stream can produce — the half that reports [`Self::FramerBypass`]
927    /// is the other.
928    ElideFixupLost {
929        /// The source stream.
930        stream_id: u64,
931        /// Why the framer gave up.
932        reason: crate::types::BypassReason,
933        /// The code the destination was reset with (`0x0`).
934        code: u64,
935    },
936    /// A [`StreamAction::SerializeAfter`](crate::action::StreamAction::SerializeAfter)
937    /// named a stream there is nothing to wait for, so the stream it was
938    /// returned on proceeded immediately.
939    ///
940    /// Three cases reach it and they are deliberately one report: the target
941    /// never existed, the target had already ended, or the target is this
942    /// stream itself. All three are a hook naming a stream that cannot end
943    /// later than now, and in all three the honest engine behaviour is to
944    /// proceed — a serialize that silently held forever would be a
945    /// `max_hold` stall attributed to the wrong thing.
946    ///
947    /// Emitted **once per stream**. A stream takes at most one serialize
948    /// decision at each of the two stream sites, and a decision that names a
949    /// live target reports nothing at all.
950    ///
951    /// Both fields are [`StreamKey`]s rather than transport ids, because
952    /// attribution is the whole point of the report and the transport id is
953    /// the constant `0` on every WebTransport stream.
954    SerializeTargetUnknown {
955        /// The stream that asked to be serialized.
956        key: StreamKey,
957        /// The target it named.
958        target: StreamKey,
959    },
960    /// A [`ClassRule`](crate::shape::ClassRule) keys on a field the wire
961    /// does not carry on this draft and stream kind, so it can never claim
962    /// a unit and everything it was aimed at falls to the default class.
963    /// This is the report that keeps the rule **a key the wire does not carry
964    /// never matches** from being a silent no-op. `Discipline::StrictPriority`
965    /// is specified on `publisher_priority`, which is `None` on drafts 15-19
966    /// under the default-priority bit — so without this, *starve video while
967    /// audio flows* is a silent no-op on the five newest drafts and the run
968    /// reports success. A rule aimed at
969    /// [`MatchKind::Datagram`](crate::shape::MatchKind::Datagram) reports
970    /// through the same seam, from the datagram forwarder rather than from the
971    /// framer, and reports the one key a datagram can be missing: its priority,
972    /// which drafts 15 and later let a type byte leave off. The key no datagram
973    /// *ever* carries is refused before the run instead — see
974    /// `Capabilities::admit_class`.
975    ///
976    /// Emitted **once per session per `(class, field)`**, on the first unit
977    /// that reaches the rule. Not once per stream and not once per unit: it
978    /// is a statement about a *profile* against a *draft*, both of which are
979    /// fixed for the session. The falsifiable companion an author reads is
980    /// `ShapeStats::default_class`, which is where the units went.
981    ///
982    /// A rule whose key is *present but out of range* reports nothing —
983    /// that is a rule working.
984    ShapeRuleUnmatchable {
985        /// The [`ClassRule::name`](crate::shape::ClassRule::name) that
986        /// cannot fire.
987        class: String,
988        /// The key it named that the wire did not carry.
989        field: crate::shape::MatcherField,
990        /// The draft the session is running as, which is half of why the
991        /// key is absent.
992        draft: DraftVersion,
993    },
994    /// A class's [`BucketConfig::burst_bytes`](crate::shape::BucketConfig::burst_bytes)
995    /// is smaller than the units it is being asked to pace, so its
996    /// configured rate never binds and every unit leaves at its `max_hold`
997    /// clamp instead.
998    ///
999    /// A token bucket can never grant a unit larger than the whole bucket —
1000    /// there is no amount of refilling that covers it — so a burst below one
1001    /// object turns a rate into a metronome running at
1002    /// `queue depth / max_hold`. Measured: `rate_bps` of 1 000 000 with
1003    /// `burst_bytes` of 100 delivered 1000-byte objects at exactly the
1004    /// clamp, a figure the configuration never mentions.
1005    ///
1006    /// **This is the report that makes that case distinguishable.** Without
1007    /// it the symptoms are `HoldClamped` on every unit and a rising
1008    /// `tokens_exhausted_episodes` — which is *also* precisely what a class
1009    /// that is genuinely rate-limited produces, so an author who wrote a rate
1010    /// and left the burst at its default read a plausible-looking starved
1011    /// class and no indication that their number had been ignored.
1012    ///
1013    /// A class whose `rate_bps` is `Some(0)` never reaches this report. That
1014    /// is a class configured to stop, doing what it was asked; only a rate
1015    /// that was asked for and cannot be applied is a fault.
1016    ///
1017    /// It cannot be rejected when the profile is built:
1018    /// [`ShapeProfile::try_new`](crate::shape::ShapeProfile::try_new) has the
1019    /// burst but not the object sizes, and the sizes are half the comparison.
1020    ///
1021    /// Emitted **once per session per class**. The burst is a property of the
1022    /// profile, so every stream carrying the class reproduces it and every
1023    /// unit of it re-triggers it; the running figures beside this report are
1024    /// the class's own `tokens_exhausted_episodes` and one `HoldClamped` per
1025    /// clamped unit.
1026    ShapeBurstBelowUnit {
1027        /// The [`ClassRule::name`](crate::shape::ClassRule::name) whose rate
1028        /// is not being applied.
1029        class: String,
1030        /// The bucket cap, as configured.
1031        burst_bytes: u64,
1032        /// The wire size of the unit it could not cover — the other half of
1033        /// the comparison, so the report is actionable without a second
1034        /// measurement.
1035        unit_bytes: u64,
1036    },
1037    /// An object too large for the framer to buffer was forwarded without
1038    /// passing any token bucket, so the named class's configured rate was
1039    /// exceeded by exactly that object.
1040    ///
1041    /// Shaping is per unit and a unit is classified from its `ObjectMeta`.
1042    /// An object beyond
1043    /// [`FramerConfig::max_buffered_object_bytes`](crate::framer::FramerConfig::max_buffered_object_bytes)
1044    /// has none — the framer streams it through rather than measuring it —
1045    /// so no rule can claim it, no bucket can charge it, and the release seam
1046    /// grants it unconditionally. Measured: a 4 MiB object crossed in 800 ms
1047    /// against a class holding a bucket configured at zero bytes per second.
1048    ///
1049    /// This report is what keeps that from being a silent breach. `class` is
1050    /// the class the stream's *classified* units are charged to, which is the
1051    /// rate the escaping object was nominally under — an empty string on a
1052    /// stream that has not classified anything yet, matching the unnamed rows
1053    /// on [`ShapeStats`](crate::shape::ShapeStats). The bytes themselves are
1054    /// accounted on
1055    /// [`ShapeStats::unshapeable`](crate::shape::ShapeStats::unshapeable), so
1056    /// the conservation identity still closes; what was missing was anything
1057    /// naming the class whose ceiling they went over.
1058    ///
1059    /// Paired with, and deliberately distinct from,
1060    /// [`ObjectNotAddressable`](Self::ObjectNotAddressable): that one says
1061    /// the *hook* cannot address the object, this one says the *shaper* did
1062    /// not pace it. A session with no
1063    /// [`ShapeProfile`](crate::shape::ShapeProfile) emits the first and never
1064    /// the second, because there is no rate to exceed.
1065    ///
1066    /// Emitted **at most once per stream**, on the first such object, for the
1067    /// reason `ObjectNotAddressable` is capped the same way: a stream of
1068    /// large objects would otherwise emit one event each.
1069    ShapeUnpacedObject {
1070        /// The [`ClassRule::name`](crate::shape::ClassRule::name) this
1071        /// stream's classified units are charged to, or an empty string when
1072        /// no rule has claimed one yet.
1073        class: String,
1074        /// The source stream.
1075        stream_id: u64,
1076        /// Wire bytes of the unpaced chunk that triggered the report.
1077        bytes: u64,
1078    },
1079    /// Two units on one destination stream resolved to **different** shaping
1080    /// classes, so that stream's throughput is decided by whichever class is
1081    /// at its head rather than by any one class's bucket.
1082    ///
1083    /// This is not a defect and not a refusal — it is what per-unit
1084    /// classification over a single per-stream FIFO *means*. Reordering the
1085    /// queue by class is forbidden outright: object IDs are delta-encoded on
1086    /// the wire on drafts 14-21, and the framer's only re-encoding primitive
1087    /// handles removal, not reordering. So the head gates everything behind
1088    /// it whatever class those units are, and this report is what keeps that
1089    /// from being mistaken for the shaping the author configured.
1090    ///
1091    /// Emitted **once per stream**, on the first disagreement. The running
1092    /// figures beside it are
1093    /// [`ShapeStats::streams_with_mixed_classes`](crate::shape::ShapeStats::streams_with_mixed_classes)
1094    /// and, per class,
1095    /// [`ClassStats::starved_behind_other_class`](crate::shape::ClassStats::starved_behind_other_class)
1096    /// — which is deliberately *not*
1097    /// [`ClassStats::tokens_exhausted_episodes`](crate::shape::ClassStats::tokens_exhausted_episodes):
1098    /// two causes of waiting, two counters.
1099    ///
1100    /// **This report's `leg` and the proxy-wide counter's cell disagree by
1101    /// exactly one leg, on purpose.** This event answers the departing
1102    /// connection, because the stream whose throughput is now shared is the
1103    /// one being written to; the same occurrence is charged to the *arrival*
1104    /// cell of
1105    /// [`ProxyStats::per_leg`](crate::shape::ProxyStats::per_leg), so it sits
1106    /// beside the `bytes_shaped` that explains it. Correlating an event with
1107    /// a cell means expecting the two labels to differ — see
1108    /// [`DirectionStats::streams_with_mixed_classes`](crate::shape::DirectionStats::streams_with_mixed_classes),
1109    /// which states it from the other side.
1110    ///
1111    /// Keyed on [`StreamKey`] and **not** on `stream_id`, for the reason
1112    /// [`SerializeTargetUnknown`](Self::SerializeTargetUnknown) is: on the
1113    /// WebTransport arm every transport stream id is the constant `0`, so a
1114    /// report identified by `stream_id` alone would make "once per stream"
1115    /// read as "once per session" — and a test asserting one event per mixed
1116    /// stream would pass on QUIC and be unwritable on WT. The transport id
1117    /// rides along because it is what correlates this report with every
1118    /// other event in this enum.
1119    ClassChangedMidStream {
1120        /// Session-local identity of the destination stream carrying both
1121        /// classes. Unique even on the WebTransport arm.
1122        key: StreamKey,
1123        /// Transport stream id, for correlation with the other events in
1124        /// this enum. **`0` for every WebTransport stream**; `key` is what
1125        /// identifies.
1126        stream_id: u64,
1127    },
1128}
1129
1130/// The connection an [`ImpairmentKind`] is about, given the side the task
1131/// that raised it was forwarding from.
1132///
1133/// The whole mapping lives here, in one exhaustive `match`, rather than at
1134/// the twenty-odd sites that raise these reports. Two reasons, and the
1135/// second is the one that matters.
1136///
1137/// A raising site knows only the direction it reads from. Working out that
1138/// a queue it could not flush belongs to the *other* connection is a turn
1139/// each site would have to make for itself, and a site that got it wrong
1140/// would produce a number that is correct under a wrong label — the failure
1141/// this whole surface is written to avoid, and one no test at that site
1142/// would notice, because the event would still arrive and still carry a
1143/// plausible leg.
1144///
1145/// And the turn is a property of the **kind**, not of the site. Whether a
1146/// report is about what came in or about what could not go out is decided
1147/// by what the report *says*, so the decision belongs beside the enum that
1148/// says it. A variant added to [`ImpairmentKind`] stops this function
1149/// compiling — the `match` has no catch-all on purpose — which is the one
1150/// place a new report reliably gets asked which leg it means.
1151///
1152/// # `arrived_on` must be an ingress side
1153///
1154/// Every [`Reporter`](crate::exec::Reporter) in this crate is built with the
1155/// side its pipe *reads* from, so `arrived_on` is `ClientToProxy` or
1156/// `RelayToProxy` in practice. The function is still total over all four,
1157/// because `ProxySide` has four variants and a panicking forwarding path is
1158/// worse than a defensible answer: an egress side is read as naming its own
1159/// connection, which is what it does.
1160pub(crate) fn impairment_leg(kind: &ImpairmentKind, arrived_on: ProxySide) -> Option<Leg> {
1161    let here = connection_of(arrived_on);
1162    match kind {
1163        // The parser gave up on bytes that arrived. Nothing here is a claim
1164        // about what could be written, so the far leg is not implicated.
1165        ImpairmentKind::FramerBypass { .. }
1166        | ImpairmentKind::ObjectNotAddressable { .. }
1167        | ImpairmentKind::ControlFrameNotDecodable { .. } => Some(here),
1168
1169        // Every one of these is a failure to place bytes on the far side —
1170        // a queue that filled in front of it, a release it clamped, bytes it
1171        // could not vouch for, a datagram it refused, a stream it had to
1172        // reset or could not renumber for, an object that crossed it
1173        // unpaced, a destination whose throughput two classes now share, a
1174        // first write that was held. The connection they are about is the
1175        // one being written to, which is the opposite of the one they
1176        // arrived on.
1177        ImpairmentKind::EgressQueueFull { .. }
1178        | ImpairmentKind::HoldClamped { .. }
1179        | ImpairmentKind::QueuedBytesAtTeardown { .. }
1180        | ImpairmentKind::DatagramNotSent { .. }
1181        | ImpairmentKind::ControlStreamTruncated { .. }
1182        | ImpairmentKind::ElideFixupLost { .. }
1183        | ImpairmentKind::ShapeUnpacedObject { .. }
1184        | ImpairmentKind::ClassChangedMidStream { .. }
1185        | ImpairmentKind::SerializeTargetUnknown { .. } => Some(other_connection(here)),
1186
1187        // A profile compared against the sizes it was asked to pace, or the
1188        // process compared against its host. All three are equally true of
1189        // both connections and none of them stops being true if one leg goes
1190        // away, so naming a leg would be naming whichever pipe noticed first.
1191        ImpairmentKind::CoarseReleaseTimer { .. }
1192        | ImpairmentKind::ShapeRuleUnmatchable { .. }
1193        | ImpairmentKind::ShapeBurstBelowUnit { .. } => None,
1194    }
1195}
1196
1197/// The connection a direction of travel belongs to.
1198fn connection_of(side: ProxySide) -> Leg {
1199    match side {
1200        ProxySide::ClientToProxy | ProxySide::ProxyToClient => Leg::Client,
1201        ProxySide::ProxyToRelay | ProxySide::RelayToProxy => Leg::Upstream,
1202    }
1203}
1204
1205/// The proxy's other connection. There are exactly two.
1206fn other_connection(leg: Leg) -> Leg {
1207    match leg {
1208        Leg::Client => Leg::Upstream,
1209        Leg::Upstream => Leg::Client,
1210    }
1211}