rws_proxy/proxy/pool.rs
1use std::collections::{HashMap, VecDeque};
2use std::net::TcpStream;
3use std::sync::Mutex;
4use std::time::{Duration, Instant};
5
6const DEFAULT_MAX_IDLE: usize = 8;
7/// How long the proxy's own pools keep an idle backend connection: shorter
8/// than the 30 s after which an rws backend closes an idle keep-alive
9/// connection, so a pooled one is normally still open. Backends that close
10/// sooner are caught by the liveness check before reuse (`UpstreamIo::peer_closed`).
11pub(crate) const IDLE_TIMEOUT: Duration = Duration::from_secs(25);
12
13/// Per-backend HTTP/1.1 connection pool of plain TCP connections — the pool
14/// `ReverseProxy::with_pool` takes. (HTTPS backends use an internal
15/// `Pool` of TLS streams.)
16pub type ConnPool = Pool<TcpStream>;
17
18/// Per-backend HTTP/1.1 connection pool, generic over the connection type.
19///
20/// Holds idle connections keyed by `"host:port"`. When a
21/// backend responds with `Connection: keep-alive` (or the HTTP/1.1 default),
22/// the stream is returned here and reused for the next request to the same
23/// backend, eliminating the TCP-handshake cost and reducing ephemeral-port
24/// exhaustion under load.
25///
26/// # Thread safety
27///
28/// All methods take `&self` and are safe to call from multiple threads.
29/// The inner map is protected by a `Mutex`.
30///
31/// # Example
32///
33/// ```rust,ignore
34/// # extern crate rws_proxy as rust_web_server;
35/// use std::sync::Arc;
36/// use std::time::Duration;
37/// use rust_web_server::proxy::ConnPool;
38/// use rust_web_server::proxy::ReverseProxy;
39///
40/// let pool = Arc::new(ConnPool::new(16, Duration::from_secs(30)));
41/// let _proxy = ReverseProxy::new(["http://backend:8080"])
42/// .with_pool(Arc::clone(&pool));
43/// ```
44pub struct Pool<S> {
45 inner: Mutex<HashMap<String, VecDeque<PoolEntry<S>>>>,
46 max_idle: usize,
47 idle_timeout: Duration,
48}
49
50struct PoolEntry<S> {
51 stream: S,
52 added: Instant,
53}
54
55impl<S> Pool<S> {
56 /// Create a pool with the given per-backend idle limit and idle timeout.
57 pub fn new(max_idle: usize, idle_timeout: Duration) -> Self {
58 Pool {
59 inner: Mutex::new(HashMap::new()),
60 max_idle,
61 idle_timeout,
62 }
63 }
64
65 /// Create a pool with defaults: 8 idle connections per backend, 25-second timeout.
66 pub fn new_default() -> Self {
67 Self::new(DEFAULT_MAX_IDLE, IDLE_TIMEOUT)
68 }
69
70 /// Try to acquire an idle connection for `key = "host:port"`.
71 ///
72 /// Stale entries (older than `idle_timeout`) are discarded automatically.
73 /// Returns `None` if no usable connection is available.
74 pub fn acquire(&self, key: &str) -> Option<S> {
75 let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
76 let queue = map.get_mut(key)?;
77 let now = Instant::now();
78 while let Some(entry) = queue.pop_front() {
79 if now.duration_since(entry.added) < self.idle_timeout {
80 return Some(entry.stream);
81 }
82 // stale — drop, which closes the TCP connection
83 }
84 None
85 }
86
87 /// Return a keep-alive connection to the pool.
88 ///
89 /// If the backend slot is already at `max_idle`, the stream is dropped
90 /// (the TCP connection closes) rather than exceeding the limit.
91 pub fn release(&self, key: &str, stream: S) {
92 let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
93 // The key is copied only the first time a backend is seen.
94 if !map.contains_key(key) {
95 map.insert(key.to_string(), Default::default());
96 }
97 let Some(queue) = map.get_mut(key) else { return };
98 if queue.len() < self.max_idle {
99 queue.push_back(PoolEntry { stream, added: Instant::now() });
100 }
101 // over limit — stream dropped here, closing the connection
102 }
103
104 /// Total idle connections across all backends (useful for testing/metrics).
105 pub fn idle_count(&self) -> usize {
106 let map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
107 map.values().map(|q| q.len()).sum()
108 }
109}