Skip to main content

H2Shell

Struct H2Shell 

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

The socket-free state machine.

§socket: Front

The only socket in the H2 frontend or backend path.

Implementations§

Source§

impl<Front: SocketHandler> H2Shell<Front>

Source

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.

Source

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

Source

pub fn has_pending_write_full<L>(&self, context: &Context<L>) -> bool

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.

Source

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.

Source

pub fn close<E, L>(&mut self, context: &mut Context<L>, endpoint: E)

Source

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.

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’s slice::from_raw_parts, which re-labels borrows of kawa.storage as 'static. It opens and closes inside that function, exactly as it always did.
  • the OUTER one is the unsafe { … } around the gather CALL below, which exists because gather is pub 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 the confirm. It is the only unsafe block 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.

Source

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.

Source

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.

Source

pub fn goaway_before_forced_close<L>( &mut self, context: &Context<L>, ) -> MuxResult

ConnectionH2::goaway_before_forced_close, settled.

Reaches ConnectionH2::force_disconnect through the serialisation-failure arm of the final GOAWAY.

Source

pub fn start_stream<L>( &mut self, stream: usize, context: &mut Context<L>, ) -> bool

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.

Source

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.

Source

pub fn end_stream<L>(&mut self, stream_gid: usize, context: &mut Context<L>)

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.

Source

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.

Source

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:

  1. 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.
  2. 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_state as an input.
  3. for H2WritableStateTarget::Flush alone — the H2State::GoAway arm — Self::flush_tls_records again, a third read, and Self::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.

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.

Source§

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

Formats the value using the given formatter. Read more
Source§

impl<Front: SocketHandler> Drop for H2Shell<Front>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

§

impl<Front> !Freeze for H2Shell<Front>

§

impl<Front> !RefUnwindSafe for H2Shell<Front>

§

impl<Front> !Send for H2Shell<Front>

§

impl<Front> !Sync for H2Shell<Front>

§

impl<Front> !UnwindSafe for H2Shell<Front>

§

impl<Front> Unpin for H2Shell<Front>
where Front: Unpin,

§

impl<Front> UnsafeUnpin for H2Shell<Front>
where Front: UnsafeUnpin,

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.