Skip to main content

connection_pool/
connection_pool.rs

1use std::collections::VecDeque;
2use std::future::Future;
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5use tokio::sync::{Mutex, Semaphore};
6use tokio::task::JoinHandle;
7use tokio::time::{interval, timeout};
8
9pub const DEFAULT_MAX_SIZE: usize = 10;
10pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); // 5 minutes
11pub const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); // 10 seconds
12pub const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(30); // 30 seconds
13/// Maximum number of *idle* connections kept in the pool, regardless of
14/// `max_size`. Bounding the idle reserve prevents the pool from hoarding dozens
15/// of idle tunnels after a burst (which then all get validated with a 3s Ping
16/// and discarded -> "Connection validation failed" churn).
17pub const MAX_IDLE_KEEP: usize = 8;
18
19/// Configuration for background cleanup task
20#[derive(Clone)]
21pub struct CleanupConfig {
22    pub interval: Duration,
23    pub enabled: bool,
24}
25
26impl Default for CleanupConfig {
27    fn default() -> Self {
28        Self {
29            interval: DEFAULT_CLEANUP_INTERVAL,
30            enabled: true,
31        }
32    }
33}
34
35/// Background cleanup task controller
36struct CleanupTaskController {
37    handle: Option<JoinHandle<()>>,
38    shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
39}
40
41impl CleanupTaskController {
42    fn new() -> Self {
43        Self {
44            handle: None,
45            shutdown_tx: None,
46        }
47    }
48
49    fn start<T, M>(
50        &mut self,
51        connections: Arc<Mutex<VecDeque<InnerConnection<T>>>>,
52        max_idle_time: Duration,
53        cleanup_interval: Duration,
54        manager: Arc<M>,
55        max_size: usize,
56    ) where
57        T: Send + 'static,
58        M: ConnectionManager<Connection = T> + Send + Sync + 'static,
59    {
60        if self.handle.is_some() {
61            log::warn!("Cleanup task is already running");
62            return;
63        }
64        let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
65        self.shutdown_tx = Some(shutdown_tx);
66        let handle = tokio::spawn(async move {
67            let mut interval_timer = interval(cleanup_interval);
68            log::info!("Background cleanup task started with interval: {cleanup_interval:?}");
69
70            loop {
71                tokio::select! {
72                    _ = interval_timer.tick() => {}
73                    _ = &mut shutdown_rx => {
74                        log::info!("Received shutdown signal, exiting cleanup loop");
75                        break;
76                    }
77                };
78
79                // Take the idle set under the lock, then validate OUTSIDE the
80                // lock. The original held the mutex for up to the validation
81                // timeout PER connection, which stalled every acquire while the
82                // cleanup ran (a major cause of the churn-induced stalls).
83                let candidates: VecDeque<_> = {
84                    let mut connections = connections.lock().await;
85                    connections.drain(..).collect()
86                };
87                let initial_count = candidates.len();
88                let now = Instant::now();
89
90                let mut valid_connections = VecDeque::new();
91                let mut candidates = candidates;
92                let mut shutdown_requested = false;
93                while let Some(mut conn) = candidates.pop_front() {
94                    let not_expired = now.duration_since(conn.created_at) < max_idle_time;
95                    let is_valid = if not_expired {
96                        let validation = tokio::select! {
97                            _ = &mut shutdown_rx => None,
98                            is_valid = manager.is_valid(&mut conn.connection) => Some(is_valid),
99                        };
100                        let Some(is_valid) = validation else {
101                            // Cancellation may leave the mutably validated connection in an unknown state.
102                            valid_connections.append(&mut candidates);
103                            shutdown_requested = true;
104                            break;
105                        };
106                        is_valid
107                    } else {
108                        false
109                    };
110                    if not_expired && is_valid {
111                        valid_connections.push_back(conn);
112                    }
113                }
114                let removed_count = initial_count - valid_connections.len();
115
116                // Re-insert the still-valid ones, preserving anything that was
117                // added/returned while we were validating.
118                {
119                    let idle_cap = max_size.min(MAX_IDLE_KEEP);
120                    let mut connections = connections.lock().await;
121                    for conn in valid_connections {
122                        if connections.len() < idle_cap {
123                            connections.push_back(conn);
124                        }
125                    }
126                    log::debug!("Current pool (remaining {}/{idle_cap}) after cleanup", connections.len());
127                }
128
129                if removed_count > 0 {
130                    log::debug!("Background cleanup removed {removed_count} expired/invalid connections");
131                }
132
133                if shutdown_requested {
134                    break;
135                }
136            }
137        });
138
139        self.handle = Some(handle);
140    }
141
142    async fn stop(&mut self) {
143        if let Some(shutdown_tx) = self.shutdown_tx.take() {
144            let _ = shutdown_tx.send(());
145        }
146        if let Some(handle) = self.handle.take() {
147            if let Err(e) = handle.await {
148                log::error!("Error while stopping background cleanup task: {e}");
149            }
150            log::info!("Background cleanup task stopped in async stop");
151        }
152    }
153
154    fn stop_sync(&mut self) {
155        if let Some(shutdown_tx) = self.shutdown_tx.take() {
156            let _ = shutdown_tx.send(());
157        }
158        if let Some(handle) = self.handle.take() {
159            std::thread::sleep(std::time::Duration::from_millis(100));
160            handle.abort();
161            log::info!("Background cleanup task stopped in stop_sync");
162        }
163    }
164}
165
166impl Drop for CleanupTaskController {
167    fn drop(&mut self) {
168        self.stop_sync();
169    }
170}
171
172/// Manager responsible for creating new [`ConnectionManager::Connection`]s or checking existing ones.
173pub trait ConnectionManager: Sync + Send + Clone {
174    /// Type of [`ConnectionManager::Connection`]s that this [`ConnectionManager`] creates and recycles.
175    type Connection: Send;
176
177    /// Error that this [`ConnectionManager`] can return when creating and/or recycling [`ConnectionManager::Connection`]s.
178    type Error: std::error::Error + Send + Sync + 'static;
179
180    /// Future that resolves to a new [`ConnectionManager::Connection`] when created.
181    type CreateFut: Future<Output = Result<Self::Connection, Self::Error>> + Send;
182
183    /// Future that resolves to true if the connection is valid, false otherwise.
184    type ValidFut<'a>: Future<Output = bool> + Send
185    where
186        Self: 'a;
187
188    /// Create a new connection.
189    fn create_connection(&self) -> Self::CreateFut;
190
191    /// Check if a connection is valid.
192    fn is_valid<'a>(&'a self, connection: &'a mut Self::Connection) -> Self::ValidFut<'a>;
193}
194
195/// Connection pool
196pub struct ConnectionPool<M: ConnectionManager> {
197    connections: Arc<Mutex<VecDeque<InnerConnection<M::Connection>>>>,
198    semaphore: Arc<Semaphore>,
199    max_size: usize,
200    manager: M,
201    max_idle_time: Duration,
202    connection_timeout: Duration,
203    cleanup_controller: Arc<Mutex<CleanupTaskController>>,
204    outstanding_count: Arc<std::sync::atomic::AtomicUsize>,
205}
206
207/// Pooled inner connection, used within the connection pool
208struct InnerConnection<T> {
209    pub connection: T,
210    pub created_at: Instant,
211}
212
213/// Pooled managed stream, provided for the outer world usage
214pub struct ManagedConnection<M>
215where
216    M: ConnectionManager + Send + Sync + 'static,
217{
218    connection: Option<M::Connection>,
219    pool: Arc<ConnectionPool<M>>,
220    _permit: tokio::sync::OwnedSemaphorePermit,
221}
222
223impl<M: ConnectionManager> ManagedConnection<M> {
224    /// Consume the managed connection and return the inner connection
225    pub fn into_inner(mut self) -> M::Connection {
226        self.connection.take().unwrap()
227    }
228
229    fn new(connection: M::Connection, pool: Arc<ConnectionPool<M>>, permit: tokio::sync::OwnedSemaphorePermit) -> Self {
230        pool.outstanding_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
231        ManagedConnection {
232            connection: Some(connection),
233            pool,
234            _permit: permit,
235        }
236    }
237}
238
239impl<M> ConnectionPool<M>
240where
241    M: ConnectionManager + Send + Sync + Clone + 'static,
242{
243    /// Create a new connection pool
244    pub fn new(
245        max_size: Option<usize>,
246        max_idle_time: Option<Duration>,
247        connection_timeout: Option<Duration>,
248        cleanup_config: Option<CleanupConfig>,
249        manager: M,
250    ) -> Arc<Self> {
251        let max_size = max_size.unwrap_or(DEFAULT_MAX_SIZE);
252        let max_idle_time = max_idle_time.unwrap_or(DEFAULT_IDLE_TIMEOUT);
253        let connection_timeout = connection_timeout.unwrap_or(DEFAULT_CONNECTION_TIMEOUT);
254        let cleanup_config = cleanup_config.unwrap_or_default();
255
256        log::info!(
257            "Creating connection pool with max_size: {max_size}, idle_timeout: {max_idle_time:?}, connection_timeout: {connection_timeout:?}, cleanup_enabled: {}",
258            cleanup_config.enabled
259        );
260
261        let connections = Arc::new(Mutex::new(VecDeque::new()));
262        let cleanup_controller = Arc::new(Mutex::new(CleanupTaskController::new()));
263
264        let pool = Arc::new(ConnectionPool {
265            connections: connections.clone(),
266            semaphore: Arc::new(Semaphore::new(max_size)),
267            max_size,
268            manager,
269            max_idle_time,
270            connection_timeout,
271            cleanup_controller: cleanup_controller.clone(),
272            outstanding_count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
273        });
274
275        // Start background cleanup task if enabled
276        if cleanup_config.enabled {
277            let manager = Arc::new(pool.manager.clone());
278            tokio::spawn(async move {
279                let mut controller = cleanup_controller.lock().await;
280                controller.start(connections, max_idle_time, cleanup_config.interval, manager, max_size);
281            });
282        }
283
284        pool
285    }
286
287    /// Get a connection from the pool
288    pub async fn get_connection(self: Arc<Self>) -> Result<ManagedConnection<M>, PoolError<M::Error>> {
289        log::debug!("Attempting to get connection from pool");
290
291        // Use semaphore to limit concurrent connections
292        let permit = self.semaphore.clone().acquire_owned().await.map_err(|_| PoolError::PoolClosed)?;
293
294        // Try to get an existing connection from the pool. Pop under the lock
295        // but validate OUTSIDE it, so a slow/dead connection cannot block other
296        // acquires (the original held the mutex for up to the validation
297        // timeout PER connection).
298        loop {
299            let candidate = {
300                let mut connections = self.connections.lock().await;
301                connections.pop_front()
302            };
303            let Some(mut pooled_conn) = candidate else {
304                // No available connection, break the loop
305                break;
306            };
307            log::trace!("Found existing connection in pool, validating...");
308            let age = Instant::now().duration_since(pooled_conn.created_at);
309            let is_valid = if age < self.max_idle_time {
310                let r = self.manager.is_valid(&mut pooled_conn.connection).await;
311                if !r {
312                    log::warn!("Connection validation failed, discarding invalid connection");
313                }
314                r
315            } else {
316                log::debug!("Connection expired (age: {age:?}), discarding");
317                false
318            };
319            if is_valid {
320                let size = self.connections.lock().await.len();
321                log::debug!("Reusing existing connection from pool (remaining: {size}/{})", self.max_size);
322                return Ok(ManagedConnection::new(pooled_conn.connection, self.clone(), permit));
323            }
324        }
325
326        log::trace!("No valid connection available, creating new connection...");
327        // Create new connection
328        match timeout(self.connection_timeout, self.manager.create_connection()).await {
329            Ok(Ok(connection)) => {
330                log::info!("Successfully created new connection");
331                Ok(ManagedConnection::new(connection, self.clone(), permit))
332            }
333            Ok(Err(e)) => {
334                log::error!("Failed to create new connection");
335                Err(PoolError::Creation(e))
336            }
337            Err(_) => {
338                log::warn!("Connection creation timed out after {:?}", self.connection_timeout);
339                Err(PoolError::Timeout)
340            }
341        }
342    }
343
344    pub fn outstanding_count(&self) -> usize {
345        self.outstanding_count.load(std::sync::atomic::Ordering::SeqCst)
346    }
347
348    pub async fn pool_size(&self) -> usize {
349        self.connections.lock().await.len()
350    }
351
352    pub fn max_size(&self) -> usize {
353        self.max_size
354    }
355
356    /// Stop the background cleanup task
357    pub async fn stop_cleanup_task(&self) {
358        let mut controller = self.cleanup_controller.lock().await;
359        controller.stop().await;
360    }
361
362    /// Restart the background cleanup task with new configuration
363    pub async fn restart_cleanup_task(&self, cleanup_config: CleanupConfig) {
364        self.stop_cleanup_task().await;
365
366        if cleanup_config.enabled {
367            let manager = Arc::new(self.manager.clone());
368            let mut controller = self.cleanup_controller.lock().await;
369            let m = self.max_size;
370            controller.start(self.connections.clone(), self.max_idle_time, cleanup_config.interval, manager, m);
371        }
372    }
373}
374
375impl<M: ConnectionManager> ConnectionPool<M> {
376    async fn recycle(&self, mut connection: M::Connection) {
377        // Only keep a small idle reserve. If it is already full, drop the
378        // connection immediately WITHOUT a 3s validation round-trip (the old
379        // code validated before the size check, so every returned connection
380        // paid the health-check latency even when it was going to be dropped).
381        let idle_cap = self.max_size.min(MAX_IDLE_KEEP);
382        {
383            let connections = self.connections.lock().await;
384            if connections.len() >= idle_cap {
385                log::debug!("Idle pool full ({idle_cap}), dropping connection");
386                return;
387            }
388        }
389        if !self.manager.is_valid(&mut connection).await {
390            log::debug!("Invalid connection, dropping");
391            return;
392        }
393        let mut connections = self.connections.lock().await;
394        if connections.len() < idle_cap {
395            connections.push_back(InnerConnection {
396                connection,
397                created_at: Instant::now(),
398            });
399            log::debug!("Connection recycled to pool (pool size: {}/{})", connections.len(), idle_cap);
400        } else {
401            log::debug!("Idle pool full ({idle_cap}), dropping connection");
402        }
403    }
404}
405
406impl<M: ConnectionManager> Drop for ManagedConnection<M> {
407    fn drop(&mut self) {
408        self.pool.outstanding_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
409        if let Some(connection) = self.connection.take() {
410            let pool = self.pool.clone();
411            _ = tokio::spawn(async move {
412                log::trace!("Recycling connection to pool on drop");
413                pool.recycle(connection).await;
414            });
415        }
416    }
417}
418
419// Generic implementations for AsRef and AsMut
420impl<M: ConnectionManager> AsRef<M::Connection> for ManagedConnection<M> {
421    fn as_ref(&self) -> &M::Connection {
422        self.connection.as_ref().unwrap()
423    }
424}
425
426impl<M: ConnectionManager> AsMut<M::Connection> for ManagedConnection<M> {
427    fn as_mut(&mut self) -> &mut M::Connection {
428        self.connection.as_mut().unwrap()
429    }
430}
431
432// Implement Deref and DerefMut for PooledStream
433impl<M: ConnectionManager> std::ops::Deref for ManagedConnection<M> {
434    type Target = M::Connection;
435
436    fn deref(&self) -> &Self::Target {
437        self.connection.as_ref().unwrap()
438    }
439}
440
441impl<M: ConnectionManager> std::ops::DerefMut for ManagedConnection<M> {
442    fn deref_mut(&mut self) -> &mut Self::Target {
443        self.connection.as_mut().unwrap()
444    }
445}
446
447/// Connection pool errors
448#[derive(Debug)]
449pub enum PoolError<E> {
450    PoolClosed,
451    Timeout,
452    Creation(E),
453}
454
455impl<E: std::fmt::Display> std::fmt::Display for PoolError<E> {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        match self {
458            PoolError::PoolClosed => write!(f, "Connection pool is closed"),
459            PoolError::Timeout => write!(f, "Connection creation timeout"),
460            PoolError::Creation(e) => write!(f, "Connection creation failed: {e}"),
461        }
462    }
463}
464
465impl<E: std::error::Error + 'static> std::error::Error for PoolError<E> {
466    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
467        match self {
468            PoolError::Creation(e) => Some(e),
469            _ => None,
470        }
471    }
472}