Skip to main content

trillium_http/
liveness.rs

1use crate::{Buffer, Conn, ProtocolSession, Version};
2use futures_lite::{AsyncRead, AsyncWrite};
3use std::{
4    future::Future,
5    pin::Pin,
6    task::{Context, Poll},
7};
8
9/// Upper bound on bytes buffered from a peer while a handler runs without
10/// reading its request. Beyond this, the socket backs up instead of memory.
11const PROBE_WINDOW_CAP: usize = 16 * 1024;
12
13/// A future that resolves when the peer abandons a single HTTP/3 stream.
14///
15/// HTTP/3 has no connection driver inside trillium-http — QUIC stream resets, `STOP_SENDING`,
16/// and connection loss are all invisible at the `AsyncRead` + `AsyncWrite` seam the h3 code is
17/// written against. The runtime adapter supplies this future at stream accept, the same way it
18/// supplies the stream-reset closure.
19pub type PeerGone = Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>;
20
21/// Resolves once the peer has abandoned this request, by whatever means the protocol makes
22/// observable.
23///
24/// - **HTTP/1.x** — reads the transport; end-of-file or a transport error resolves. Bytes read
25///   accumulate on `buffer` (where the inbound state machine picks them up) until it holds
26///   `read_allowance` bytes, after which the probe goes dormant. A half-closed peer that shut down
27///   its write side but still reads is indistinguishable from a departed one and counts as gone.
28/// - **HTTP/2** — the stream is reset, both halves are complete, or the connection is torn down.
29///   Never reads the transport, so `read_allowance` is unused.
30/// - **HTTP/3** — `peer_gone` resolves: `STOP_SENDING`, stream reset, or connection loss. Never
31///   reads the transport. `None` when the runtime adapter supplied no future, in which case this
32///   never resolves.
33///
34/// Reading the transport is only correct for h1. On h2 and h3 the peer half-closes its send
35/// side as a matter of course — an h2 request carries `END_STREAM` on its HEADERS and an h3
36/// client FINs its half of the bidi stream — so an EOF there is the *normal* state of a live
37/// request, not evidence of anything.
38pub(crate) fn poll_peer_gone<T: AsyncRead + Unpin>(
39    session: &ProtocolSession,
40    version: Version,
41    buffer: &mut Buffer,
42    transport: &mut T,
43    peer_gone: Option<&mut PeerGone>,
44    read_allowance: usize,
45    cx: &mut Context<'_>,
46) -> Poll<()> {
47    match session {
48        ProtocolSession::Http2 {
49            connection,
50            stream_id,
51        } => return connection.poll_stream_closed(*stream_id, cx),
52
53        ProtocolSession::Http3 { .. } => {
54            return match peer_gone {
55                Some(peer_gone) => peer_gone.as_mut().poll(cx),
56                None => Poll::Pending,
57            };
58        }
59
60        ProtocolSession::Http1 => {}
61    }
62
63    // A synthetic conn has no peer to depart, and `Http1` is also the session for h3 conns
64    // before their session is attached; neither should be probed by reading.
65    if !matches!(
66        version,
67        Version::Http0_9 | Version::Http1_0 | Version::Http1_1
68    ) {
69        return Poll::Pending;
70    }
71
72    loop {
73        // A peer that pipelines faster than handlers consume is put under backpressure rather
74        // than buffered without bound; a connection this busy is definitionally alive.
75        let want = read_allowance.saturating_sub(buffer.live_len());
76        if want == 0 {
77            return Poll::Pending;
78        }
79
80        match Pin::new(&mut *transport).poll_read(cx, buffer.window(want)) {
81            Poll::Ready(Ok(0) | Err(_)) => return Poll::Ready(()),
82            Poll::Ready(Ok(n)) => buffer.advance(n),
83            Poll::Pending => return Poll::Pending,
84        }
85    }
86}
87
88pub(crate) struct LivenessFut<'a, T>(&'a mut Conn<T>);
89
90impl<'a, T> LivenessFut<'a, T> {
91    pub(crate) fn new(conn: &'a mut Conn<T>) -> Self {
92        Self(conn)
93    }
94}
95
96impl<T> Future for LivenessFut<'_, T>
97where
98    T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
99{
100    type Output = ();
101
102    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
103        let LivenessFut(Conn {
104            buffer,
105            transport,
106            protocol_session,
107            version,
108            peer_gone,
109            ..
110        }) = &mut *self;
111
112        poll_peer_gone(
113            protocol_session,
114            *version,
115            buffer,
116            transport,
117            peer_gone.as_mut(),
118            PROBE_WINDOW_CAP,
119            cx,
120        )
121    }
122}
123
124pub(crate) struct CancelOnDisconnect<'a, Fut, T>(
125    pub(crate) &'a mut Conn<T>,
126    pub(crate) Pin<&'a mut Fut>,
127);
128impl<'a, Fut, T> Future for CancelOnDisconnect<'a, Fut, T>
129where
130    Fut: Future + Send + 'a,
131    T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
132{
133    type Output = Option<Fut::Output>;
134
135    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
136        let CancelOnDisconnect(conn, fut) = &mut *self;
137        let fut_poll = fut.as_mut().poll(cx);
138        let disconnect = Pin::new(&mut LivenessFut(conn)).poll(cx);
139        match (fut_poll, disconnect) {
140            (Poll::Ready(output), _) => Poll::Ready(Some(output)),
141            (Poll::Pending, Poll::Ready(())) => Poll::Ready(None),
142            (Poll::Pending, Poll::Pending) => Poll::Pending,
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::{HttpContext, ProtocolSession, h3::H3Connection};
151    use futures_lite::io::Cursor;
152    use std::sync::{
153        Arc,
154        atomic::{AtomicBool, Ordering},
155    };
156
157    /// A future that resolves only once `flag` is set, standing in for the runtime adapter's
158    /// QUIC abandonment signal.
159    fn gated(flag: Arc<AtomicBool>) -> PeerGone {
160        Box::pin(std::future::poll_fn(move |cx| {
161            if flag.load(Ordering::SeqCst) {
162                Poll::Ready(())
163            } else {
164                cx.waker().wake_by_ref();
165                Poll::Pending
166            }
167        }))
168    }
169
170    fn h3_session() -> ProtocolSession {
171        ProtocolSession::Http3 {
172            connection: H3Connection::new(Arc::new(HttpContext::new())),
173            stream_id: 0,
174        }
175    }
176
177    /// A transport at EOF is the *normal* state of a live h3 request — the client finishes its
178    /// half of the bidi stream once the request is complete — so the dispatcher must not read
179    /// it. With no abandonment signal supplied, there is nothing to report.
180    #[test]
181    fn h3_ignores_an_eof_transport() {
182        let mut transport = Cursor::new(Vec::new());
183        let mut buffer = Buffer::default();
184        let polled = futures_lite::future::block_on(std::future::poll_fn(|cx| {
185            Poll::Ready(poll_peer_gone(
186                &h3_session(),
187                Version::Http3,
188                &mut buffer,
189                &mut transport,
190                None,
191                16 * 1024,
192                cx,
193            ))
194        }));
195
196        assert!(
197            polled.is_pending(),
198            "an h3 client that finished sending its request has not departed"
199        );
200    }
201
202    #[test]
203    fn h3_reports_departure_only_once_signalled() {
204        let flag = Arc::new(AtomicBool::new(false));
205        let mut peer_gone = gated(flag.clone());
206        let mut transport = Cursor::new(Vec::new());
207        let mut buffer = Buffer::default();
208        let session = h3_session();
209
210        let poll =
211            |peer_gone: &mut PeerGone, transport: &mut Cursor<Vec<u8>>, buffer: &mut Buffer| {
212                futures_lite::future::block_on(std::future::poll_fn(|cx| {
213                    Poll::Ready(poll_peer_gone(
214                        &session,
215                        Version::Http3,
216                        buffer,
217                        transport,
218                        Some(peer_gone),
219                        16 * 1024,
220                        cx,
221                    ))
222                }))
223            };
224
225        assert!(poll(&mut peer_gone, &mut transport, &mut buffer).is_pending());
226
227        flag.store(true, Ordering::SeqCst);
228        assert!(
229            poll(&mut peer_gone, &mut transport, &mut buffer).is_ready(),
230            "once the adapter signals abandonment, the probe must resolve"
231        );
232    }
233}