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 { inner, transport }) = &mut *self;
104
105        poll_peer_gone(
106            &inner.protocol_session,
107            inner.version,
108            &mut inner.buffer,
109            transport,
110            inner.peer_gone.as_mut(),
111            PROBE_WINDOW_CAP,
112            cx,
113        )
114    }
115}
116
117pub(crate) struct CancelOnDisconnect<'a, Fut, T>(
118    pub(crate) &'a mut Conn<T>,
119    pub(crate) Pin<&'a mut Fut>,
120);
121impl<'a, Fut, T> Future for CancelOnDisconnect<'a, Fut, T>
122where
123    Fut: Future + Send + 'a,
124    T: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
125{
126    type Output = Option<Fut::Output>;
127
128    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
129        let CancelOnDisconnect(conn, fut) = &mut *self;
130        let fut_poll = fut.as_mut().poll(cx);
131        let disconnect = Pin::new(&mut LivenessFut(conn)).poll(cx);
132        match (fut_poll, disconnect) {
133            (Poll::Ready(output), _) => Poll::Ready(Some(output)),
134            (Poll::Pending, Poll::Ready(())) => Poll::Ready(None),
135            (Poll::Pending, Poll::Pending) => Poll::Pending,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::{HttpContext, ProtocolSession, h3::H3Connection};
144    use futures_lite::io::Cursor;
145    use std::sync::{
146        Arc,
147        atomic::{AtomicBool, Ordering},
148    };
149
150    /// A future that resolves only once `flag` is set, standing in for the runtime adapter's
151    /// QUIC abandonment signal.
152    fn gated(flag: Arc<AtomicBool>) -> PeerGone {
153        Box::pin(std::future::poll_fn(move |cx| {
154            if flag.load(Ordering::SeqCst) {
155                Poll::Ready(())
156            } else {
157                cx.waker().wake_by_ref();
158                Poll::Pending
159            }
160        }))
161    }
162
163    fn h3_session() -> ProtocolSession {
164        ProtocolSession::Http3 {
165            connection: H3Connection::new(Arc::new(HttpContext::new())),
166            stream_id: 0,
167        }
168    }
169
170    /// A transport at EOF is the *normal* state of a live h3 request — the client finishes its
171    /// half of the bidi stream once the request is complete — so the dispatcher must not read
172    /// it. With no abandonment signal supplied, there is nothing to report.
173    #[test]
174    fn h3_ignores_an_eof_transport() {
175        let mut transport = Cursor::new(Vec::new());
176        let mut buffer = Buffer::default();
177        let polled = futures_lite::future::block_on(std::future::poll_fn(|cx| {
178            Poll::Ready(poll_peer_gone(
179                &h3_session(),
180                Version::Http3,
181                &mut buffer,
182                &mut transport,
183                None,
184                16 * 1024,
185                cx,
186            ))
187        }));
188
189        assert!(
190            polled.is_pending(),
191            "an h3 client that finished sending its request has not departed"
192        );
193    }
194
195    #[test]
196    fn h3_reports_departure_only_once_signalled() {
197        let flag = Arc::new(AtomicBool::new(false));
198        let mut peer_gone = gated(flag.clone());
199        let mut transport = Cursor::new(Vec::new());
200        let mut buffer = Buffer::default();
201        let session = h3_session();
202
203        let poll =
204            |peer_gone: &mut PeerGone, transport: &mut Cursor<Vec<u8>>, buffer: &mut Buffer| {
205                futures_lite::future::block_on(std::future::poll_fn(|cx| {
206                    Poll::Ready(poll_peer_gone(
207                        &session,
208                        Version::Http3,
209                        buffer,
210                        transport,
211                        Some(peer_gone),
212                        16 * 1024,
213                        cx,
214                    ))
215                }))
216            };
217
218        assert!(poll(&mut peer_gone, &mut transport, &mut buffer).is_pending());
219
220        flag.store(true, Ordering::SeqCst);
221        assert!(
222            poll(&mut peer_gone, &mut transport, &mut buffer).is_ready(),
223            "once the adapter signals abandonment, the probe must resolve"
224        );
225    }
226}