Skip to main content

Stream

Struct Stream 

Source
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: i32

RFC 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: i32

RFC 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: bool

True when the frontend connection has received end_of_stream from the client.

§back_received_end_of_stream: bool

True when the backend connection has received end_of_stream from the backend server.

§front_data_received: usize

Tracks total DATA payload bytes received on the frontend for content-length validation (RFC 9113 §8.1.1)

§back_data_received: usize

Tracks total DATA payload bytes received on the backend for content-length validation (RFC 9113 §8.1.1)

§request_counted: bool

True 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: bool

True 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

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn split(&mut self, position: &Position) -> StreamParts<'_>

Source

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).

Source

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.

Source

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’s RetryType::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::readable treats every size == 0 alike. 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” unless non_idempotent is set; pingora vetoes on !method.is_idempotent(); HAProxy provides an http-request disable-l7-retry action 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’s junk-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.

Source

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.

Trait Implementations§

Source§

impl Debug for Stream

Source§

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

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl !RefUnwindSafe for Stream

§

impl !Send for Stream

§

impl !Sync for Stream

§

impl !UnwindSafe for Stream

§

impl Freeze for Stream

§

impl Unpin for Stream

§

impl UnsafeUnpin for Stream

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<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

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> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

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> Same for T

Source§

type Output = T

Should always be Self
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, !>

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.