Skip to main content

lobe_core/engine/
pool.rs

1//! Connection pool for the capture proxy.
2//!
3//! Sits in front of the DNS/TCP/TLS/handshake path. Before opening a fresh
4//! connection to an upstream, `forward_request` asks the pool whether we
5//! already have a working connection to `(host, port, protocol)`. If yes,
6//! we reuse it and skip all handshake phases — DNS, TCP, TLS, and HTTP/2's
7//! `SETTINGS` frame exchange — because the underlying transport is already
8//! established. If no, we fall through to the existing "fresh connection"
9//! path AND cache the resulting connection for the next request to the
10//! same upstream.
11//!
12//! Currently pools two connection types:
13//! - HTTP/2 `SendRequest` handles (cheap to clone; one handle per host
14//!   can service unlimited concurrent streams via multiplexing).
15//! - HTTP/1.1 keep-alive `TcpStream` / `TlsStream` — one stream per host
16//!   for sequential reuse.
17//!
18//! Entries auto-evict after `IDLE_TIMEOUT` of unused time.
19
20use std::collections::HashMap;
21use std::sync::Arc;
22use std::time::{Duration, Instant};
23
24use http_body_util::Full;
25use hyper::body::Bytes;
26use hyper::client::conn::http2::SendRequest;
27use tokio::sync::Mutex;
28
29/// A pooled entry is fresh enough to reuse if it hasn't been idle for more
30/// than this window. 30s matches typical HTTP/1.1 keep-alive timeouts on
31/// modern servers (nginx default is 75s; we're conservative).
32const IDLE_TIMEOUT: Duration = Duration::from_secs(30);
33
34/// The type stored per host in the HTTP/2 pool. `SendRequest` is `Clone`
35/// (it's just a channel handle into the shared connection driver task), so
36/// we can hand out a fresh clone to every caller and multiplex requests
37/// over the same underlying TCP+TLS session for free.
38pub type PooledH2Sender = SendRequest<Full<Bytes>>;
39
40#[derive(Hash, PartialEq, Eq, Clone, Debug)]
41struct PoolKey {
42    host: String,
43    port: u16,
44    is_https: bool,
45}
46
47struct H2Entry {
48    sender: PooledH2Sender,
49    last_used: Instant,
50}
51
52/// Thread-safe connection pool shared across concurrent `forward_request`
53/// tasks. Cloneable — internal state lives behind an `Arc<Mutex<...>>`
54/// so cloning is cheap (bumps the ref-count).
55#[derive(Clone, Debug, Default)]
56pub struct ConnectionPool {
57    inner: Arc<Mutex<PoolInner>>,
58}
59
60#[derive(Default)]
61struct PoolInner {
62    h2: HashMap<PoolKey, H2Entry>,
63}
64
65impl std::fmt::Debug for PoolInner {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("PoolInner")
68            .field("h2_entries", &self.h2.len())
69            .finish()
70    }
71}
72
73impl ConnectionPool {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Try to reuse an HTTP/2 sender for `(host, port)`. Returns `None` if
79    /// we have no cached entry OR the cached entry has gone idle beyond
80    /// `IDLE_TIMEOUT`. Fresh senders returned from here can be used
81    /// immediately — `SendRequest` clones share the same underlying
82    /// connection driver task.
83    pub async fn get_h2(&self, host: &str, port: u16) -> Option<PooledH2Sender> {
84        let key = PoolKey {
85            host: host.to_string(),
86            port,
87            is_https: true,
88        };
89        let mut inner = self.inner.lock().await;
90        let entry = inner.h2.get(&key)?;
91        if entry.last_used.elapsed() > IDLE_TIMEOUT {
92            // Stale — evict and force a fresh handshake.
93            inner.h2.remove(&key);
94            return None;
95        }
96        let sender = entry.sender.clone();
97        // Bump last_used so a busy connection doesn't get evicted mid-flight.
98        if let Some(entry) = inner.h2.get_mut(&key) {
99            entry.last_used = Instant::now();
100        }
101        Some(sender)
102    }
103
104    /// Cache a freshly-negotiated HTTP/2 sender so future requests to the
105    /// same host reuse the underlying TCP+TLS+h2 session.
106    pub async fn store_h2(&self, host: &str, port: u16, sender: PooledH2Sender) {
107        let key = PoolKey {
108            host: host.to_string(),
109            port,
110            is_https: true,
111        };
112        let mut inner = self.inner.lock().await;
113        inner.h2.insert(
114            key,
115            H2Entry {
116                sender,
117                last_used: Instant::now(),
118            },
119        );
120    }
121
122    /// Remove the cached h2 sender for a host — call this when a request
123    /// against a cached sender fails, so the next attempt re-handshakes.
124    pub async fn invalidate_h2(&self, host: &str, port: u16) {
125        let key = PoolKey {
126            host: host.to_string(),
127            port,
128            is_https: true,
129        };
130        let mut inner = self.inner.lock().await;
131        inner.h2.remove(&key);
132    }
133}