#[non_exhaustive]pub enum ProxyEvent {
Show 19 variants
SessionStarted {
session_id: SessionId,
client_addr: SocketAddr,
client_transport: String,
},
SetupMessage {
session_id: SessionId,
side: ProxySide,
message: AnyControlMessage,
},
ControlMessage {
session_id: SessionId,
side: ProxySide,
message: AnyControlMessage,
},
DataStreamHeader {
session_id: SessionId,
side: ProxySide,
header: DataStreamHeaderKind,
},
ObjectHeader {
session_id: SessionId,
side: ProxySide,
header: AnyObjectHeader,
},
Object {
session_id: SessionId,
side: ProxySide,
meta: ObjectMeta,
},
Datagram {
session_id: SessionId,
side: ProxySide,
header: AnyDatagramHeader,
payload_len: usize,
},
BiStreamOpened {
session_id: SessionId,
side: ProxySide,
},
UniStreamOpened {
session_id: SessionId,
side: ProxySide,
},
ParseError {
session_id: SessionId,
side: ProxySide,
error: String,
},
StreamClosed {
session_id: SessionId,
side: ProxySide,
},
StreamReset {
session_id: SessionId,
side: ProxySide,
code: u64,
},
SessionEnded {
session_id: SessionId,
reason: String,
},
ActionApplied {
session_id: SessionId,
side: ProxySide,
stream_id: Option<u64>,
site: Site,
action: ActionKind,
effect: Effect,
},
ActionRefused {
session_id: SessionId,
side: ProxySide,
stream_id: Option<u64>,
site: Site,
action: ActionKind,
refusal: Refusal,
},
ActionFailed {
session_id: SessionId,
side: ProxySide,
site: Site,
action: ActionKind,
error: String,
},
Impairment {
session_id: SessionId,
side: ProxySide,
leg: Option<Leg>,
kind: ImpairmentKind,
},
Shaped {
session_id: SessionId,
side: ProxySide,
key: StreamKey,
stream_id: u64,
class: String,
outcome: ShapeOutcome,
},
ShapedDatagram {
session_id: SessionId,
side: ProxySide,
class: String,
outcome: ShapeOutcome,
},
}Expand description
Events emitted by the proxy during stream forwarding.
Marked #[non_exhaustive]: observers must carry a catch-all arm, so
that adding an event is not a breaking change.
§Asserting on events — destructure, do not compare
ProxyEvent derives Debug and Clone only. It carries decoded
codec types (AnyControlMessage, AnyDatagramHeader, …) that do
not implement PartialEq, so there is no assert_eq!(event, ProxyEvent::Something { .. }) to write and there will not be one:
deriving PartialEq here would require it on every draft’s message
enum.
Every assertion in this workspace therefore has the same two-step
shape — count with matches!, then destructure and compare the
payload:
use moqtap_proxy::event::{Effect, ProxyEvent};
fn exactly_one_replacement(events: &[ProxyEvent]) {
// 1. count the variant with `matches!`
let applied: Vec<&ProxyEvent> = events
.iter()
.filter(|e| matches!(e, ProxyEvent::ActionApplied { .. }))
.collect();
assert_eq!(applied.len(), 1);
// 2. destructure, then compare the payload with `assert_eq!`
let ProxyEvent::ActionApplied { effect, .. } = applied[0] else {
unreachable!("filtered above")
};
assert_eq!(*effect, Effect::Replaced { bytes: 4 });
}Step 2 works because the payload types added in 0.4.0 — Effect,
ImpairmentKind, Refusal, Site and ActionKind — do
derive PartialEq and Eq. The boundary is exactly at the event: the
event is destructured, what comes out of it is compared.
ProxyEvent and every one of those payload enums is
#[non_exhaustive], so a match over any of them from outside this
crate needs a catch-all arm; matches! supplies one for free, which
is the other reason it is the recommended spelling.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
SessionStarted
A new client connected and a session was created.
Fields
client_addr: SocketAddrThe client’s remote address.
SetupMessage
A setup message (CLIENT_SETUP or SERVER_SETUP) was observed.
Fields
message: AnyControlMessageThe decoded setup message.
ControlMessage
A control message was parsed from the forwarded byte stream.
Fields
message: AnyControlMessageThe decoded control message.
DataStreamHeader
A data stream header was parsed from a unidirectional stream.
Fields
header: DataStreamHeaderKindThe parsed header.
ObjectHeader
superseded by ProxyEvent::Object
An object header was parsed on a data stream.
Never emitted: AnyObjectHeader has no variants past draft-13, so
this could only ever report objects on the oldest drafts, and even
there it reported a header without consuming the payload behind it.
ProxyEvent::Object replaces it and covers drafts 07-19.
Fields
session_id: SessionIdsuperseded by ProxyEvent::Object
The session identifier.
side: ProxySidesuperseded by ProxyEvent::Object
Which side sent the object.
header: AnyObjectHeadersuperseded by ProxyEvent::Object
The parsed object header.
Object
A complete object was framed on a data stream.
Emitted once per object, in stream order, on every draft 07-19. Objects the framer could not address individually — one larger than its buffer cap, or a stream it stopped parsing — produce no event; their bytes are still forwarded unchanged.
Fields
meta: ObjectMetaThe object’s identity and framing, without its payload.
Datagram
A datagram arrived and its header was parsed.
An observation of what came in, emitted where the decode happens
— before any hook is consulted and before the datagram is handed to
the far transport. It is deliberately not a delivery receipt, and it
once said “was forwarded”, which was wrong in two directions at
once: a hook that drops or replaces the datagram still produces this
event, and the forward itself can be refused, which is reported
separately as ImpairmentKind::DatagramNotSent or, when somebody
acted on it, as ProxyEvent::ActionFailed.
It sits with ProxyEvent::Object and
ProxyEvent::ControlMessage rather than with the action events:
all three say a unit was seen and understood, and none of them says
where it ended up. Withholding it until after the send would make an
undecodable-or-dropped datagram invisible, which is exactly the case
an observer is usually watching for.
Emitted only when an observer is attached — the decode is skipped entirely otherwise — and only when the header actually decoded. A datagram whose header this crate cannot read produces no event and is still forwarded.
Fields
header: AnyDatagramHeaderThe parsed datagram header.
BiStreamOpened
A bidirectional stream was opened or accepted.
UniStreamOpened
A unidirectional stream was opened or accepted.
ParseError
Inline parse failed (non-fatal — bytes are still forwarded).
Fields
StreamClosed
A stream direction ended cleanly — the peer sent a FIN and every byte it wrote was forwarded.
An abnormal end is reported as ProxyEvent::StreamReset
instead, never as this event.
StreamReset
A peer tore a stream down abnormally — either RESET_STREAM from
the sender or STOP_SENDING from the receiver.
This reports the observation. The proxy mirrors the teardown onto the opposite stream with the same application error code; when that stream has already gone away the mirror is a no-op, and the event is still emitted so the teardown is never invisible.
Distinct from ProxyEvent::StreamClosed, which reports an
orderly FIN. An observer that sees this knows the stream was
abandoned and any data on it may be truncated.
Not emitted for streams the proxy itself tears down at session shutdown — those still end with a FIN, as they did before this event existed.
Fields
SessionEnded
The session ended.
ActionApplied
An action was executed and the wire changed.
Emitted once the engine has admitted the action and produced the
bytes or the plan for it: every guard has run, every refusal has
already been taken, and nothing between here and the wire can decline
it on this proxy’s behalf. effect is what was decided, down to the
byte count.
§It is not a delivery receipt, and one case makes that visible
The engine decides; the forwarding task then hands the result to the
transport. That transport can still say no — a replacement datagram
above the path MTU is the case that exists — and when it does, this
event has already been emitted and is followed by
ProxyEvent::ActionFailed naming the same site and action. Both
events are emitted for that attempt, in that order, and the pair is
the whole truth: the action was admitted, and it did not reach the
peer.
Folding the two into one report was rejected twice over. Withholding
this event until the write returned would mean an action that was
admitted, queued behind a Delay, and lost at teardown reported
nothing at all — and it is precisely the admitted-then-lost case that
ImpairmentKind::QueuedBytesAtTeardown is paired against. Reporting
only the failure would lose which action was taken, since a refusal
and a rejection name different things.
So the reading is: this event means the engine did it. An observer
that needs the peer got it must also watch for ActionFailed, and
for the impairments that report queued bytes.
A proxy-initiated reset appears here, not as
ProxyEvent::StreamReset, which continues to mean an observed
peer teardown.
§Cardinality under Delay and Hold
A deferred action emits two ActionApplied events, not one,
and they are distinguishable by action:
- at the decision,
{ action: Delay | Hold, effect: Queued { release_at } }— the modifier was accepted and the unit is in the queue; - at the release,
{ action: <the inner kind>, effect: <what the inner action did> }— e.g.{ action: Replace, effect: Replaced { bytes } }.
One event would force a choice between reporting the delay and reporting
the effect, and a queued unit that is later lost at teardown would have
reported a Replaced that never happened. Two events keep the engine
accepted this and “the wire changed” separately falsifiable, which is
what ImpairmentKind::QueuedBytesAtTeardown is paired against.
So the count to assert is exactly one ActionApplied per
(attempt, phase): one for a non-deferred action, two for a
deferred one.
Fields
action: ActionKindWhat was executed.
ActionRefused
An action could not be executed. Emitted per attempt, and the unit is forwarded unchanged.
The engine declined before anything reached the transport: the
wire carries exactly what it would have carried with no hook at
all. This is the pre-admission half of the two failure reports —
ProxyEvent::ActionFailed is the post-admission one, where the
engine accepted the action and the transport rejected it.
Fields
action: ActionKindWhat was attempted.
ActionFailed
An action was admitted but the transport rejected it. The session survives.
Emitted per failed attempt. The unit is not forwarded — the transport already declined it — and forwarding continues on everything else.
Reaching this means the capability table admitted the action and
the path disagreed; a replacement datagram above the path MTU is
the case that exists in 0.4.0. Neither
ProxyEvent::ActionApplied nor ProxyEvent::ActionRefused
can carry it on its own: the action was admitted, so refusing it
after the fact would be a lie, and it did not reach the peer, so
reporting only that it applied would be a bigger one.
It is paired with ActionApplied, not exclusive of it. The
engine admits, emits ActionApplied, and hands the bytes on; the
transport then declines them and this follows. An attempt that
reaches here therefore contributes two events, in that order. It is
exclusive of ProxyEvent::ActionRefused, which is the other
half of the same split: a refused unit is forwarded unchanged and a
transport failure on those bytes is
ImpairmentKind::DatagramNotSent, because nobody’s action was in
flight.
Distinct from ImpairmentKind::DatagramNotSent, which is the
same transport failure on a unit nobody acted on — there is no
site and no action to name there, and this variant requires
both.
Connection-level errors (ConnectionLost, Connection(_)) are
not reported here: they still end the session.
Fields
action: ActionKindWhat was executed.
Impairment
Something reduced what the proxy can do, with no action involved.
Every ImpairmentKind states its own emission cardinality;
read it there before asserting a count.
§This is emitted after the thing it reports, never before
Whatever the report describes has already happened by the time the observer is called: the reset has been handed to the transport, the datagram has been refused, the queue has been abandoned. Nothing in this crate emits one of these on the way into an operation that could still be declined.
That ordering is what makes the event stream a record rather than an
intention. Reversed, an impairment raised before a step that a guard
then refuses is an observer told about a loss that did not occur —
and there is no later event that retracts it, because this variant
has no counterpart to ProxyEvent::ActionFailed. An observer may
therefore treat every one of these as a fact about the past.
The one place the rule is visibly not the same is
ProxyEvent::ActionApplied, which is emitted when the engine
admits an action and before the caller hands the bytes to the
transport; that pairing is covered in its own rustdoc and is why
ActionFailed exists.
Fields
side: ProxySideThe side the reporting task was forwarding from — the
direction bytes were arriving on, not necessarily the direction
the impairment was felt in. Read leg for that.
leg: Option<Leg>Which of the proxy’s two connections the report is about, or
None when it is about neither.
§Not derivable from side, which is why it is here
side names the direction the reporting task reads from, so it
answers where did these bytes come from. Most of what
ImpairmentKind reports is a failure to write: a queue that
could not be flushed, a datagram the far transport refused, a
destination stream that had to be reset. Those belong to the
opposite connection from the one the bytes arrived on, and a
reader who mapped side to a connection would attribute every one
of them to the wrong leg — silently, and in a way that looks
entirely plausible in a log.
So the two are split, exactly as
ProxyStats splits what a leg read
from what it wrote:
- the arriving connection for
ImpairmentKind::FramerBypass,ImpairmentKind::ObjectNotAddressableandImpairmentKind::ControlFrameNotDecodable— all three are a parser giving up on bytes that came in, and none of them says anything about what could be written; - the departing connection for
ImpairmentKind::EgressQueueFull,ImpairmentKind::HoldClamped,ImpairmentKind::QueuedBytesAtTeardown,ImpairmentKind::DatagramNotSent,ImpairmentKind::ControlStreamTruncated,ImpairmentKind::ElideFixupLost,ImpairmentKind::ShapeUnpacedObject,ImpairmentKind::ClassChangedMidStreamandImpairmentKind::SerializeTargetUnknown— every one of them is about bytes the proxy was trying to place on the far side; NoneforImpairmentKind::CoarseReleaseTimer,ImpairmentKind::ShapeRuleUnmatchableandImpairmentKind::ShapeBurstBelowUnit. These three compare a profile against the sizes it is asked to pace, or the process against its host. None of them is a property of a connection, and all three are equally true of both legs.Nonesays that in the type instead of picking whichever leg the reporting task happened to be on.
A caller that wants which connection is unhealthy reads this field
and skips the Nones. A caller that wants which direction was
being forwarded when this was noticed reads side.
kind: ImpairmentKindWhat happened.
Shaped
The egress shaper acted on a unit as configured.
This is the product working, not an impairment: a configured drop is
the tool doing what it was told, where an
ImpairmentKind is the tool declining to. It is also the only
event that can carry a class label, because a class is a shaping
concept and nothing else in this enum has one.
Cardinality: once per stream per distinct outcome. Running
totals live in ShapeStats — a
per-object event would drown an observer at line rate, which is the
same reason ImpairmentKind::ObjectNotAddressable carries a total
instead of firing per object.
Fields
key: StreamKeySession-local identity. Unique even on the WebTransport arm,
where every transport stream id is the constant 0.
stream_id: u64Transport stream id, for correlation with the other events in
this enum. 0 for every WebTransport stream; key is what
identifies.
class: StringThe class the unit resolved to, or an empty string for a unit that matched no rule and for an outcome that is about the stream rather than about a unit.
outcome: ShapeOutcomeWhat the shaper did.
ShapedDatagram
The egress shaper discarded a datagram as configured.
Self::Shaped’s datagram sibling, and separate from it because a
datagram belongs to no stream: Shaped carries a StreamKey and a
transport stream id, and there is nothing honest to put in either.
Merging the two by making those fields optional would put an
Option on the far commoner event to describe the rarer one.
Cardinality: once per forwarding direction per distinct
outcome, which is the same rule Self::Shaped states per
stream — the direction is a datagram’s whole scope. Running totals
live in ShapeStats.
Trait Implementations§
Source§impl Clone for ProxyEvent
impl Clone for ProxyEvent
Source§fn clone(&self) -> ProxyEvent
fn clone(&self) -> ProxyEvent
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more