pub struct Stream {Show 17 fields
pub front_window: i32,
pub back_window: i32,
pub attempts: u8,
pub tried_backends: Vec<SocketAddr>,
pub state: StreamState,
pub front_received_end_of_stream: bool,
pub back_received_end_of_stream: bool,
pub front_data_received: usize,
pub back_data_received: usize,
pub request_counted: bool,
pub front: Kawa<Checkout>,
pub back: Kawa<Checkout>,
pub retry_buffer: Option<ReplayCapture>,
pub front_bound_to_backend: bool,
pub context: HttpContext,
pub metrics: SessionMetrics,
pub answers: Rc<RefCell<HttpAnswers>>,
}Fields§
§front_window: i32RFC 9113 §6.9 send window of this stream on the FRONTEND connection:
the credit the client granted for the response DATA Sōzu writes to
it. Sized by the client’s SETTINGS_INITIAL_WINDOW_SIZE when the
stream is created and moved only by that connection’s SETTINGS and
WINDOW_UPDATE frames. Unused when the frontend speaks H1.
back_window: i32RFC 9113 §6.9 send window of this stream on the BACKEND connection:
the credit the H2 backend granted for the request DATA Sōzu writes
to it. Flow control is hop-by-hop, so it is independent of
Self::front_window: ConnectionH2::start_stream sizes it from the
backend’s SETTINGS_INITIAL_WINDOW_SIZE each time the stream is
opened on a backend connection, and only that connection’s SETTINGS
and WINDOW_UPDATE frames move it. Unused when the backend speaks H1.
attempts: u8§tried_backends: Vec<SocketAddr>Addresses of the backends this request failed to connect to: refused,
timed out, or reporting a socket error once the dial completed. A
retry prefers any other backend (BackendMap::reserve_backend_excluding
in lib/src/backends.rs, sozu-proxy/sozu#1800). Cleared with
Self::attempts for each request; empty, and unallocated, on every
request whose first dial succeeds.
state: StreamState§front_received_end_of_stream: boolTrue when the frontend connection has received end_of_stream from the client.
back_received_end_of_stream: boolTrue when the backend connection has received end_of_stream from the backend server.
front_data_received: usizeTracks total DATA payload bytes received on the frontend for content-length validation (RFC 9113 §8.1.1)
back_data_received: usizeTracks total DATA payload bytes received on the backend for content-length validation (RFC 9113 §8.1.1)
request_counted: boolTrue when gauge_add!(names::http::ACTIVE_REQUESTS, 1) was emitted for this stream.
Prevents underflow when generate_access_log is called for streams that never
had their request fully parsed (idle timeouts, malformed requests).
front: Kawa<Checkout>§back: Kawa<Checkout>§retry_buffer: Option<ReplayCapture>The serialized request already written to the upstream, kept so it can be replayed on a fresh connection when a POOLED keep-alive upstream turns out to have been closed by its peer before it answered.
None means “not replayable” and is the state on every path that is
not the stale-pool race: a freshly dialled upstream never fills it
(see super::h1::ConnectionH1’s reused_from_pool), a response
byte clears it, and a request too large to fit one front buffer
truncates it back to None. Some therefore carries both the bytes
and the proof that replay is allowed.
This mirrors pingora’s RetryType::ReusedOnly + retry_buffer_ truncated() pair and HAProxy’s “requires to allocate a buffer and
copy the whole request into it […] Requests not fitting in a single
buffer will never be retried” — replaying a request that kawa has
already consumed is impossible without holding its bytes.
The capture is charged against MAX_ARMED_REPLAY_CAPTURES for as
long as it is Some; see ReplayCapture.
front_bound_to_backend: boolTrue once an H2 backend connection has HPACK-encoded this request
into front.out (ConnectionH2::poll_write_target’s
H2WritePhase::Prepare arm, Position::Client only).
Those frames carry that connection’s stream id and field blocks
encoded against that connection’s HPACK encoder, and the request’s
header blocks are gone from front.blocks: the request can no longer
be sent on any other connection, nor encoded again. Until a request
byte reaches the socket (front.consumed), nothing distinguishes it
from an untouched request, so this is what keeps
end_stream_decision and ConnectionH2::handle_goaway_frame from
linking it to another backend, where the stale frames would decode
against another table — fields another request of this frontend
connection inserted (a backend connection belongs to one Mux), or a
COMPRESSION_ERROR that takes that connection down (sozu-proxy/sozu#1632). An H1 backend never sets it: its output is
protocol bytes any fresh H1 connection can send as they are.
Cleared with the front kawa, wherever the slot takes a new request.
context: HttpContext§metrics: SessionMetrics§answers: Rc<RefCell<HttpAnswers>>The listener answer registry this stream renders its default answers from, captured when the request that owns the slot arrived and held for the rest of the stream’s life.
Every set_default_answer site in the mux reads this handle instead
of borrowing Context::listener again, so
an operator listener reload landing mid-request cannot change the
template a stream already in flight is about to render.
HttpListener::update_config / HttpsListener::update_config
publish a new registry rather than overwriting this one, which is
what makes the captured handle a snapshot and not just a second name
for the live one. See LIFECYCLE.md §2.5.
Rc so the capture is a refcount bump on a path that runs once per
request; the inner RefCell is the registry’s own, not a mutation
point for the mux — nothing under mux/ ever takes it mutably.
Implementations§
Source§impl Stream
impl Stream
Sourcepub fn new(
buffers: &mut dyn BufferSource,
context: HttpContext,
answers: Rc<RefCell<HttpAnswers>>,
window: u32,
) -> Option<Self>
pub fn new( buffers: &mut dyn BufferSource, context: HttpContext, answers: Rc<RefCell<HttpAnswers>>, window: u32, ) -> Option<Self>
None when buffers cannot supply the pair. Both halves are asked
for together and neither is kept without the other, so a stream that
cannot be served leaves the source exactly as it found it — which is
what makes the caller’s RST_STREAM(REFUSED_STREAM) a refusal of one
stream rather than a leak charged to the next.
Sourcepub fn linked_token(&self) -> Option<Token>
pub fn linked_token(&self) -> Option<Token>
Convenience accessor for the backend token when the stream is Linked.
Used by access-log emission sites to look up the backend socket on the
owning Endpoint/Router without re-pattern-matching state inline.
Sourcepub fn is_quiesced(&self) -> bool
pub fn is_quiesced(&self) -> bool
Returns true when both front and back kawa buffers are in a terminal or initial state with no pending data. Used during shutdown to skip streams that have already completed their work.
Sourcepub fn send_window_mut(&mut self, position: &Position) -> &mut i32
pub fn send_window_mut(&mut self, position: &Position) -> &mut i32
The RFC 9113 §6.9 send window the connection at position holds for
this stream: Self::back_window on a backend connection
(Position::Client), Self::front_window on a frontend one. The
same selection as Self::split’s window, for the paths that
adjust a window without splitting the stream.
pub fn split(&mut self, position: &Position) -> StreamParts<'_>
Sourcepub fn arm_upstream_replay(&mut self)
pub fn arm_upstream_replay(&mut self)
Arm request capture for an attempt on a reused keep-alive upstream.
Idempotent WITHIN one attempt: an already-armed buffer is left alone so a multi-pass write keeps accumulating into the same allocation.
It is NOT once per request. super::h1::ConnectionH1::start_stream
calls this on every KeepAlive -> Connected transition and
reused_from_pool is never cleared, so a replay that lands on
another pooled connection re-arms a fresh capture and may itself be
replayed. Router::plan_connect’s gate of stream.attempts against the
cluster’s max_connection_attempts is the only bound on how many times
one request is re-issued.
Arming is BEST EFFORT. Past MAX_ARMED_REPLAY_CAPTURES no buffer is
installed, backend.retry.captures_declined is incremented, and the
request proceeds un-replayable: Stream::can_replay_on_fresh_upstream
reads the absent buffer as “not replayable”, so a stale pooled upstream
yields the same 502 Bad Gateway it yielded before the replay existed.
Nothing is refused and no frontend parks — which is exactly what
pool-allocating the captures would have cost under the same memory
pressure (sozu-proxy/sozu#1450).
A NON-IDEMPOTENT request is not captured at all, and does not spend a
charge. Stream::can_replay_on_fresh_upstream vetoes the method, so
such a capture could only ever be held for the life of the attempt and
thrown away. Before the ceiling that was pure waste; with one it is
also a slot a replayable request cannot have — on a POST-heavy pooled
workload it would be most of the budget, while
backend.retry.captures_armed sat at the ceiling showing an operator
nothing about why.
The method is known here. crate::protocol::http::editor::HttpContext
sets it in on_request_headers during the frontend parse, and
super::router::Router::plan_connect routes on it — route_from_request,
which fails with RetrieveClusterError::NoMethod without it — before
it reaches super::h1::ConnectionH1::start_stream, the sole caller of
this function.
This makes the idempotence conjunct in
Stream::can_replay_on_fresh_upstream unreachable in production
rather than redundant by accident. It is deliberately KEPT as defense
in depth: it is the conjunct a later widening of this guard would
silently undo, and it is the one the replay’s whole justification
rests on (RFC 9110 §9.2.2).
Sourcepub fn forget_upstream_replay(&mut self)
pub fn forget_upstream_replay(&mut self)
Forget the captured request.
Called as soon as the upstream produces its first byte: past that point the attempt is no longer replayable (the boundary below), so holding the copy would only cost memory.
Sourcepub fn can_replay_on_fresh_upstream(&self) -> bool
pub fn can_replay_on_fresh_upstream(&self) -> bool
May this stream’s request be re-issued on a fresh upstream connection?
The boundary is no response byte has been received, narrowed to the stale-pool race:
retry_buffer.is_some()proves BOTH that the whole serialized request is still held AND that it went onto a reused keep-alive connection, because that is the only path that arms the capture. This is pingora’sRetryType::ReusedOnly. It is a POLICY, not a diagnosis: sozu cannot tell a stale pool socket from an origin that half-closed after processing the request, or from one that crashed mid-request —super::h1::ConnectionH1::readabletreats everysize == 0alike. Restricting replay to pooled connections narrows it to the case where an unobserved idle close is plausible; the conjunct below is what makes re-issuing permissible.- the method is idempotent, which RFC 9110 §9.2.2 defines as exactly
this permission: re-issuing it has the same intended effect as
issuing it once, so a client cannot observe the difference. That is
the load-bearing justification for the whole feature. nginx refuses
POST, LOCK, PATCH“if a request has been sent to an upstream server” unlessnon_idempotentis set; pingora vetoes on!method.is_idempotent(); HAProxy provides anhttp-request disable-l7-retryaction and gives POST as the rationale for reaching for it. - the upstream produced nothing at all: no byte forwarded
(
!back.consumed) and no byte even buffered (back.storage.is_empty()). nginx: “passing a request to the next server is only possible if nothing has been sent to a client yet”. Requiring an empty back buffer is stricter than that — a partial status line is HAProxy’sjunk-response, which it keeps out of every default.
The caller must additionally have established that no response is
available at all; super::shared::end_stream_decision owns that part
and is the only caller.
Sourcepub fn queue_upstream_replay(&mut self) -> Option<usize>
pub fn queue_upstream_replay(&mut self) -> Option<usize>
Queue the captured request for re-serialization onto a fresh upstream.
The bytes go back into front.out as an owned Store::Alloc, and
they are PREPENDED. out is not necessarily empty at this point:
super::h1::ConnectionH1::writable answers a partial
socket_write_vectored by signalling a pending write and returning,
and kawa::Kawa::consume pushes the partially consumed store back to
the FRONT of out (kawa-0.7.1 storage/repr.rs), so the bytes the
kernel refused are still queued here. The capture holds exactly the
bytes the socket DID accept, so capture and remainder are the
complementary halves of one serialization and the capture belongs
ahead of the remainder. kawa::Kawa::push_out appends, which would
put [tail][head] on the wire — a request mangled mid-token.
VecDeque::push_front preserves the relative order of what is already
queued, so this is correct however many entries the remainder spans:
consume pushes back at most ONE partially consumed store and drops
every entry ahead of it.
Prepending an owned Store::Alloc is also safe for kawa’s storage
bookkeeping. Kawa::leftmost_ref scans out for the first
Store::Slice and skips an Alloc, so buffer reclamation is
unchanged, and Store::push_left is a no-op on an Alloc, whose
bytes live outside storage and survive a shift.
front.blocks was drained by the prepare that preceded the write,
so the next writable pass’s prepare contributes nothing of its
own. Replaying the SERIALIZED form (rather than re-running the block
converter) also makes the retried request byte-identical to the first
attempt, Sozu-Id and X-Forwarded-* included.
Returns the number of bytes queued, or None when nothing was held.