Skip to main content

h2ts_client/
pool.rs

1//! A pool of HTTP/2-over-WebSocket connections — Go-style multi-connection
2//! parallelism (`golang.org/x/net/http2` with `StrictMaxConcurrentStreams =
3//! false`). Each request is routed to a connection that still has a free stream
4//! slot (per the peer's SETTINGS_MAX_CONCURRENT_STREAMS); when all are saturated,
5//! a new connection is opened.
6//!
7//! The routing logic here is transport-agnostic and host-testable; the wasm
8//! `connect_pool` (in `web.rs`) wires it to real `connect_websocket` connections.
9
10use std::cell::RefCell;
11use std::future::Future;
12use std::rc::Rc;
13
14use futures::channel::oneshot;
15use futures::future::LocalBoxFuture;
16use futures::FutureExt;
17
18use crate::connection::{H2Connection, RequestInit, Response};
19use crate::errors::H2Error;
20
21/// A connection a [`H2Pool`] can route over. Implemented for [`H2Connection`];
22/// tests supply their own.
23pub trait PoolConnection {
24    /// Whether the connection has been torn down.
25    fn is_closed(&self) -> bool;
26    /// Whether a new request would stay within the peer's advertised
27    /// SETTINGS_MAX_CONCURRENT_STREAMS.
28    fn can_open_stream(&self) -> bool;
29    /// Issue a request on this connection.
30    fn request(&self, init: RequestInit) -> LocalBoxFuture<'static, Result<Response, H2Error>>;
31    /// Gracefully close the connection.
32    fn close(&self);
33}
34
35impl PoolConnection for H2Connection {
36    fn is_closed(&self) -> bool {
37        H2Connection::is_closed(self)
38    }
39    fn can_open_stream(&self) -> bool {
40        H2Connection::can_open_stream(self)
41    }
42    fn request(&self, init: RequestInit) -> LocalBoxFuture<'static, Result<Response, H2Error>> {
43        let conn = self.clone();
44        async move { conn.request(init).await }.boxed_local()
45    }
46    fn close(&self) {
47        H2Connection::close(self)
48    }
49}
50
51type Factory = Rc<dyn Fn() -> LocalBoxFuture<'static, Result<Rc<dyn PoolConnection>, H2Error>>>;
52
53struct PoolState {
54    conns: Vec<Rc<dyn PoolConnection>>,
55    /// One connection is opened at a time; concurrent requests that need a new
56    /// connection wait on `open_waiters` rather than each opening their own.
57    opening: bool,
58    open_waiters: Vec<oneshot::Sender<()>>,
59}
60
61/// A pool of HTTP/2 connections. Cheap to clone (shares one `Rc` state).
62#[derive(Clone)]
63pub struct H2Pool {
64    shared: Rc<RefCell<PoolState>>,
65    factory: Factory,
66    max_connections: usize,
67}
68
69impl H2Pool {
70    /// Build a pool from a connection `factory` (each call opens a fresh
71    /// connection). `max_connections` caps how many connections are opened
72    /// (`usize::MAX` for unbounded); past the cap, requests park on an existing
73    /// connection (which queues them internally until a stream slot frees).
74    pub fn new<F, Fut>(factory: F, max_connections: usize) -> Self
75    where
76        F: Fn() -> Fut + 'static,
77        Fut: Future<Output = Result<Rc<dyn PoolConnection>, H2Error>> + 'static,
78    {
79        Self {
80            shared: Rc::new(RefCell::new(PoolState {
81                conns: Vec::new(),
82                opening: false,
83                open_waiters: Vec::new(),
84            })),
85            factory: Rc::new(move || factory().boxed_local()),
86            max_connections,
87        }
88    }
89
90    /// Route a request to a connection with a free stream slot, opening a new
91    /// connection if all existing ones are saturated.
92    pub async fn request(&self, init: RequestInit) -> Result<Response, H2Error> {
93        // RequestInit isn't Clone (a body may be a stream), so issue it exactly once.
94        let mut init = Some(init);
95        loop {
96            let chosen: Option<Rc<dyn PoolConnection>> = {
97                let mut st = self.shared.borrow_mut();
98                st.conns.retain(|c| !c.is_closed());
99                if let Some(c) = st.conns.iter().find(|c| c.can_open_stream()) {
100                    Some(c.clone())
101                } else if !st.conns.is_empty() && st.conns.len() >= self.max_connections {
102                    // At the connection cap: park on an existing connection.
103                    st.conns.first().cloned()
104                } else {
105                    None
106                }
107            };
108            if let Some(conn) = chosen {
109                return conn.request(init.take().expect("request issued once")).await;
110            }
111
112            // All connections are full (and under the cap): open a new one.
113            self.open_one().await?;
114
115            // A brand-new connection already at its limit (a server advertising a
116            // tiny limit): park on it rather than opening unboundedly many.
117            let fresh = self.shared.borrow().conns.last().cloned();
118            if let Some(conn) = fresh {
119                if !conn.is_closed() && !conn.can_open_stream() {
120                    return conn.request(init.take().expect("request issued once")).await;
121                }
122            }
123        }
124    }
125
126    /// Open exactly one connection; concurrent callers await the in-flight open.
127    async fn open_one(&self) -> Result<(), H2Error> {
128        let wait = {
129            let mut st = self.shared.borrow_mut();
130            if st.opening {
131                let (tx, rx) = oneshot::channel();
132                st.open_waiters.push(tx);
133                Some(rx)
134            } else {
135                st.opening = true;
136                None
137            }
138        };
139        if let Some(rx) = wait {
140            let _ = rx.await; // another request is opening; re-check once it lands
141            return Ok(());
142        }
143
144        // We own the open.
145        let result = (self.factory)().await;
146        let mut st = self.shared.borrow_mut();
147        st.opening = false;
148        for tx in st.open_waiters.drain(..) {
149            let _ = tx.send(());
150        }
151        let conn = result?;
152        st.conns.push(conn);
153        Ok(())
154    }
155
156    /// The number of live connections currently in the pool.
157    pub fn connections(&self) -> usize {
158        self.shared
159            .borrow()
160            .conns
161            .iter()
162            .filter(|c| !c.is_closed())
163            .count()
164    }
165
166    /// Gracefully close every connection in the pool.
167    pub fn close(&self) {
168        for conn in self.shared.borrow_mut().conns.drain(..) {
169            conn.close();
170        }
171    }
172}