struct ForwardCtx {Show 24 fields
session_id: SessionId,
draft: Arc<SessionDraft>,
draft_is_fixed: bool,
observer: Arc<dyn ProxyObserver>,
hook: Arc<dyn ProxyHook>,
cancel: CancellationToken,
counters: Arc<Recorder>,
shape_stats: Arc<ShapeRecorder>,
closer: SessionCloser,
egress: EgressConfig,
observer_enabled: bool,
objects_enabled: bool,
object_hook: bool,
shaping_enabled: bool,
shape: Option<Arc<SessionShaper>>,
control_mutation: bool,
control_parse: bool,
fetch_orders_wanted: bool,
fetch_orders: Arc<FetchGroupOrders>,
streams_enabled: bool,
datagram_hook: bool,
next_stream_id: Arc<AtomicU64>,
streams: Arc<StreamRegistry>,
gauge: Arc<EgressGauge>,
}Expand description
Shared context for forwarding helpers, avoiding repeated parameter lists.
Fields§
§session_id: SessionId§draft: Arc<SessionDraft>The draft this session frames with, shared by every task rather than
copied into each — see SessionDraft for why that matters and for
what settles it. Read through ForwardCtx::draft, or through
ForwardCtx::resolved_draft where the answer has to be right
rather than current.
draft_is_fixed: boolWhether draft is fixed (from ALPN) and should not be refined by
peeking at SETUP messages.
observer: Arc<dyn ProxyObserver>§hook: Arc<dyn ProxyHook>§cancel: CancellationToken§counters: Arc<Recorder>This session’s slow-path counters.
shape_stats: Arc<ShapeRecorder>This session’s shaping counters. Cloned per task exactly as
counters is, and carried unconditionally: an unshaped
session’s recorder has no class rows and no writer, so the cost of
carrying it is one Arc clone per forwarding task and the cost of
not carrying it would be an Option branch on the data path.
closer: SessionCloserWhere an Action::CloseSession lands, and what run_with_transport
reads its close code and reason back out of.
egress: EgressConfigEngine knobs for the per-stream deferred write queues.
observer_enabled: boolCached observer.wants_events() — gates event construction and
emission in the hot forwarding loop. When false, the proxy can
skip parsing for observation purposes and run as a byte pump.
objects_enabled: boolWhether data streams are framed into objects — the framing gate,
which decides whether pipe_data calls pipe_data_framed or
pipe_data_passthrough. Keeps its observer_enabled || term
because ProxyEvent::Object is an observer-only guarantee. This is
not the gate on calling on_object; see object_hook.
object_hook: boolWhether ProxyHook::on_object is consulted. Interest::OBJECTS
alone, with no observer_enabled || term: an event observer must
not hand a hook that declared no object interest the power to drop,
delay and rewrite traffic.
shaping_enabled: boolWhether this session was configured with a
ShapeProfile.
config.shape.is_some() alone, with no observer_enabled ||
term — the same asymmetry as object_hook and for the same
reason: shaping is configuration, so attaching an observer must not
arm it. It is a term of objects_enabled, because a profile has
to arm framing on its own.
The read below is what makes that implication checkable rather than merely written down.
Exactly shape.is_some(), and the two are kept as separate fields
on purpose: this one is a bool a debug_assert! and a hot-path
branch can read without touching an Arc, and shape is the
engine. The equivalence is checked in pipe_data, where the
framing decision is taken.
shape: Option<Arc<SessionShaper>>This session’s shaper, or None when no
ShapeProfile was configured.
Unlike shape_stats and streams, which are always constructed,
this is genuinely optional — there is nothing for an unshaped
session to share, and an Option here is what makes “a session with
shape: None adds nothing to the shaping path” a fact the type
system carries rather than a claim a reviewer checks.
Some or None is fixed for the session’s life. A profile installed
on the proxy afterwards can replace what is inside this, and cannot
put something here: framing is armed at session start and a session
that began as a byte pump produces no ObjectMeta to classify.
control_mutation: boolWhether ProxyHook::on_control_message is consulted, which also
routes the control stream through the parse-then-forward pipe: the
pass-through pipe writes before it parses, so a hook return there
would be unexecutable by construction.
control_parse: boolWhether a ControlStreamParser is built for somebody to read.
Interest::NONE with no observer builds none, which is what makes
control_parsers_created == 0 unconditional on that path.
Not the whole answer to “is there a parser”: fetch_orders_wanted is
the other, and it builds one for the session’s own use. Ask
ForwardCtx::control_frames_are_decoded rather than either alone.
fetch_orders_wanted: boolWhether this session has to decode control frames to read its own fetch streams — drafts 18, 19 and 20, framing data.
Unlike control_parse this arms no report and calls no hook. It is
the one case where the proxy parses the control plane for itself, and
it is why a hook declaring Interest::OBJECTS alone can still see a
draft-19 fetch Object.
fetch_orders: Arc<FetchGroupOrders>What each FETCH this session carried asked for, waiting for the response stream that answers it.
Written by both control pipes and read by the object framer; see
FetchGroupOrders.
streams_enabled: boolWhether on_stream_open, on_stream_header and on_stream_end are
consulted. Interest::STREAMS contains Interest::OBJECTS
structurally, so this implies objects_enabled.
datagram_hook: boolWhether ProxyHook::on_datagram is consulted.
next_stream_id: Arc<AtomicU64>The session’s StreamKey mint.
One counter per session, shared by every forwarding task through the
Arc — ForwardCtx is cloned per task and per stream, so a plain
AtomicU64 would give each clone its own sequence and two streams would
collide on id 0. The Arc is what makes unique for the session’s
lifetime true rather than aspirational.
streams: Arc<StreamRegistry>Every forwarded stream that is still live, and the gate each one releases when it ends.
Always constructed, for every session, exactly like
next_stream_id and unlike anything a ShapeProfile will later
arm: StreamAction::SerializeAfter is gated by Interest::STREAMS
and the capability table publishes it as an unconditional Yes at
both stream sites, so a registry that only existed when a profile
was configured would make that published cell a lie. An empty
registry allocates nothing and touches no counter, so
interest_none.rs’s whole-struct Counters::default() comparison
and its !release_timer_started() companion stay falsifiable.
gauge: Arc<EgressGauge>How many bytes this session’s egress queues are holding, summed
across every stream.
Always constructed, like streams and for a related reason: a gauge
that only some queues reported into would answer this session has
nothing left to flush while another stream still held a deferred frame,
and the one caller that reads it — a requested close deciding whether it
may stop waiting — would act on that answer.
Costs one Arc clone per forwarding task and two relaxed atomic
updates per queued unit. A session that queues nothing, which is
every session with no timing action and no profile, never touches
it: the counters only move inside PendingQueue::push and its
releases.
Implementations§
Source§impl ForwardCtx
impl ForwardCtx
Sourcefn draft(&self) -> DraftVersion
fn draft(&self) -> DraftVersion
The draft this session frames with, as it stands now.
Sourcefn control_frames_are_decoded(&self) -> bool
fn control_frames_are_decoded(&self) -> bool
Whether a control frame gets decoded on this session at all.
Two unrelated reasons, deliberately summed in one place rather than
spelled a || b at each of the pipes: control_parse is somebody
asking to be told, and fetch_orders_wanted is the session needing
the answer itself. A pipe that tested only the first left a
draft-19 fetch stream unaddressable on an Interest::OBJECTS
session, which is the shape of hook the object site exists for.
Sourcefn caps(&self) -> Capabilities
fn caps(&self) -> Capabilities
What this session’s draft can be asked for.
Built here, at each site that needs one, rather than cached on this
struct. Capabilities is a Copy newtype over a draft, so
constructing it costs a move of one enum and answers for the draft
the session is framing with at that moment — while a cached copy
would have been built beside the guess and would go on answering for
it after the peer named something else. One draft in one cell has one
consumer to keep correct; a cached table beside it would be a second.
Sourceasync fn resolved_draft(&self) -> DraftVersion
async fn resolved_draft(&self) -> DraftVersion
The draft this session frames with, waited for.
The ordering edge between the control stream, which learns the draft,
and the tasks that have to agree with it. Called where the wrong
draft produces a wrong result rather than a stale label: the object
framer decides where an object ends, and a datagram header decoder
decides what a datagram says. See SessionDraft::resolved for what
bounds the wait.
Sourcefn mint_key(&self, side: ProxySide) -> StreamKey
fn mint_key(&self, side: ProxySide) -> StreamKey
Mint this stream’s session-local identity.
Called once per forwarded stream, at accept, and handed to every
hook site that stream reaches. Monotonic, never reused, and
deliberately not the transport stream id: on the WebTransport arm
that is the constant 0 for every stream, so a transport-keyed
identity collapses a whole side onto one entry.
Sourcefn emit(&self, event: impl FnOnce() -> ProxyEvent)
fn emit(&self, event: impl FnOnce() -> ProxyEvent)
Emit a proxy event only if the observer wants events.
Takes a closure so the ProxyEvent is not constructed when
observation is disabled — avoiding clones of message payloads in
the hot path.
Trait Implementations§
Source§impl Clone for ForwardCtx
impl Clone for ForwardCtx
Source§fn clone(&self) -> ForwardCtx
fn clone(&self) -> ForwardCtx
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more