Skip to main content

Action

Enum Action 

Source
#[non_exhaustive]
pub enum Action { Pass, Replace(Bytes), ReplacePayload(Bytes), Delay { by: Duration, then: Box<Action>, }, Hold { gate: Gate, then: Box<Action>, }, Drop(DropMode), Truncate { bytes: usize, code: u64, }, ResetStream { code: u64, }, CloseSession { code: u32, reason: Bytes, }, }
Expand description

What the engine should do with the unit of traffic the hook was shown.

Self::Delay and Self::Hold are modifiers: they carry the action to perform once the unit is released. Their then must be a content action — Self::Pass, Self::Replace, Self::ReplacePayload or Self::Drop. Any other nesting is refused with Refusal::WrongComposition rather than silently ignored, so Delay { then: ResetStream { .. } } and Delay { then: Truncate { .. } } (terminals — positional by construction, so the queue already orders them and a delay would only move the end of the stream), Delay { then: CloseSession { .. } } (a session-scoped decision, with no per-unit release to attach it to) and Delay { then: Delay { .. } } (a nested modifier) are all loud. Those four, and only those four, are the refused shapes; the three detail strings in exec.rs enumerate them.

§Delay { then: Drop(_) } is admitted, and is not inert

A previous revision of this paragraph named it as a refused shape and called it “unobservable”. Both halves were wrong, and they also contradicted the sentence above them, which lists Drop as a legal inner action. The engine’s behaviour is the correct one and the doc has been brought to it.

It is not unobservable. A deferred drop takes an ordering slot in the stream’s pending queue for the whole of by, and the queue only ever writes from its front — so nothing the hook decides after it can reach the wire until it is released. So Action::Drop(DropMode::Elide).delayed(d) deletes this object and head-of-line-blocks everything after it on that stream for d: one decision, two effects, both on the wire. That is a scenario worth expressing — a relay that loses an object and stalls while it notices — and refusing it would take it away. Hold { then: Drop(_) } is the same impairment under a Gate instead of a clock.

It follows that this composition is not a way to write a deliberately inert guard. Drop deletes the unit wherever it is admitted, wrapped or not; a hook that wants the unit forwarded untouched returns Self::Pass, which is the only action that promises that.

Variants (Non-exhaustive)§

This enum is marked as non-exhaustive
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

Pass

Forward unchanged.

On the object site this forwards the framer’s own Bytes slice — never a re-encode, on any draft. That is what makes a no-action session byte-identical to the byte pump.

§

Replace(Bytes)

Replace the whole unit’s wire bytes.

Valid at the control and datagram sites, where the unit is self-delimiting and the hook owns all of it. Refused at the object site with Refusal::WrongSite: replacing a whole wire object would require the hook to encode the draft’s object framing, which is the knowledge this crate exists to hide. Use Self::ReplacePayload.

§

ReplacePayload(Bytes)

Replace an object’s or a datagram’s payload, keeping its framing.

At the object site. The replacement must be exactly ObjectMeta::payload_len bytes; a different length is refused with Refusal::LengthChanged rather than mis-framed. The engine splices at raw.len() - meta.payload_len, which is the payload offset on every draft and both stream kinds.

Refused when the object carries a status (ObjectMeta::status is Some), because a status object has no payload slot.

At the datagram site, where it is gated on Precondition::DatagramPayloadDelimited: the engine splices after the decoded header, at data.len() - cursor.len(). Refused with Refusal::PayloadNotDelimited in the three cases where no such offset exists — on draft-14, whose AnyDatagramHeader decode consumes the payload; on a status datagram, which has no payload slot; and when the header did not decode, where the hook still fires but there is nothing to splice after. See ProxyHook::on_datagram, whose rustdoc says the same thing from the caller’s side.

Refused with Refusal::WrongSite everywhere else — the control site’s unit has no payload/framing split the proxy may assume, and the stream sites take a StreamAction.

§

Delay

Release then no earlier than arrived_at + by.

A deadline, not a spacing: two objects that arrive together, each with Delay { by: 100ms }, are both released about 100 ms later — not at +100 ms and +200 ms. Release times are clamped monotonically against the queue’s tail, so a later unit can never overtake an earlier one regardless of its delay.

Reads continue while units wait, so this is a latency shift rather than a rate limit — until the stream’s pending queue reaches EgressConfig::max_pending_bytes, at which point reads stop and the delay becomes backpressure. That transition is reported once as ProxyEvent::Impairment { kind: EgressQueueFull }.

by is clamped to EgressConfig::max_hold. A clamp is reported as ProxyEvent::Impairment { kind: HoldClamped { .. } }.

§Resolution

