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}