Skip to main content

network_protocol/service/
pool.rs

1//! # Connection Pooling
2//!
3//! Generic connection pool for all transport types.
4//!
5//! This module provides a thread-safe, async-aware connection pooling mechanism
6//! that eliminates expensive TLS handshakes for repeated connections to the same
7//! endpoint. Essential for database and RPC scenarios where clients make many
8//! short-lived requests.
9//!
10//! ## Features
11//! - Generic over any transport type `T`
12//! - Configurable pool size (min/max connections)
13//! - TTL-based connection expiration
14//! - Health checks with automatic eviction
15//! - FIFO acquisition with LRU eviction on overflow
16//! - Thread-safe async operations
17
18use std::collections::VecDeque;
19use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22use tokio::sync::{Mutex, Semaphore};
23use tracing::{debug, error, warn};
24
25use crate::error::{ProtocolError, Result};
26
27/// Configuration for connection pooling
28#[derive(Debug, Clone)]
29pub struct PoolConfig {
30    /// Minimum connections to maintain (pre-warmed)
31    pub min_size: usize,
32    /// Maximum idle connections kept in the pool. A connection released while the pool
33    /// already holds this many is closed instead of being kept.
34    pub max_size: usize,
35    /// Time-to-live for idle connections before eviction
36    pub idle_timeout: Duration,
37    /// Maximum lifetime of a connection regardless of idle time, measured from when the
38    /// connection was created (reuse does not reset it)
39    pub max_lifetime: Duration,
40    /// Maximum concurrent waiters for connections (backpressure limit)
41    pub max_waiters: usize,
42    /// Circuit breaker: consecutive failures before opening
43    pub circuit_breaker_threshold: usize,
44    /// Circuit breaker: time to wait before trying again
45    pub circuit_breaker_timeout: Duration,
46}
47
48impl Default for PoolConfig {
49    fn default() -> Self {
50        Self {
51            min_size: 5,
52            max_size: 50,
53            idle_timeout: Duration::from_secs(300), // 5 minutes
54            max_lifetime: Duration::from_secs(3600), // 1 hour
55            max_waiters: 1000,                      // Prevent OOM
56            circuit_breaker_threshold: 5,
57            circuit_breaker_timeout: Duration::from_secs(10),
58        }
59    }
60}
61
62impl PoolConfig {
63    /// Validate configuration parameters
64    pub fn validate(&self) -> Result<()> {
65        let mut errors = Vec::new();
66
67        // Validate pool sizes
68        if self.max_size == 0 {
69            errors.push("Pool max_size must be greater than 0".to_string());
70        }
71
72        if self.min_size > self.max_size {
73            errors.push(format!(
74                "Pool min_size ({}) cannot exceed max_size ({})",
75                self.min_size, self.max_size
76            ));
77        }
78
79        // Validate reasonable limits
80        if self.max_size > 10_000 {
81            errors.push(format!(
82                "Pool max_size ({}) exceeds recommended limit (10,000)",
83                self.max_size
84            ));
85        }
86
87        if self.max_waiters == 0 {
88            errors.push("Pool max_waiters must be greater than 0".to_string());
89        }
90
91        if self.max_waiters > 1_000_000 {
92            errors.push(format!(
93                "Pool max_waiters ({}) exceeds recommended limit (1,000,000)",
94                self.max_waiters
95            ));
96        }
97
98        // Validate timeouts
99        if self.idle_timeout.is_zero() {
100            errors.push("Pool idle_timeout must be greater than 0".to_string());
101        }
102
103        if self.max_lifetime.is_zero() {
104            errors.push("Pool max_lifetime must be greater than 0".to_string());
105        }
106
107        if self.idle_timeout >= self.max_lifetime {
108            errors.push(format!(
109                "Pool idle_timeout ({:?}) should be less than max_lifetime ({:?})",
110                self.idle_timeout, self.max_lifetime
111            ));
112        }
113
114        if self.idle_timeout.as_secs() > 3600 {
115            errors.push(format!(
116                "Pool idle_timeout ({} seconds) is unusually long (recommended: < 1 hour)",
117                self.idle_timeout.as_secs()
118            ));
119        }
120
121        if self.max_lifetime.as_secs() > 86400 {
122            errors.push(format!(
123                "Pool max_lifetime ({} seconds) is unusually long (recommended: < 24 hours)",
124                self.max_lifetime.as_secs()
125            ));
126        }
127
128        // Validate circuit breaker settings
129        if self.circuit_breaker_threshold == 0 {
130            errors.push("Circuit breaker threshold must be greater than 0".to_string());
131        }
132
133        if self.circuit_breaker_threshold > 100 {
134            errors.push(format!(
135                "Circuit breaker threshold ({}) is unusually high (recommended: < 100)",
136                self.circuit_breaker_threshold
137            ));
138        }
139
140        if self.circuit_breaker_timeout.is_zero() {
141            errors.push("Circuit breaker timeout must be greater than 0".to_string());
142        }
143
144        if self.circuit_breaker_timeout.as_secs() > 300 {
145            errors.push(format!(
146                "Circuit breaker timeout ({} seconds) is unusually long (recommended: < 5 minutes)",
147                self.circuit_breaker_timeout.as_secs()
148            ));
149        }
150
151        // Return aggregated errors
152        if errors.is_empty() {
153            Ok(())
154        } else {
155            Err(ProtocolError::ConfigError(format!(
156                "Pool configuration validation failed:\n  - {}",
157                errors.join("\n  - ")
158            )))
159        }
160    }
161}
162
163/// Pooled connection wrapper with metadata
164struct PooledConnection<T> {
165    connection: T,
166    created_at: Instant,
167    last_used_at: Instant,
168}
169
170impl<T> PooledConnection<T> {
171    fn new(connection: T) -> Self {
172        Self::returned(connection, Instant::now())
173    }
174
175    /// A connection coming back to the pool: it keeps its original creation time, so
176    /// `max_lifetime` counts from when it was opened, not from its last release.
177    fn returned(connection: T, created_at: Instant) -> Self {
178        Self {
179            connection,
180            created_at,
181            last_used_at: Instant::now(),
182        }
183    }
184
185    fn is_expired(&self, config: &PoolConfig) -> bool {
186        let now = Instant::now();
187        // Check max lifetime exceeded
188        if now.duration_since(self.created_at) > config.max_lifetime {
189            return true;
190        }
191        // Check idle timeout exceeded
192        if now.duration_since(self.last_used_at) > config.idle_timeout {
193            return true;
194        }
195        false
196    }
197
198    fn touch(&mut self) {
199        self.last_used_at = Instant::now();
200    }
201}
202
203/// Factory trait for creating new connections
204pub trait ConnectionFactory<T>: Send + Sync {
205    /// Create a new connection asynchronously
206    fn create(&self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T>> + Send>>;
207
208    /// Check if a connection is still healthy
209    fn is_healthy(&self, _conn: &T) -> bool {
210        true
211    }
212}
213
214/// Pool metrics for monitoring and capacity planning
215#[derive(Debug, Default)]
216pub struct PoolMetrics {
217    /// Total connections created
218    pub connections_created: AtomicU64,
219    /// Total connections reused from pool
220    pub connections_reused: AtomicU64,
221    /// Total connections evicted (expired/unhealthy)
222    pub connections_evicted: AtomicU64,
223    /// Total acquisition errors
224    pub acquisition_errors: AtomicU64,
225    /// Current active (checked out) connections
226    pub active_connections: AtomicUsize,
227    /// Current idle (in pool) connections
228    pub idle_connections: AtomicUsize,
229    /// Total wait time in microseconds (for avg calculation)
230    pub total_wait_time_us: AtomicU64,
231    /// Total successful acquisitions
232    pub total_acquisitions: AtomicU64,
233}
234
235impl PoolMetrics {
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    pub fn average_wait_time_us(&self) -> u64 {
241        let total = self.total_acquisitions.load(Ordering::Relaxed);
242        if total == 0 {
243            return 0;
244        }
245        self.total_wait_time_us.load(Ordering::Relaxed) / total
246    }
247
248    pub fn utilization_percent(&self) -> f64 {
249        let active = self.active_connections.load(Ordering::Relaxed) as f64;
250        let idle = self.idle_connections.load(Ordering::Relaxed) as f64;
251        let total = active + idle;
252        if total == 0.0 {
253            return 0.0;
254        }
255        (active / total) * 100.0
256    }
257}
258
259/// Circuit breaker for fail-fast behavior
260#[derive(Debug)]
261struct CircuitBreaker {
262    consecutive_failures: AtomicUsize,
263    threshold: usize,
264    timeout: Duration,
265    opened_at: Mutex<Option<Instant>>,
266}
267
268impl CircuitBreaker {
269    fn new(threshold: usize, timeout: Duration) -> Self {
270        Self {
271            consecutive_failures: AtomicUsize::new(0),
272            threshold,
273            timeout,
274            opened_at: Mutex::new(None),
275        }
276    }
277
278    async fn check(&self) -> Result<()> {
279        let mut opened_at = self.opened_at.lock().await;
280        if let Some(opened_time) = *opened_at {
281            // Circuit is open, check if timeout elapsed
282            if opened_time.elapsed() < self.timeout {
283                return Err(ProtocolError::CircuitBreakerOpen);
284            }
285            // Timeout elapsed, enter half-open state
286            *opened_at = None;
287            self.consecutive_failures.store(0, Ordering::SeqCst);
288            debug!("Circuit breaker entering half-open state");
289        }
290        Ok(())
291    }
292
293    async fn record_success(&self) {
294        self.consecutive_failures.store(0, Ordering::SeqCst);
295        let mut opened_at = self.opened_at.lock().await;
296        if opened_at.is_some() {
297            *opened_at = None;
298            debug!("Circuit breaker closed after successful operation");
299        }
300    }
301
302    async fn record_failure(&self) {
303        let failures = self.consecutive_failures.fetch_add(1, Ordering::SeqCst) + 1;
304        if failures >= self.threshold {
305            let mut opened_at = self.opened_at.lock().await;
306            *opened_at = Some(Instant::now());
307            error!(
308                "Circuit breaker opened after {} consecutive failures",
309                failures
310            );
311        }
312    }
313}
314
315/// Generic connection pool for any transport type
316pub struct ConnectionPool<T> {
317    config: PoolConfig,
318    factory: Arc<dyn ConnectionFactory<T>>,
319    connections: Arc<Mutex<VecDeque<PooledConnection<T>>>>,
320    metrics: Arc<PoolMetrics>,
321    circuit_breaker: Arc<CircuitBreaker>,
322    backpressure: Arc<Semaphore>,
323}
324
325impl<T: Send + 'static> ConnectionPool<T> {
326    /// Create a new connection pool
327    pub fn new(factory: Arc<dyn ConnectionFactory<T>>, config: PoolConfig) -> Result<Self> {
328        config.validate()?;
329
330        let metrics = Arc::new(PoolMetrics::new());
331        let circuit_breaker = Arc::new(CircuitBreaker::new(
332            config.circuit_breaker_threshold,
333            config.circuit_breaker_timeout,
334        ));
335
336        let pool = Self {
337            config: config.clone(),
338            factory: factory.clone(),
339            connections: Arc::new(Mutex::new(VecDeque::new())),
340            metrics: metrics.clone(),
341            circuit_breaker,
342            backpressure: Arc::new(Semaphore::new(config.max_waiters)),
343        };
344
345        // Spawn connection warming task
346        if config.min_size > 0 {
347            let factory_clone = factory;
348            let connections_clone = pool.connections.clone();
349            let metrics_clone = metrics;
350            let min_size = config.min_size;
351
352            tokio::spawn(async move {
353                debug!("Warming connection pool with {} connections", min_size);
354                for _ in 0..min_size {
355                    match factory_clone.create().await {
356                        Ok(conn) => {
357                            let mut connections = connections_clone.lock().await;
358                            connections.push_back(PooledConnection::new(conn));
359                            metrics_clone
360                                .connections_created
361                                .fetch_add(1, Ordering::Relaxed);
362                            metrics_clone
363                                .idle_connections
364                                .fetch_add(1, Ordering::Relaxed);
365                        }
366                        Err(e) => {
367                            warn!("Failed to warm connection: {}", e);
368                            break;
369                        }
370                    }
371                }
372                debug!("Connection pool warming complete");
373            });
374        }
375
376        Ok(pool)
377    }
378
379    /// Get a connection from the pool or create a new one
380    pub async fn acquire(&self) -> Result<PooledConnectionGuard<T>> {
381        let start = Instant::now();
382
383        // Enforce backpressure limit
384        let _permit = self
385            .backpressure
386            .acquire()
387            .await
388            .map_err(|_| ProtocolError::PoolExhausted)?;
389
390        // Check circuit breaker
391        self.circuit_breaker.check().await?;
392
393        let mut connections = self.connections.lock().await;
394
395        // Try to find a valid connection in the pool (LRU: take from back)
396        while let Some(mut pooled) = connections.pop_back() {
397            if !pooled.is_expired(&self.config) && self.factory.is_healthy(&pooled.connection) {
398                pooled.touch();
399                self.metrics
400                    .connections_reused
401                    .fetch_add(1, Ordering::Relaxed);
402                self.metrics
403                    .idle_connections
404                    .fetch_sub(1, Ordering::Relaxed);
405                self.metrics
406                    .active_connections
407                    .fetch_add(1, Ordering::Relaxed);
408                self.metrics
409                    .total_acquisitions
410                    .fetch_add(1, Ordering::Relaxed);
411                self.metrics
412                    .total_wait_time_us
413                    .fetch_add(start.elapsed().as_micros() as u64, Ordering::Relaxed);
414
415                debug!("Reused connection from pool (LRU)");
416                return Ok(PooledConnectionGuard {
417                    connection: Some(pooled.connection),
418                    created_at: pooled.created_at,
419                    max_idle: self.config.max_size,
420                    pool: self.connections.clone(),
421                    metrics: self.metrics.clone(),
422                });
423            }
424            debug!("Evicted expired/unhealthy connection from pool");
425            self.metrics
426                .connections_evicted
427                .fetch_add(1, Ordering::Relaxed);
428            self.metrics
429                .idle_connections
430                .fetch_sub(1, Ordering::Relaxed);
431        }
432
433        // No valid connection found, create new one
434        drop(connections); // Release lock before creating new connection
435
436        match self.factory.create().await {
437            Ok(new_conn) => {
438                self.circuit_breaker.record_success().await;
439                self.metrics
440                    .connections_created
441                    .fetch_add(1, Ordering::Relaxed);
442                self.metrics
443                    .active_connections
444                    .fetch_add(1, Ordering::Relaxed);
445                self.metrics
446                    .total_acquisitions
447                    .fetch_add(1, Ordering::Relaxed);
448                self.metrics
449                    .total_wait_time_us
450                    .fetch_add(start.elapsed().as_micros() as u64, Ordering::Relaxed);
451
452                debug!("Created new connection for pool");
453
454                Ok(PooledConnectionGuard {
455                    connection: Some(new_conn),
456                    created_at: Instant::now(),
457                    max_idle: self.config.max_size,
458                    pool: self.connections.clone(),
459                    metrics: self.metrics.clone(),
460                })
461            }
462            Err(e) => {
463                self.circuit_breaker.record_failure().await;
464                self.metrics
465                    .acquisition_errors
466                    .fetch_add(1, Ordering::Relaxed);
467                Err(e)
468            }
469        }
470    }
471
472    /// Get pool metrics
473    pub fn metrics(&self) -> Arc<PoolMetrics> {
474        self.metrics.clone()
475    }
476
477    /// Current number of connections in pool
478    pub async fn size(&self) -> usize {
479        self.connections.lock().await.len()
480    }
481
482    /// Clear all connections from the pool
483    pub async fn clear(&self) {
484        self.connections.lock().await.clear();
485        debug!("Cleared all connections from pool");
486    }
487
488    /// Get pool configuration
489    pub fn config(&self) -> &PoolConfig {
490        &self.config
491    }
492}
493
494/// RAII guard for pooled connections
495///
496/// Returns the connection to the pool on drop.
497pub struct PooledConnectionGuard<T: Send + 'static> {
498    connection: Option<T>,
499    /// When the connection was opened, carried back into the pool on release
500    created_at: Instant,
501    /// `PoolConfig::max_size`: idle connections the pool may hold
502    max_idle: usize,
503    pool: Arc<Mutex<VecDeque<PooledConnection<T>>>>,
504    metrics: Arc<PoolMetrics>,
505}
506
507impl<T: Send + 'static> PooledConnectionGuard<T> {
508    /// Get a reference to the underlying connection
509    pub fn get(&self) -> Option<&T> {
510        self.connection.as_ref()
511    }
512
513    /// Get a mutable reference to the underlying connection
514    pub fn get_mut(&mut self) -> Option<&mut T> {
515        self.connection.as_mut()
516    }
517
518    /// Extract the connection (won't be returned to pool)
519    pub fn into_inner(mut self) -> Option<T> {
520        self.connection.take()
521    }
522}
523
524impl<T: Send + 'static> AsRef<T> for PooledConnectionGuard<T> {
525    #[allow(clippy::expect_used)] // Connection is guaranteed to exist unless into_inner() was called
526    fn as_ref(&self) -> &T {
527        self.connection.as_ref().expect("Connection should exist")
528    }
529}
530
531impl<T: Send + 'static> AsMut<T> for PooledConnectionGuard<T> {
532    #[allow(clippy::expect_used)] // Connection is guaranteed to exist unless into_inner() was called
533    fn as_mut(&mut self) -> &mut T {
534        self.connection.as_mut().expect("Connection should exist")
535    }
536}
537
538impl<T: Send + 'static> Drop for PooledConnectionGuard<T> {
539    fn drop(&mut self) {
540        if let Some(conn) = self.connection.take() {
541            let pool = self.pool.clone();
542            let metrics = self.metrics.clone();
543            let max_idle = self.max_idle;
544            let pooled = PooledConnection::returned(conn, self.created_at);
545
546            // Update metrics
547            metrics.active_connections.fetch_sub(1, Ordering::Relaxed);
548
549            // Try to return connection to pool (async context may not be available)
550            // This spawns a background task to handle the return
551            tokio::spawn(async move {
552                let mut connections = pool.lock().await;
553                if connections.len() < max_idle {
554                    connections.push_back(pooled);
555                    metrics.idle_connections.fetch_add(1, Ordering::Relaxed);
556                } else {
557                    warn!("Connection pool at capacity, discarding connection");
558                }
559            });
560        }
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use std::sync::atomic::{AtomicUsize, Ordering};
568
569    #[allow(dead_code)]
570    struct TestConnection {
571        id: usize,
572    }
573
574    struct TestFactory {
575        counter: Arc<AtomicUsize>,
576    }
577
578    impl TestFactory {
579        fn new() -> Self {
580            Self {
581                counter: Arc::new(AtomicUsize::new(0)),
582            }
583        }
584
585        fn count(&self) -> usize {
586            self.counter.load(Ordering::SeqCst)
587        }
588    }
589
590    impl ConnectionFactory<TestConnection> for TestFactory {
591        fn create(
592            &self,
593        ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<TestConnection>> + Send>>
594        {
595            let id = self.counter.fetch_add(1, Ordering::SeqCst);
596            Box::pin(async move { Ok(TestConnection { id }) })
597        }
598    }
599
600    #[tokio::test]
601    async fn test_pool_creation() {
602        let factory = Arc::new(TestFactory::new());
603        let pool = ConnectionPool::new(
604            factory.clone(),
605            PoolConfig {
606                min_size: 2,
607                max_size: 10,
608                idle_timeout: Duration::from_secs(60),
609                max_lifetime: Duration::from_secs(600),
610                ..Default::default()
611            },
612        );
613
614        assert!(pool.is_ok());
615    }
616
617    #[tokio::test]
618    #[allow(clippy::unwrap_used)] // Test code
619    async fn test_pool_acquire_creates_connection() {
620        let factory = Arc::new(TestFactory::new());
621        let pool = ConnectionPool::new(factory.clone(), PoolConfig::default()).unwrap();
622
623        let guard = pool.acquire().await.unwrap();
624        assert!(guard.get().is_some());
625        assert_eq!(factory.count(), 1);
626    }
627
628    #[tokio::test]
629    async fn test_config_validation() {
630        let invalid_config = PoolConfig {
631            min_size: 100,
632            max_size: 10,
633            idle_timeout: Duration::from_secs(60),
634            max_lifetime: Duration::from_secs(600),
635            ..Default::default()
636        };
637
638        let factory = Arc::new(TestFactory::new());
639        let result = ConnectionPool::new(factory, invalid_config);
640        assert!(result.is_err());
641    }
642
643    #[tokio::test]
644    async fn test_config_validation_zero_max_size() {
645        let config = PoolConfig {
646            max_size: 0,
647            ..Default::default()
648        };
649        assert!(config.validate().is_err());
650    }
651
652    #[tokio::test]
653    async fn test_config_validation_zero_timeouts() {
654        let config = PoolConfig {
655            idle_timeout: Duration::from_secs(0),
656            ..Default::default()
657        };
658        assert!(config.validate().is_err());
659
660        let config2 = PoolConfig {
661            max_lifetime: Duration::from_secs(0),
662            ..Default::default()
663        };
664        assert!(config2.validate().is_err());
665    }
666
667    #[tokio::test]
668    async fn test_config_validation_idle_exceeds_lifetime() {
669        let config = PoolConfig {
670            idle_timeout: Duration::from_secs(600),
671            max_lifetime: Duration::from_secs(300),
672            ..Default::default()
673        };
674        assert!(config.validate().is_err());
675    }
676
677    #[tokio::test]
678    async fn test_config_validation_circuit_breaker() {
679        let config = PoolConfig {
680            circuit_breaker_threshold: 0,
681            ..Default::default()
682        };
683        assert!(config.validate().is_err());
684
685        let config2 = PoolConfig {
686            circuit_breaker_timeout: Duration::from_secs(0),
687            ..Default::default()
688        };
689        assert!(config2.validate().is_err());
690    }
691
692    #[tokio::test]
693    async fn test_config_validation_valid_config() {
694        let config = PoolConfig::default();
695        assert!(config.validate().is_ok());
696    }
697
698    /// Wait for the background tasks that return dropped guards to the pool.
699    async fn settle<T: Send + 'static>(pool: &ConnectionPool<T>, expected: usize) -> usize {
700        for _ in 0..100 {
701            if pool.size().await == expected {
702                break;
703            }
704            tokio::time::sleep(Duration::from_millis(5)).await;
705        }
706        pool.size().await
707    }
708
709    #[tokio::test]
710    #[allow(clippy::unwrap_used)] // Test code
711    async fn test_released_connections_are_capped_at_max_size() {
712        let factory = Arc::new(TestFactory::new());
713        let pool = ConnectionPool::new(
714            factory.clone(),
715            PoolConfig {
716                min_size: 0,
717                max_size: 2,
718                ..Default::default()
719            },
720        )
721        .unwrap();
722
723        let mut guards = Vec::new();
724        for _ in 0..5 {
725            guards.push(pool.acquire().await.unwrap());
726        }
727        assert_eq!(factory.count(), 5);
728        drop(guards);
729
730        // Only max_size of the five are kept; the rest are closed.
731        tokio::time::sleep(Duration::from_millis(50)).await;
732        assert_eq!(settle(&pool, 2).await, 2);
733    }
734
735    #[tokio::test]
736    #[allow(clippy::unwrap_used)] // Test code
737    async fn test_max_lifetime_counts_from_creation_not_last_release() {
738        let factory = Arc::new(TestFactory::new());
739        let pool = ConnectionPool::new(
740            factory.clone(),
741            PoolConfig {
742                min_size: 0,
743                max_size: 4,
744                idle_timeout: Duration::from_millis(100),
745                max_lifetime: Duration::from_millis(250),
746                ..Default::default()
747            },
748        )
749        .unwrap();
750
751        // Keep reusing one connection, never idle longer than idle_timeout.
752        drop(pool.acquire().await.unwrap());
753        assert_eq!(settle(&pool, 1).await, 1);
754        for _ in 0..8 {
755            tokio::time::sleep(Duration::from_millis(50)).await;
756            drop(pool.acquire().await.unwrap());
757            assert_eq!(settle(&pool, 1).await, 1);
758        }
759
760        // 400 ms of use is past the 250 ms lifetime, so the connection was replaced
761        // even though it was reused well within the idle timeout.
762        assert!(
763            factory.count() >= 2,
764            "connection outlived max_lifetime: created {}",
765            factory.count()
766        );
767    }
768}