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}