Release timing does not use tokio::time::sleep, which is bounded below by the ~15.6 ms Windows system tick — as is every other interruptible wait in std. Releases are driven by a process-wide release wheel on one dedicated OS thread, measured at p50 0.11-0.17 ms, p95 0.52-0.57 ms end-to-end on Windows 11 against 11-15 ms for tokio::time::sleep. The measured lateness of every deferred release is reported in crate::instrument::Counters::release_errors; assert on it rather than assuming the delay was honoured.

Three consequences worth knowing:

  • When MOQTAP_RELEASE_TIMER forces the coarse backend, the floor returns to ~15.6 ms on Windows, and the session says so exactly once with ProxyEvent::Impairment { kind: CoarseReleaseTimer { .. } }. A run that could not honour its own delays is never silent about it. See crate::instrument::release_timer_backend.
  • The wheel is on a real clock, not tokio’s. A test using #[tokio::test(start_paused = true)] and expecting a Delay to complete will wait out the real deadline, not advance virtual time. Do not pause time around Delay or Hold.
  • The wheel’s thread is created on the first deferred release in the process and is never joined (it lives in a static OnceLock). Leak detectors and thread counters will see one live thread and one leaked allocation after any delaying test. A session that never delays never creates it — crate::instrument::release_timer_started is the falsifiable form of that claim.

Fields

§by: Duration

How long to hold the unit past its arrival.

§then: Box<Action>

What to do once it is released.

§

Hold

Release then when gate is released, at EgressConfig::max_hold, or at session teardown — whichever comes first.

Fields

§gate: Gate

The release handle.

§then: Box<Action>

What to do once it is released.

§

Drop(DropMode)

Remove the unit from the wire. See DropMode.

§

Truncate

Write the first bytes bytes of this unit, then reset the stream.

Positional: everything queued ahead of it is written first, then the truncated prefix, then RESET_STREAM with code. code is stated rather than defaulted, exactly as in Self::ResetStream. A truncation is a simulated publisher abandonment and the code is the whole of what the scenario is simulating: 0x0 INTERNAL_ERROR reads as “the proxy did this”, 0x2 DELIVERY_TIMEOUT reads as a relay hit its delivery timeout, and the two make a subscriber take different paths. There is no default that is right for both, so the type asks. The same range check as Self::ResetStream applies: Refusal::ErrorCodeOutOfRange above 2^62 - 1, before anything is sent. On drafts 07-10, which define no stream-reset code vocabulary at all, the reset still executes and Effect::Truncated reports code_defined: false. The peer observes at most bytes further bytes, and may observe none. quinn clears the receive assembler the moment RESET_STREAM is processed (quinn-proto/src/connection/streams/recv.rs: Nuke buffers so that future reads fail immediately), so only bytes the peer application had already read out survive; reset() additionally discards anything still in the local send buffer. Assert an upper bound and a prefix, never an exact count.

Refused on control streams on every draft, and on datagrams.

Fields

§bytes: usize

How many bytes of this unit to write before resetting.

§code: u64

The application error code for the reset that follows.

§

ResetStream

Reset the destination stream with this application error code.

Refused on control streams on every draft: resetting a control stream at the transport layer is a session-level PROTOCOL_VIOLATION in all of drafts 07-19. The documented escalation is Self::CloseSession.

Codes above the QUIC varint ceiling (2^62 - 1) are refused with Refusal::ErrorCodeOutOfRange before anything is sent, rather than silently becoming a FIN.

Fields

§code: u64

The application error code to send.

§

CloseSession

Close both legs of the session with this code and reason.

code is a session termination code, a different namespace from a stream reset code: 0x0 NO_ERROR, 0x1 INTERNAL_ERROR, 0x2 UNAUTHORIZED, 0x3 PROTOCOL_VIOLATION. Note that session INTERNAL_ERROR is 0x1 while stream-reset INTERNAL_ERROR is 0x0.

Honoured at every site that returns an Action, including Site::StreamEnd on a data stream and on the control stream. A close is session-scoped, so no site can be the wrong one for it: unlike Self::ResetStream, there is no per-stream object it needs and nothing left to forward that honouring it could corrupt. It is the documented escalation for the two sites where a stream reset is a protocol violation.

The first close request wins; later ones are refused with Refusal::SessionAlreadyClosing.

Fields

§code: u32

The session termination code.

§reason: Bytes

The reason phrase.

Implementations§

Source§

impl Action

Source

pub fn delayed(self, by: Duration) -> Self

Wrap this action in a Action::Delay.

Source

pub fn held(self, gate: Gate) -> Self

Wrap this action in a Action::Hold.

Trait Implementations§

Source§

impl Clone for Action

Source§

fn clone(&self) -> Action

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

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

Performs copy-assignment from source. Read more
Source§

impl Debug for Action

Source§

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

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

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

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

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

Source§

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

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

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

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

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

fn in_current_span(self) -> Instrumented<Self>

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

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

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

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

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

Source§

type Error = !

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

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

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

fn with_current_subscriber(self) -> WithDispatch<Self>

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