Skip to main content

trillium_http/h2/acceptor/
close.rs

1use super::{
2    H2Driver,
3    types::{CloseOutcome, DriverState},
4};
5use crate::{
6    Conn,
7    graceful_close::GracefulClose,
8    h2::{H2Error, H2ErrorCode, stream_state::StreamEvent, transport::H2Transport},
9};
10use futures_lite::io::{AsyncRead, AsyncWrite};
11use std::{
12    io,
13    pin::Pin,
14    task::{Context, Poll},
15};
16
17impl<T> H2Driver<T>
18where
19    T: AsyncRead + AsyncWrite + Unpin + Send,
20{
21    /// Convert the current `close_outcome` into the terminal return of [`Self::drive`]. Must
22    /// only be called after outbound bytes have been flushed. Graceful closes return `None`;
23    /// errors surface as a final `Some(Err(...))` before subsequent polls return `None`.
24    pub(super) fn finish_with_current_outcome(
25        &mut self,
26    ) -> Option<Result<Conn<H2Transport>, H2Error>> {
27        self.finished = true;
28        // Shut the connection swansong down on every exit path, not just peer GOAWAY:
29        // it is the signal reuse decisions key on (`open_stream`, `can_open_stream`, and
30        // pooling clients' classify). A driver that dies on an I/O error or peer FIN while
31        // leaving the swansong running turns the connection into a landmine — a pool hands
32        // it out as available, `open_stream` publishes a stream no driver will ever
33        // service, and the response waiter parks forever. Must happen *before* the shared-
34        // map walk below: `open_stream` checks the swansong under the streams lock, so any
35        // stream that slips in concurrently either observes the shutdown and is refused,
36        // or was published before we take the lock and gets reset by the walk.
37        self.connection.shut_down();
38        // Complete every outstanding `H2Connection::send_ping` future with an error so
39        // awaiting callers don't block forever. Safe to call regardless of outcome —
40        // a no-op if no pings are in flight.
41        self.connection.fail_pending_pings(
42            io::ErrorKind::ConnectionAborted,
43            "h2 connection closed before PING ACK",
44        );
45        // Wake any `PeerSettings` waiters so a peer that disconnects without ever sending
46        // SETTINGS doesn't strand them. Their `poll` rechecks swansong state and returns
47        // Ready; the caller's follow-up operation surfaces the connection-closed error.
48        self.connection.wake_peer_settings_waiters();
49        // Resolve every still-live stream's recv-side waiters. A connection that dies with
50        // an in-flight stream (server GOAWAY + close, peer FIN, I/O error) leaves any task
51        // parked on the response — `response_headers`, a body `poll_read`, an upgrade
52        // `poll_write` — with no other wake source. Without this a client request hangs
53        // forever on a graceful server shutdown. Mirror the per-stream RST teardown:
54        // terminal `Reset` (recv reports eof → `ResponseHeaders` yields `ConnectionAborted`,
55        // reads return EOF, writes `BrokenPipe`) + the same waker fan-out.
56        let reset_code = match &self.close_outcome {
57            Some(CloseOutcome::Protocol(code)) => *code,
58            _ => H2ErrorCode::NoError,
59        };
60        // Walk the *shared* map, not the driver-private mirror: a client stream published
61        // between the driver's last pickup pass and this teardown exists only in the shared
62        // map, and every conn-task waiter (`response_headers`, body reads, `SubmitSend`)
63        // reaches its stream through the shared map — so this walk covers all of them.
64        for state in self.connection.streams_lock().values() {
65            // Move each still-live stream to `Closed{Reset}` (a no-op on streams already closed, so
66            // an existing reason isn't clobbered), then fan out every recv/send waker so parked
67            // tasks observe the close instead of hanging.
68            let _ = state.apply_event(StreamEvent::RecvReset(reset_code));
69            state.recv.waker.wake();
70            state.recv.response_headers_waker.wake();
71            state.send.outbound_write_waker.wake();
72            // A handler already parked in `SubmitSend` (response staged, awaiting the driver to
73            // frame it) needs this wake to re-poll and observe the now-reset stream — the recv
74            // fan-out above doesn't reach the send-completion waiter.
75            state.send.completion_waker.wake();
76        }
77        match self.close_outcome.take() {
78            None | Some(CloseOutcome::Graceful) => None,
79            Some(CloseOutcome::Protocol(code)) => Some(Err(H2Error::Protocol(code))),
80            Some(CloseOutcome::Io(e)) => Some(Err(H2Error::Io(e))),
81        }
82    }
83
84    /// Enter the closing state: record the outcome and queue a GOAWAY (only for outcomes
85    /// that warrant one). The main loop will drain `write_buf` and then finish.
86    pub(super) fn begin_close(&mut self, outcome: CloseOutcome) {
87        // Idempotent: with the recv pump now running in Closing (so we keep
88        // decoding inbound frames for in-flight streams across GOAWAY), a peer
89        // GOAWAY arriving after we've already begun closing would otherwise
90        // re-queue our own GOAWAY and re-enter Closing, ping-ponging forever
91        // with a peer that mirrors the behavior.
92        if self.state == DriverState::Closing || self.state == DriverState::Drained {
93            log::trace!(
94                "h2 driver: begin_close({outcome:?}) — already in {:?}, ignoring",
95                self.state,
96            );
97            return;
98        }
99        // Don't overwrite a prior outcome (e.g. if an error fires in the middle of a
100        // graceful shutdown, keep the error).
101        let code = match &outcome {
102            CloseOutcome::Graceful => Some(H2ErrorCode::NoError),
103            CloseOutcome::Protocol(code) => Some(*code),
104            CloseOutcome::Io(_) => None,
105        };
106        let reason = match &outcome {
107            CloseOutcome::Graceful => "graceful close",
108            CloseOutcome::Protocol(_) => "protocol error",
109            CloseOutcome::Io(_) => "i/o error",
110        };
111        if self.close_outcome.is_none() {
112            self.close_outcome = Some(outcome);
113        }
114        if let Some(code) = code {
115            self.queue_goaway(self.last_peer_stream_id, code);
116        }
117        self.set_state(DriverState::Closing, reason);
118    }
119
120    /// Log which in-flight streams are blocking the `Closing → Drained` transition.
121    /// Called from the closing-state check when at least one predicate (`has_active_send_cursors`
122    /// or `has_pending_recv`) is still true, so a trace log shows exactly which streams the
123    /// driver is waiting on.
124    pub(super) fn log_closing_blockers(&self) {
125        if !log::log_enabled!(log::Level::Trace) {
126            return;
127        }
128        for (id, entry) in &self.streams {
129            let lifecycle = *entry.shared.lifecycle_lock();
130            let queued = !entry
131                .shared
132                .send
133                .queue
134                .lock()
135                .expect("send queue mutex poisoned")
136                .is_empty();
137            if entry.send.is_some() || queued || !lifecycle.recv_closed() {
138                log::trace!(
139                    "h2 driver: Closing — stream {id} blocking drain (lifecycle={lifecycle:?}, \
140                     cursor_present={}, queued={queued})",
141                    entry.send.is_some(),
142                );
143            }
144        }
145    }
146
147    /// Post-GOAWAY, drain whatever inbound bytes are *immediately* available from the
148    /// peer so our Drop sends a clean FIN (no unread data → no TCP RST) while the peer
149    /// sees the GOAWAY we just emitted. Read loops internally: consume each Ready chunk,
150    /// discard it, ask for more. Exits as soon as the transport returns `Pending` (no
151    /// bytes available right now) OR `Ready(0)` (peer FIN already arrived) OR any error.
152    ///
153    /// Does **not** register the waker on `Pending` — we're actively closing, not
154    /// observing the peer. Bytes the peer sends after this returns are [`Self::close`]'s to
155    /// wait out; dropping the transport instead answers them with a TCP reset.
156    ///
157    /// Returning `Ready(())` unconditionally (no `Pending` case) lets the caller finalize
158    /// immediately. The `Poll` wrapper is kept for symmetry with the rest of the driver's
159    /// poll-style methods.
160    pub(super) fn poll_drain_peer(&mut self, cx: &mut Context<'_>) -> Poll<()> {
161        // A peer flooding us with bytes could keep this loop going a long time. Cap it
162        // so a pathological client can't pin our close-out forever.
163        const MAX_DISCARD_ITERATIONS: usize = 256;
164        // Lightweight scratch — we're throwing it away. 512 balances "drain in few
165        // iterations" against "don't hold a large buffer for a rare path."
166        let mut scratch = [0u8; 512];
167        for _ in 0..MAX_DISCARD_ITERATIONS {
168            // We pass `cx` through for the benefit of the transport's `poll_read` contract,
169            // but we *interpret* `Pending` as "done draining" rather than parking on it —
170            // we're actively closing, not observing.
171            match Pin::new(&mut self.transport).poll_read(cx, &mut scratch) {
172                Poll::Ready(Ok(0) | Err(_)) | Poll::Pending => {
173                    return Poll::Ready(());
174                }
175                Poll::Ready(Ok(_)) => {}
176            }
177        }
178        Poll::Ready(())
179    }
180
181    /// Close the connection: shut down our sending side of the transport, then read and discard
182    /// whatever the peer still sends until it closes its side.
183    ///
184    /// If the transport is dropped while the peer is still sending, the operating system answers
185    /// the peer's data with a TCP reset, which can destroy data the peer hasn't read yet — our
186    /// final GOAWAY, or the end of a response. Waiting for the peer to close avoids that. A peer
187    /// that never closes keeps this waiting indefinitely, so bound it with a timeout.
188    ///
189    /// Call this once [`Self::next`] returns `None` or an error. Closing a driver that hasn't
190    /// finished ends the connection without a GOAWAY, resetting any streams still in flight.
191    pub async fn close(mut self) {
192        if !self.finished {
193            self.finish_with_current_outcome();
194        }
195
196        let reads_per_yield = self.config.copy_loops_per_yield();
197        GracefulClose::new(self.transport, self.read_buf, reads_per_yield).await;
198    }
199}