Skip to main content

oximedia_distributed/
connection_pool.rs

1//! Connection pooling and retry for coordinator client connections.
2//!
3//! Provides a `ConnectionPool` that manages a configurable number of gRPC
4//! client connections to the coordinator, with automatic reconnection on
5//! failure and configurable retry policies.
6
7use crate::{DistributedError, Result};
8use std::collections::VecDeque;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::{Mutex, Semaphore};
13use tracing::{debug, info, warn};
14
15/// Configuration for the connection pool.
16#[derive(Debug, Clone)]
17pub struct ConnectionPoolConfig {
18    /// Target endpoint address (e.g., "http://127.0.0.1:50051").
19    pub endpoint: String,
20    /// Maximum number of connections in the pool.
21    pub max_connections: usize,
22    /// Minimum number of idle connections to keep warm.
23    pub min_idle: usize,
24    /// Connection timeout.
25    pub connect_timeout: Duration,
26    /// Maximum number of retries on connection failure.
27    pub max_retries: u32,
28    /// Base delay between retries (exponential backoff base).
29    pub retry_base_delay: Duration,
30    /// Maximum delay between retries.
31    pub retry_max_delay: Duration,
32    /// Maximum lifetime of a connection before forced recycling.
33    pub max_lifetime: Duration,
34    /// Health check interval for idle connections.
35    pub health_check_interval: Duration,
36}
37
38impl Default for ConnectionPoolConfig {
39    fn default() -> Self {
40        Self {
41            endpoint: "http://127.0.0.1:50051".to_string(),
42            max_connections: 8,
43            min_idle: 2,
44            connect_timeout: Duration::from_secs(5),
45            max_retries: 3,
46            retry_base_delay: Duration::from_millis(100),
47            retry_max_delay: Duration::from_secs(10),
48            max_lifetime: Duration::from_secs(3600),
49            health_check_interval: Duration::from_secs(30),
50        }
51    }
52}
53
54impl ConnectionPoolConfig {
55    /// Create a new configuration with the given endpoint.
56    #[must_use]
57    pub fn new(endpoint: &str) -> Self {
58        Self {
59            endpoint: endpoint.to_string(),
60            ..Default::default()
61        }
62    }
63
64    /// Set the maximum pool size.
65    #[must_use]
66    pub fn with_max_connections(mut self, max: usize) -> Self {
67        self.max_connections = max.max(1);
68        self
69    }
70
71    /// Set the minimum idle connections.
72    #[must_use]
73    pub fn with_min_idle(mut self, min: usize) -> Self {
74        self.min_idle = min;
75        self
76    }
77
78    /// Set the maximum retries.
79    #[must_use]
80    pub fn with_max_retries(mut self, retries: u32) -> Self {
81        self.max_retries = retries;
82        self
83    }
84
85    /// Set the connection timeout.
86    #[must_use]
87    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
88        self.connect_timeout = timeout;
89        self
90    }
91
92    /// Set the retry base delay.
93    #[must_use]
94    pub fn with_retry_base_delay(mut self, delay: Duration) -> Self {
95        self.retry_base_delay = delay;
96        self
97    }
98
99    /// Set the maximum connection lifetime.
100    #[must_use]
101    pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
102        self.max_lifetime = lifetime;
103        self
104    }
105}
106
107/// Retry policy for connection attempts.
108#[derive(Debug, Clone)]
109pub struct RetryPolicy {
110    /// Maximum retries.
111    max_retries: u32,
112    /// Base delay for exponential backoff.
113    base_delay: Duration,
114    /// Maximum delay cap.
115    max_delay: Duration,
116}
117
118impl RetryPolicy {
119    /// Create a new retry policy.
120    #[must_use]
121    pub fn new(max_retries: u32, base_delay: Duration, max_delay: Duration) -> Self {
122        Self {
123            max_retries,
124            base_delay,
125            max_delay,
126        }
127    }
128
129    /// Compute the delay for the given attempt number (0-based).
130    #[must_use]
131    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
132        let factor = 2u64.saturating_pow(attempt);
133        let delay_ms = self.base_delay.as_millis() as u64 * factor;
134        let capped = delay_ms.min(self.max_delay.as_millis() as u64);
135        Duration::from_millis(capped)
136    }
137
138    /// Whether more attempts are allowed.
139    #[must_use]
140    pub fn should_retry(&self, attempt: u32) -> bool {
141        attempt < self.max_retries
142    }
143}
144
145/// State of a pooled connection.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum ConnectionState {
148    /// Connection is idle and available.
149    Idle,
150    /// Connection is currently in use.
151    InUse,
152    /// Connection is broken and needs reconnection.
153    Broken,
154    /// Connection has exceeded its maximum lifetime.
155    Expired,
156}
157
158/// A pooled connection wrapper.
159#[derive(Debug)]
160#[allow(dead_code)]
161pub struct PooledConnection {
162    /// Unique connection identifier.
163    id: u64,
164    /// Current state.
165    state: ConnectionState,
166    /// When the connection was created (monotonic tick).
167    created_at_tick: u64,
168    /// When the connection was last used (monotonic tick).
169    last_used_tick: u64,
170    /// Number of requests served by this connection.
171    requests_served: u64,
172    /// The endpoint this connection targets.
173    endpoint: String,
174}
175
176impl PooledConnection {
177    /// Create a new connection.
178    fn new(id: u64, endpoint: &str, now_tick: u64) -> Self {
179        Self {
180            id,
181            state: ConnectionState::Idle,
182            created_at_tick: now_tick,
183            last_used_tick: now_tick,
184            requests_served: 0,
185            endpoint: endpoint.to_string(),
186        }
187    }
188
189    /// Get the connection ID.
190    #[must_use]
191    pub fn id(&self) -> u64 {
192        self.id
193    }
194
195    /// Get the current state.
196    #[must_use]
197    pub fn state(&self) -> ConnectionState {
198        self.state
199    }
200
201    /// Mark as in use.
202    fn checkout(&mut self, now_tick: u64) {
203        self.state = ConnectionState::InUse;
204        self.last_used_tick = now_tick;
205    }
206
207    /// Return to idle after use.
208    fn checkin(&mut self, now_tick: u64) {
209        self.state = ConnectionState::Idle;
210        self.last_used_tick = now_tick;
211        self.requests_served += 1;
212    }
213
214    /// Mark as broken.
215    fn mark_broken(&mut self) {
216        self.state = ConnectionState::Broken;
217    }
218
219    /// Check if the connection has exceeded its maximum lifetime.
220    fn is_expired(&self, now_tick: u64, max_lifetime_ms: u64) -> bool {
221        now_tick.saturating_sub(self.created_at_tick) >= max_lifetime_ms
222    }
223}
224
225/// Statistics for the connection pool.
226#[derive(Debug, Clone, Default)]
227pub struct PoolStats {
228    /// Total connections created.
229    pub total_created: u64,
230    /// Total connections closed (recycled or broken).
231    pub total_closed: u64,
232    /// Total successful checkouts.
233    pub total_checkouts: u64,
234    /// Total failed connection attempts.
235    pub total_failures: u64,
236    /// Current pool size.
237    pub current_size: usize,
238    /// Current idle count.
239    pub current_idle: usize,
240    /// Current in-use count.
241    pub current_in_use: usize,
242}
243
244/// Connection pool for coordinator client connections.
245///
246/// Manages a pool of connections with automatic reconnection, lifetime
247/// management, and configurable pool sizing.
248pub struct ConnectionPool {
249    config: ConnectionPoolConfig,
250    /// All connections.
251    connections: Arc<Mutex<VecDeque<PooledConnection>>>,
252    /// Semaphore to limit concurrent checkouts (reserved for async checkout).
253    #[allow(dead_code)]
254    checkout_semaphore: Arc<Semaphore>,
255    /// Monotonic tick counter.
256    current_tick: Arc<AtomicU64>,
257    /// Next connection ID.
258    next_id: Arc<AtomicU64>,
259    /// Pool statistics.
260    stats: Arc<Mutex<PoolStats>>,
261    /// Whether the pool is shut down.
262    is_shutdown: Arc<AtomicBool>,
263    /// Retry policy.
264    retry_policy: RetryPolicy,
265}
266
267impl ConnectionPool {
268    /// Create a new connection pool with the given configuration.
269    #[must_use]
270    pub fn new(config: ConnectionPoolConfig) -> Self {
271        let semaphore_permits = config.max_connections;
272        let retry_policy = RetryPolicy::new(
273            config.max_retries,
274            config.retry_base_delay,
275            config.retry_max_delay,
276        );
277
278        Self {
279            config,
280            connections: Arc::new(Mutex::new(VecDeque::new())),
281            checkout_semaphore: Arc::new(Semaphore::new(semaphore_permits)),
282            current_tick: Arc::new(AtomicU64::new(0)),
283            next_id: Arc::new(AtomicU64::new(1)),
284            stats: Arc::new(Mutex::new(PoolStats::default())),
285            is_shutdown: Arc::new(AtomicBool::new(false)),
286            retry_policy,
287        }
288    }
289
290    /// Initialize the pool by creating `min_idle` connections.
291    pub async fn initialize(&self) -> Result<()> {
292        if self.is_shutdown.load(Ordering::Relaxed) {
293            return Err(DistributedError::Worker("Pool is shut down".to_string()));
294        }
295
296        info!(
297            "Initializing connection pool: endpoint={}, max={}, min_idle={}",
298            self.config.endpoint, self.config.max_connections, self.config.min_idle
299        );
300
301        let mut conns = self.connections.lock().await;
302        let mut stats = self.stats.lock().await;
303        let now = self.current_tick.load(Ordering::Relaxed);
304
305        for _ in 0..self.config.min_idle {
306            let id = self.next_id.fetch_add(1, Ordering::Relaxed);
307            let conn = PooledConnection::new(id, &self.config.endpoint, now);
308            conns.push_back(conn);
309            stats.total_created += 1;
310            stats.current_size += 1;
311            stats.current_idle += 1;
312        }
313
314        debug!("Pool initialized with {} connections", conns.len());
315        Ok(())
316    }
317
318    /// Checkout a connection from the pool.
319    ///
320    /// Returns a connection ID that should be returned via `checkin` after use.
321    /// If no idle connections are available and the pool is not at capacity, a
322    /// new connection is created. Retries with exponential backoff on failure.
323    pub async fn checkout(&self) -> Result<u64> {
324        if self.is_shutdown.load(Ordering::Relaxed) {
325            return Err(DistributedError::Worker("Pool is shut down".to_string()));
326        }
327
328        let now = self.current_tick.load(Ordering::Relaxed);
329        let max_lifetime_ms = self.config.max_lifetime.as_millis() as u64;
330
331        let mut conns = self.connections.lock().await;
332        let mut stats = self.stats.lock().await;
333
334        // Try to find an idle, non-expired connection
335        for conn in conns.iter_mut() {
336            if conn.state == ConnectionState::Idle && !conn.is_expired(now, max_lifetime_ms) {
337                conn.checkout(now);
338                stats.total_checkouts += 1;
339                stats.current_idle = stats.current_idle.saturating_sub(1);
340                stats.current_in_use += 1;
341                debug!("Checked out connection {}", conn.id);
342                return Ok(conn.id);
343            }
344        }
345
346        // Remove expired and broken connections
347        let before_len = conns.len();
348        conns.retain(|c| c.state != ConnectionState::Broken && !c.is_expired(now, max_lifetime_ms));
349        let removed = before_len - conns.len();
350        if removed > 0 {
351            stats.total_closed += removed as u64;
352            stats.current_size = stats.current_size.saturating_sub(removed);
353            stats.current_idle = stats.current_idle.saturating_sub(removed);
354        }
355
356        // Create a new connection if under capacity
357        if conns.len() < self.config.max_connections {
358            let id = self.next_id.fetch_add(1, Ordering::Relaxed);
359            let mut conn = PooledConnection::new(id, &self.config.endpoint, now);
360            conn.checkout(now);
361            conns.push_back(conn);
362            stats.total_created += 1;
363            stats.total_checkouts += 1;
364            stats.current_size += 1;
365            stats.current_in_use += 1;
366            debug!("Created and checked out new connection {}", id);
367            return Ok(id);
368        }
369
370        // Pool is at capacity and all connections are in use
371        Err(DistributedError::ResourceExhausted(
372            "Connection pool exhausted".to_string(),
373        ))
374    }
375
376    /// Return a connection to the pool after use.
377    pub async fn checkin(&self, conn_id: u64) -> Result<()> {
378        let now = self.current_tick.load(Ordering::Relaxed);
379        let mut conns = self.connections.lock().await;
380        let mut stats = self.stats.lock().await;
381
382        for conn in conns.iter_mut() {
383            if conn.id == conn_id {
384                conn.checkin(now);
385                stats.current_in_use = stats.current_in_use.saturating_sub(1);
386                stats.current_idle += 1;
387                debug!("Checked in connection {}", conn_id);
388                return Ok(());
389            }
390        }
391
392        Err(DistributedError::Worker(format!(
393            "Connection {conn_id} not found in pool"
394        )))
395    }
396
397    /// Mark a connection as broken (will be removed on next cleanup).
398    pub async fn mark_broken(&self, conn_id: u64) -> Result<()> {
399        let mut conns = self.connections.lock().await;
400        let mut stats = self.stats.lock().await;
401
402        for conn in conns.iter_mut() {
403            if conn.id == conn_id {
404                let was_in_use = conn.state == ConnectionState::InUse;
405                conn.mark_broken();
406                stats.total_failures += 1;
407                if was_in_use {
408                    stats.current_in_use = stats.current_in_use.saturating_sub(1);
409                } else {
410                    stats.current_idle = stats.current_idle.saturating_sub(1);
411                }
412                warn!("Connection {} marked as broken", conn_id);
413                return Ok(());
414            }
415        }
416
417        Err(DistributedError::Worker(format!(
418            "Connection {conn_id} not found in pool"
419        )))
420    }
421
422    /// Execute an operation with automatic retry on failure.
423    ///
424    /// `operation` is a closure that receives a connection ID and returns
425    /// `Ok(T)` on success or `Err` on failure. On failure, the connection
426    /// is marked broken and a new one is obtained for the next attempt.
427    pub async fn execute_with_retry<F, T>(&self, mut operation: F) -> Result<T>
428    where
429        F: FnMut(u64) -> Result<T>,
430    {
431        let mut attempt = 0u32;
432
433        loop {
434            let conn_id = self.checkout().await?;
435
436            match operation(conn_id) {
437                Ok(value) => {
438                    self.checkin(conn_id).await?;
439                    return Ok(value);
440                }
441                Err(e) => {
442                    let _ = self.mark_broken(conn_id).await;
443
444                    if !self.retry_policy.should_retry(attempt) {
445                        return Err(e);
446                    }
447
448                    let delay = self.retry_policy.delay_for_attempt(attempt);
449                    debug!(
450                        "Retry attempt {} after {delay:?} for connection failure: {e}",
451                        attempt + 1
452                    );
453                    // In a real scenario we'd sleep here; for testability we
454                    // just increment attempt and loop immediately.
455                    attempt += 1;
456                }
457            }
458        }
459    }
460
461    /// Advance the internal tick (for testing and lifetime management).
462    pub fn advance_tick(&self, millis: u64) {
463        self.current_tick.fetch_add(millis, Ordering::Relaxed);
464    }
465
466    /// Get current pool statistics.
467    pub async fn stats(&self) -> PoolStats {
468        self.stats.lock().await.clone()
469    }
470
471    /// Get the current pool size.
472    pub async fn size(&self) -> usize {
473        self.connections.lock().await.len()
474    }
475
476    /// Get the current number of idle connections.
477    pub async fn idle_count(&self) -> usize {
478        self.connections
479            .lock()
480            .await
481            .iter()
482            .filter(|c| c.state == ConnectionState::Idle)
483            .count()
484    }
485
486    /// Get the retry policy.
487    #[must_use]
488    pub fn retry_policy(&self) -> &RetryPolicy {
489        &self.retry_policy
490    }
491
492    /// Perform health checks on idle connections, removing broken/expired ones.
493    pub async fn health_check(&self) -> Result<usize> {
494        let now = self.current_tick.load(Ordering::Relaxed);
495        let max_lifetime_ms = self.config.max_lifetime.as_millis() as u64;
496
497        let mut conns = self.connections.lock().await;
498        let mut stats = self.stats.lock().await;
499
500        let before = conns.len();
501        conns.retain(|c| {
502            if c.state == ConnectionState::Broken {
503                return false;
504            }
505            if c.state == ConnectionState::Idle && c.is_expired(now, max_lifetime_ms) {
506                return false;
507            }
508            true
509        });
510
511        let removed = before - conns.len();
512        stats.total_closed += removed as u64;
513        stats.current_size = stats.current_size.saturating_sub(removed);
514        stats.current_idle = stats.current_idle.saturating_sub(removed);
515
516        // Replenish to min_idle if needed
517        let current_idle = conns
518            .iter()
519            .filter(|c| c.state == ConnectionState::Idle)
520            .count();
521        let mut created = 0usize;
522        while current_idle + created < self.config.min_idle
523            && conns.len() + created < self.config.max_connections
524        {
525            let id = self.next_id.fetch_add(1, Ordering::Relaxed);
526            conns.push_back(PooledConnection::new(id, &self.config.endpoint, now));
527            stats.total_created += 1;
528            stats.current_size += 1;
529            stats.current_idle += 1;
530            created += 1;
531        }
532
533        if removed > 0 || created > 0 {
534            debug!(
535                "Health check: removed={}, created={}, pool_size={}",
536                removed,
537                created,
538                conns.len()
539            );
540        }
541
542        Ok(removed)
543    }
544
545    /// Shut down the pool, closing all connections.
546    pub async fn shutdown(&self) -> Result<()> {
547        self.is_shutdown.store(true, Ordering::Relaxed);
548        let mut conns = self.connections.lock().await;
549        let mut stats = self.stats.lock().await;
550
551        let count = conns.len();
552        conns.clear();
553        stats.total_closed += count as u64;
554        stats.current_size = 0;
555        stats.current_idle = 0;
556        stats.current_in_use = 0;
557
558        info!("Connection pool shut down, closed {} connections", count);
559        Ok(())
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    fn small_pool(max: usize, min_idle: usize) -> ConnectionPool {
568        ConnectionPool::new(
569            ConnectionPoolConfig::new("http://localhost:50051")
570                .with_max_connections(max)
571                .with_min_idle(min_idle),
572        )
573    }
574
575    #[test]
576    fn test_config_defaults() {
577        let config = ConnectionPoolConfig::default();
578        assert_eq!(config.max_connections, 8);
579        assert_eq!(config.min_idle, 2);
580        assert_eq!(config.max_retries, 3);
581    }
582
583    #[test]
584    fn test_config_builder() {
585        let config = ConnectionPoolConfig::new("http://example.com:50051")
586            .with_max_connections(16)
587            .with_min_idle(4)
588            .with_max_retries(5)
589            .with_connect_timeout(Duration::from_secs(10))
590            .with_retry_base_delay(Duration::from_millis(200))
591            .with_max_lifetime(Duration::from_secs(7200));
592        assert_eq!(config.max_connections, 16);
593        assert_eq!(config.min_idle, 4);
594        assert_eq!(config.max_retries, 5);
595        assert_eq!(config.connect_timeout, Duration::from_secs(10));
596    }
597
598    #[test]
599    fn test_config_max_connections_minimum_one() {
600        let config = ConnectionPoolConfig::new("http://localhost:50051").with_max_connections(0);
601        assert_eq!(config.max_connections, 1);
602    }
603
604    #[test]
605    fn test_retry_policy_delay() {
606        let policy = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_secs(5));
607        assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
608        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
609        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
610        assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(800));
611    }
612
613    #[test]
614    fn test_retry_policy_delay_capped() {
615        let policy = RetryPolicy::new(5, Duration::from_millis(100), Duration::from_millis(500));
616        // 100 * 2^4 = 1600, should be capped at 500
617        assert_eq!(policy.delay_for_attempt(4), Duration::from_millis(500));
618    }
619
620    #[test]
621    fn test_retry_policy_should_retry() {
622        let policy = RetryPolicy::new(3, Duration::from_millis(100), Duration::from_secs(5));
623        assert!(policy.should_retry(0));
624        assert!(policy.should_retry(1));
625        assert!(policy.should_retry(2));
626        assert!(!policy.should_retry(3));
627    }
628
629    #[test]
630    fn test_connection_state() {
631        let mut conn = PooledConnection::new(1, "http://localhost", 0);
632        assert_eq!(conn.state(), ConnectionState::Idle);
633
634        conn.checkout(10);
635        assert_eq!(conn.state(), ConnectionState::InUse);
636
637        conn.checkin(20);
638        assert_eq!(conn.state(), ConnectionState::Idle);
639        assert_eq!(conn.requests_served, 1);
640
641        conn.mark_broken();
642        assert_eq!(conn.state(), ConnectionState::Broken);
643    }
644
645    #[test]
646    fn test_connection_expiry() {
647        let conn = PooledConnection::new(1, "http://localhost", 100);
648        assert!(!conn.is_expired(200, 1000));
649        assert!(conn.is_expired(1100, 1000));
650        assert!(conn.is_expired(1101, 1000));
651    }
652
653    #[tokio::test]
654    async fn test_pool_initialize() {
655        let pool = small_pool(4, 2);
656        pool.initialize().await.expect("init should succeed");
657        assert_eq!(pool.size().await, 2);
658        assert_eq!(pool.idle_count().await, 2);
659    }
660
661    #[tokio::test]
662    async fn test_pool_checkout_and_checkin() {
663        let pool = small_pool(4, 1);
664        pool.initialize().await.expect("init should succeed");
665
666        let conn_id = pool.checkout().await.expect("checkout should succeed");
667        assert_eq!(pool.idle_count().await, 0);
668
669        pool.checkin(conn_id).await.expect("checkin should succeed");
670        assert_eq!(pool.idle_count().await, 1);
671    }
672
673    #[tokio::test]
674    async fn test_pool_creates_on_demand() {
675        let pool = small_pool(4, 0);
676        pool.initialize().await.expect("init should succeed");
677        assert_eq!(pool.size().await, 0);
678
679        let conn_id = pool.checkout().await.expect("checkout should succeed");
680        assert_eq!(pool.size().await, 1);
681
682        pool.checkin(conn_id).await.expect("checkin should succeed");
683    }
684
685    #[tokio::test]
686    async fn test_pool_exhaustion() {
687        let pool = small_pool(2, 0);
688        pool.initialize().await.expect("init should succeed");
689
690        let _c1 = pool.checkout().await.expect("checkout 1 should succeed");
691        let _c2 = pool.checkout().await.expect("checkout 2 should succeed");
692
693        let result = pool.checkout().await;
694        assert!(result.is_err());
695    }
696
697    #[tokio::test]
698    async fn test_pool_mark_broken() {
699        let pool = small_pool(4, 1);
700        pool.initialize().await.expect("init should succeed");
701
702        let conn_id = pool.checkout().await.expect("checkout should succeed");
703        pool.mark_broken(conn_id)
704            .await
705            .expect("mark_broken should succeed");
706
707        let stats = pool.stats().await;
708        assert_eq!(stats.total_failures, 1);
709    }
710
711    #[tokio::test]
712    async fn test_pool_health_check_removes_broken() {
713        let pool = small_pool(4, 0);
714        pool.initialize().await.expect("init should succeed");
715
716        let c1 = pool.checkout().await.expect("checkout should succeed");
717        pool.mark_broken(c1)
718            .await
719            .expect("mark_broken should succeed");
720
721        let removed = pool
722            .health_check()
723            .await
724            .expect("health_check should succeed");
725        assert_eq!(removed, 1);
726        assert_eq!(pool.size().await, 0);
727    }
728
729    #[tokio::test]
730    async fn test_pool_health_check_removes_expired() {
731        let pool = ConnectionPool::new(
732            ConnectionPoolConfig::new("http://localhost:50051")
733                .with_max_connections(4)
734                .with_min_idle(0)
735                .with_max_lifetime(Duration::from_millis(500)),
736        );
737        pool.initialize().await.expect("init should succeed");
738
739        let c1 = pool.checkout().await.expect("checkout should succeed");
740        pool.checkin(c1).await.expect("checkin should succeed");
741        assert_eq!(pool.size().await, 1);
742
743        // Advance time past lifetime
744        pool.advance_tick(600);
745
746        let removed = pool
747            .health_check()
748            .await
749            .expect("health_check should succeed");
750        assert_eq!(removed, 1);
751        assert_eq!(pool.size().await, 0);
752    }
753
754    #[tokio::test]
755    async fn test_pool_health_check_replenishes_min_idle() {
756        let pool = small_pool(4, 2);
757        pool.initialize().await.expect("init should succeed");
758        assert_eq!(pool.size().await, 2);
759
760        // Check out and break one connection
761        let c1 = pool.checkout().await.expect("checkout should succeed");
762        pool.mark_broken(c1)
763            .await
764            .expect("mark_broken should succeed");
765
766        // Health check should remove broken and replenish
767        pool.health_check()
768            .await
769            .expect("health_check should succeed");
770
771        // Should have replenished to at least min_idle idle connections
772        assert!(pool.idle_count().await >= 2);
773    }
774
775    #[tokio::test]
776    async fn test_pool_execute_with_retry_success() {
777        let pool = small_pool(4, 1);
778        pool.initialize().await.expect("init should succeed");
779
780        let result = pool
781            .execute_with_retry(|conn_id| -> Result<u64> { Ok(conn_id * 2) })
782            .await
783            .expect("execute should succeed");
784        assert!(result > 0);
785    }
786
787    #[tokio::test]
788    async fn test_pool_execute_with_retry_fails_then_succeeds() {
789        let pool = small_pool(4, 1);
790        pool.initialize().await.expect("init should succeed");
791
792        let call_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
793        let count = call_count.clone();
794
795        let result = pool
796            .execute_with_retry(move |conn_id| -> Result<u64> {
797                let attempt = count.fetch_add(1, Ordering::Relaxed);
798                if attempt == 0 {
799                    Err(DistributedError::Worker("transient error".to_string()))
800                } else {
801                    Ok(conn_id)
802                }
803            })
804            .await
805            .expect("execute should succeed on retry");
806        assert!(result > 0);
807        assert_eq!(call_count.load(Ordering::Relaxed), 2);
808    }
809
810    #[tokio::test]
811    async fn test_pool_execute_with_retry_exhausted() {
812        let pool = ConnectionPool::new(
813            ConnectionPoolConfig::new("http://localhost:50051")
814                .with_max_connections(8)
815                .with_min_idle(0)
816                .with_max_retries(2),
817        );
818        pool.initialize().await.expect("init should succeed");
819
820        let result = pool
821            .execute_with_retry(|_conn_id| -> Result<u64> {
822                Err(DistributedError::Worker("permanent error".to_string()))
823            })
824            .await;
825        assert!(result.is_err());
826    }
827
828    #[tokio::test]
829    async fn test_pool_shutdown() {
830        let pool = small_pool(4, 3);
831        pool.initialize().await.expect("init should succeed");
832        assert_eq!(pool.size().await, 3);
833
834        pool.shutdown().await.expect("shutdown should succeed");
835        assert_eq!(pool.size().await, 0);
836
837        // Should reject new checkouts
838        let result = pool.checkout().await;
839        assert!(result.is_err());
840    }
841
842    #[tokio::test]
843    async fn test_pool_stats() {
844        let pool = small_pool(4, 1);
845        pool.initialize().await.expect("init should succeed");
846
847        let c1 = pool.checkout().await.expect("checkout should succeed");
848        pool.checkin(c1).await.expect("checkin should succeed");
849
850        let stats = pool.stats().await;
851        assert!(stats.total_created >= 1);
852        assert!(stats.total_checkouts >= 1);
853        assert_eq!(stats.current_in_use, 0);
854    }
855
856    #[tokio::test]
857    async fn test_pool_checkin_nonexistent() {
858        let pool = small_pool(4, 0);
859        pool.initialize().await.expect("init should succeed");
860
861        let result = pool.checkin(999).await;
862        assert!(result.is_err());
863    }
864
865    #[tokio::test]
866    async fn test_pool_mark_broken_nonexistent() {
867        let pool = small_pool(4, 0);
868        pool.initialize().await.expect("init should succeed");
869
870        let result = pool.mark_broken(999).await;
871        assert!(result.is_err());
872    }
873
874    #[tokio::test]
875    async fn test_pool_multiple_checkouts() {
876        let pool = small_pool(4, 0);
877        pool.initialize().await.expect("init should succeed");
878
879        let c1 = pool.checkout().await.expect("checkout 1 should succeed");
880        let c2 = pool.checkout().await.expect("checkout 2 should succeed");
881        let c3 = pool.checkout().await.expect("checkout 3 should succeed");
882
883        assert_ne!(c1, c2);
884        assert_ne!(c2, c3);
885
886        let stats = pool.stats().await;
887        assert_eq!(stats.current_in_use, 3);
888        assert_eq!(stats.current_size, 3);
889
890        pool.checkin(c1).await.expect("checkin should succeed");
891        pool.checkin(c2).await.expect("checkin should succeed");
892        pool.checkin(c3).await.expect("checkin should succeed");
893
894        let stats = pool.stats().await;
895        assert_eq!(stats.current_in_use, 0);
896    }
897
898    #[tokio::test]
899    async fn test_pool_reuses_idle_connections() {
900        let pool = small_pool(4, 1);
901        pool.initialize().await.expect("init should succeed");
902
903        let c1 = pool.checkout().await.expect("checkout 1 should succeed");
904        pool.checkin(c1).await.expect("checkin should succeed");
905
906        let c2 = pool.checkout().await.expect("checkout 2 should succeed");
907        // Should reuse the same connection
908        assert_eq!(c1, c2);
909        pool.checkin(c2).await.expect("checkin should succeed");
910    }
911}