pub struct H2Shell<Front: SocketHandler> {
pub core: ConnectionH2,
pub socket: Front,
/* private fields */
}Expand description
A ConnectionH2 and the socket it speaks through.
The split issue #1339 Q10 decided, in the shape it decided: the core is
byte-in / byte-out and knows nothing about sockets, while everything that
reaches one lives here. Connection::H2 holds this rather than the core,
so Connection<Front: SocketHandler> keeps its bound and Router::backends
keeps its declared type — the parameter did not leave the mux, it moved off
the state machine.
Both halves sit in this module, so this impl reaches the core’s private
fields exactly as the methods it inherited used to. Nothing was widened to
pub to let the socket keep working; the pub surface that did grow
exists for a driver OUTSIDE this crate — sim/tests/h2_simulation.rs is
the first, and it builds its own shell over the same poll/handle pairs
this one uses.
Fields§
§core: ConnectionH2The socket-free state machine.
socket: FrontThe only socket in the H2 frontend or backend path.
Implementations§
Source§impl<Front: SocketHandler> H2Shell<Front>
impl<Front: SocketHandler> H2Shell<Front>
Sourcepub fn initiate_close_notify(&mut self) -> bool
pub fn initiate_close_notify(&mut self) -> bool
Start TLS close_notify on the frontend and keep the session alive until rustls has flushed the generated records.
Sourcepub fn has_pending_write(&self) -> bool
pub fn has_pending_write(&self) -> bool
Returns true if there is data queued waiting to be flushed:
- The ordered output queue: control frames (GOAWAY, SETTINGS ACK, etc.) and the rest of a stream frame a stalled write cut
- A stream parked by a stalled write (
expect_write) - Encrypted TLS records in rustls’s output buffer not yet flushed to TCP
The TLS check is critical: shutting_down() uses this to prevent
premature session close while response DATA is still in rustls’s
buffer (accepted by socket_write_vectored but not yet on the wire).
Does NOT check per-stream back.out/back.blocks; use
Self::has_pending_write_full on paths that must honour
LIFECYCLE invariant 16 (e.g. shutdown-drain).
Sourcepub fn has_pending_write_full<L>(&self, context: &Context<L>) -> boolwhere
L: ListenerHandler + L7ListenerHandler,
pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> boolwhere
L: ListenerHandler + L7ListenerHandler,
Connection-level Self::has_pending_write extended with a per-stream
back-buffer probe (LIFECYCLE §9 invariant 16). Used by shutdown-drain
paths that must not close while any open stream still has outbound
kawa bytes queued — a voluntary scheduler yield can leave back.out
or back.blocks non-empty without expect_write being set.
Sourcepub fn flush_output_buffer(&mut self)
pub fn flush_output_buffer(&mut self)
Directly flush the ordered output queue to the socket without going through the full writable() path. Used during shutdown when the event loop won’t deliver new epoll events for this session (edge-triggered).
Whatever is queued can go at any time: every entry is a whole frame (or the rest of one a stalled write cut), queued in wire order, and the queue shares nothing with the read side (#1604). A stream parked by a stalled write keeps its park; only its already-started frame lives in the queue.
pub fn close<E, L>(&mut self, context: &mut Context<L>, endpoint: E)
Sourcepub fn new(
session_ulid: Ulid,
socket: Front,
position: Position,
buffers: &mut dyn BufferSource,
flood_config: H2FloodConfig,
connection_config: H2ConnectionConfig,
stream_idle_timeout: Duration,
graceful_shutdown_deadline: Option<Duration>,
timeout_duration: Duration,
expect_read: Option<(H2StreamId, usize)>,
readiness_interest: Ready,
) -> Option<Self>
pub fn new( session_ulid: Ulid, socket: Front, position: Position, buffers: &mut dyn BufferSource, flood_config: H2FloodConfig, connection_config: H2ConnectionConfig, stream_idle_timeout: Duration, graceful_shutdown_deadline: Option<Duration>, timeout_duration: Duration, expect_read: Option<(H2StreamId, usize)>, readiness_interest: Ready, ) -> Option<Self>
Wrap a freshly built core around the socket it will speak through.
The peer_addr() call is here because this is the layer that has a
socket to ask. It is still the one read of it on the connection’s whole
lifetime — ConnectionH2::peer_address documents why the answer is
snapshotted rather than re-read — and it now happens one layer out,
before the core exists rather than inside its constructor.
Source§impl<Front: SocketHandler> H2Shell<Front>
The socket shell of the H2 data path.
impl<Front: SocketHandler> H2Shell<Front>
The socket shell of the H2 data path.
Every production self.socket in this file is in this block and its
sibling above it, and ConnectionH2 has none. H2Shell::readable and
H2Shell::write_streams are the two byte movers: each is the caller that
sits between one of the core’s two poll/handle pairs —
ConnectionH2::poll_read_target with ConnectionH2::handle_read, and
ConnectionH2::poll_write_target with ConnectionH2::handle_write.
All four are pub, and so are the buffers they name
(ConnectionH2::read_space, ConnectionH2::write_buffer), so a driver
outside this crate can stand exactly where these two functions stand.
sim/tests/h2_simulation.rs is that driver, and it is what makes the claim
measurable rather than asserted.
H2Shell::writable is the third member, and the only socket it touches it
touches through the three TLS seams — H2Shell::tls_wants_write,
H2Shell::flush_tls_records and H2Shell::begin_tls_close — whose bodies
live here too. It drives two more poll/handle pairs:
ConnectionH2::flush_pending_control_frames with
ConnectionH2::handle_control_flush, and
ConnectionH2::dispatch_writable_state with
ConnectionH2::dispatch_writable_state_after_flush. Neither takes a
Front: the (H2State, Position) dispatch and the control-frame walk are
both drivable without a socket, which is the whole claim.
The fourth pair is ConnectionH2::force_disconnect with
ConnectionH2::force_disconnect_after_query, and it is the one that
cannot ride a return value — see H2ForceDisconnectTarget. H2Shell::settled
is where it lands, and every MuxResult a core step hands this block goes
through it.
§Why write_streams stays private, and where the unsafe windows are
H2Shell::write_streams drives the Vec<IoSlice<'static>> that brackets
h2_transmit::gather and h2_transmit::confirm, and it drives it because
the loop that opens and closes that bracket lives with it. The vector
itself is the shell’s private io_slices field, so its capacity outlives
the pass while no descriptor does. There are TWO
unsafe blocks on this path and they nest:
- the INNER one is
gather’sslice::from_raw_parts, which re-labels borrows ofkawa.storageas'static. It opens and closes inside that function, exactly as it always did. - the OUTER one is the
unsafe { … }around thegatherCALL below, which exists becausegatherispub unsafe fn: the lifetime obligation is stated in its signature and discharged by the caller. It wraps the call and nothing else — not the vectored write, not theconfirm. It is the onlyunsafeblock in production code in this file.
The window did not widen when write_streams moved onto H2Shell, nor
when the vector moved into a field of it. It still opens at that gather
call and closes at the confirm three statements later, where
io_slices.clear() runs before the consume which may relocate the storage.
The only thing entered while the descriptors are live is
SocketHandler::socket_write_vectored, which reborrows them for the
duration of the call and cannot retain them. No IoSlice<'static>
survives one iteration of the loop: what the field keeps between passes is
capacity, and the vector is empty whenever write_streams is not inside
that bracket. gather clears it on entry as well, so even a descriptor
stranded by a panic out of the vectored write is discarded before anything
could read it. What changed is the
receiver, from &mut ConnectionH2<Front> to &mut H2Shell<Front>, and the
buffer now arrives through ConnectionH2::write_buffer instead of the
free function — neither of which moves either end of the bracket.
That the bracket does not span the poll/handle seam matters MORE now, not
less: the seam is pub, the buffers it names are pub, and an
out-of-crate driver really does exist, so halves on opposite sides of it
could be separated by a caller this crate cannot see. write_streams stays
private for that reason, on H2Shell exactly as it was on ConnectionH2.
h2_transmit’s pair is exported — gather as pub unsafe fn, confirm
as a plain pub fn that produces no descriptor and so leaves no
obligation — which moves the same argument onto that driver’s own bracket
rather than dissolving it. H2Harness::write_streams in
sim/tests/h2_simulation.rs opens and closes its own bracket inside its
own single function, for the same reason and with the same shape.
Sourcepub fn force_disconnect(&mut self) -> MuxResult
pub fn force_disconnect(&mut self) -> MuxResult
Answer a core step, settling any H2ForceDisconnectTarget it parked
on the way.
The one place the socket answers a force-disconnect, and the reason
ConnectionH2::force_disconnect can be raised from seventeen sites
that hold no socket. Every MuxResult a core step hands this impl
block leaves through here, which is what makes the discipline
greppable rather than asserted: a core call in this block whose type is
MuxResult and which does not appear as this function’s argument is
a missed settlement.
result is the core’s provisional answer and survives untouched when
nothing was parked — the overwhelmingly common case, since only a
force-disconnect parks anything.
ConnectionH2::force_disconnect, settled.
The public spelling of the decision: the core raises it and this reads the query it could not. Every out-of-shell caller goes through here.
Sourcepub fn graceful_goaway(&mut self, now: Instant) -> MuxResult
pub fn graceful_goaway(&mut self, now: Instant) -> MuxResult
ConnectionH2::graceful_goaway, settled.
Reaches ConnectionH2::force_disconnect through
ConnectionH2::send_initial_goaway’s serialisation-failure arm.
Sourcepub fn goaway_before_forced_close<L>(
&mut self,
context: &Context<L>,
) -> MuxResultwhere
L: ListenerHandler + L7ListenerHandler,
pub fn goaway_before_forced_close<L>(
&mut self,
context: &Context<L>,
) -> MuxResultwhere
L: ListenerHandler + L7ListenerHandler,
ConnectionH2::goaway_before_forced_close, settled.
Reaches ConnectionH2::force_disconnect through the
serialisation-failure arm of the final GOAWAY.
Sourcepub fn start_stream<L>(
&mut self,
stream: usize,
context: &mut Context<L>,
) -> boolwhere
L: ListenerHandler + L7ListenerHandler,
pub fn start_stream<L>(
&mut self,
stream: usize,
context: &mut Context<L>,
) -> boolwhere
L: ListenerHandler + L7ListenerHandler,
ConnectionH2::start_stream, settled.
Answers the core’s bool unchanged. The settlement is still required
and still belongs HERE: the core reaches
ConnectionH2::graceful_goaway from its stream-id-exhaustion arm, and
the readiness that settlement writes is read by Mux before it decides
whether this connection gets a writable at all.
Sourcepub fn cancel_timed_out_streams<E, L>(
&mut self,
context: &mut Context<L>,
endpoint: &mut E,
)
pub fn cancel_timed_out_streams<E, L>( &mut self, context: &mut Context<L>, endpoint: &mut E, )
ConnectionH2::cancel_timed_out_streams, settled.
The core answers nothing and already discards the flood-violation
MuxResult its ConnectionH2::enqueue_rst sweep can raise — see its
own comment. That discard is preserved: what this adds is the readiness
half of a force-disconnect that discard used to leave completed.
Sourcepub fn end_stream<L>(&mut self, stream_gid: usize, context: &mut Context<L>)where
L: ListenerHandler + L7ListenerHandler,
pub fn end_stream<L>(&mut self, stream_gid: usize, context: &mut Context<L>)where
L: ListenerHandler + L7ListenerHandler,
ConnectionH2::end_stream, forwarded and drained.
No settlement: ConnectionH2::end_stream cannot reach
ConnectionH2::force_disconnect. It is here only because
Connection’s forward! needs one name on both arms.
It still has to drain. The core’s end_stream serialises a
RST_STREAM(CANCEL) for a stream the peer abandoned, and that is the one
producer this file reaches through a public entry point that settles
neither through H2Shell::settled nor through H2Shell::settle.
Sourcepub fn readable<E, L>(
&mut self,
context: &mut Context<L>,
endpoint: E,
) -> MuxResult
pub fn readable<E, L>( &mut self, context: &mut Context<L>, endpoint: E, ) -> MuxResult
Drive one frontend read pass.
The core lives in Self::poll_read_target and Self::handle_read,
both pub; this function is the caller that sits between them, and its
self.socket.socket_read is the only socket touch on the whole H2 read
path. It lives in the shell impl rather than among the core impls for
the reason stated there: the socket touch sits where the socket does,
and the frame state machine between the two calls is drivable without
one, exactly as h2_transmit::gather/confirm already bracket the
vectored write.
Sourcepub fn writable<E, L>(
&mut self,
context: &mut Context<L>,
endpoint: E,
) -> MuxResult
pub fn writable<E, L>( &mut self, context: &mut Context<L>, endpoint: E, ) -> MuxResult
Drive one frontend write pass.
The core lives in Self::flush_pending_control_frames,
Self::dispatch_writable_state and
Self::dispatch_writable_state_after_flush; this function is the
caller that sits between them, and every TLS seam call this function’s
own body used to make inline is here. It lives in the shell impl rather
than among the core impls for the reason stated there: the socket
touches sit where the socket does, and the (H2State, Position)
dispatch between them is drivable without one.
The pass performs the query / flush / query triple h2_close names
TWICE, and the second time only for one arm:
- the preamble — read
Self::tls_wants_write, flush if it says yes. It pushes bytes for every state, which is why it is unconditional rather than owned by a close arm. - a FRESH read of the same query, which is that preamble’s post-flush
answer and the pre-flush question both close arms decide on. It is
handed to
Self::dispatch_writable_stateas an input. - for
H2WritableStateTarget::Flushalone — theH2State::GoAwayarm —Self::flush_tls_recordsagain, a third read, andSelf::dispatch_writable_state_after_flush.
Each read is a separate let, and the third shadows the second inside
the Flush arm only. That is the whole guard against the confusion
h2_close::TlsFlushPhase exists to prevent: “does rustls hold records”
and “did the kernel take them” are two questions, the second cannot be
read off the first, and a core that reused one answer for both would
close a connection with records pending.
The tick count is load-bearing: the flush and the re-query happen
within THIS call and the pass does not return Continue to be called
again, which a_flush_that_succeeds_closes_within_one_writable_call
pins. See h2_close’s module doc for why.
Trait Implementations§
Source§impl<Front: SocketHandler> Debug for H2Shell<Front>
Renders the core and nothing else.
impl<Front: SocketHandler> Debug for H2Shell<Front>
Renders the core and nothing else.
Deliberately NOT derived. A derive would render socket through
Front’s own Debug, which for a mio::net::TcpStream is a local
address, a peer address and a file descriptor — precisely what
ConnectionH2’s own Debug stopped rendering, and for the same reasons
it gives. Wrapping the core in a new type must not hand that back through
the wrapper.