Skip to main content

ipfrs_network/
connection_pool_manager.rs

1//! Connection Pool Manager
2//!
3//! Production-quality connection pool with health checking, adaptive sizing,
4//! multiple acquisition policies, idle/lifetime eviction, and event streaming.
5//!
6//! # Overview
7//!
8//! [`ConnectionPoolManager`] maintains a collection of [`PooledConnection`] objects
9//! keyed by auto-increment IDs. Connections move through the [`ConnState`] lifecycle
10//! (`Idle → InUse → Idle | Draining | Closed`) and are evicted by background
11//! maintenance when they exceed configured timeouts.
12//!
13//! # Example
14//!
15//! ```rust
16//! use ipfrs_network::connection_pool_manager::{
17//!     ConnectionPoolManager, CpmPoolConfig, AcquirePolicy,
18//! };
19//!
20//! let config = CpmPoolConfig {
21//!     min_connections: 1,
22//!     max_connections: 8,
23//!     idle_timeout_us: 30_000_000,
24//!     max_lifetime_us: 3_600_000_000,
25//!     health_check_interval_us: 10_000_000,
26//!     acquire_timeout_us: 5_000_000,
27//!     policy: AcquirePolicy::HealthBest,
28//! };
29//! let mut pool = ConnectionPoolManager::new(config);
30//! let id = pool.add_connection("127.0.0.1:9000".to_string(), vec![]).unwrap();
31//! let acquired = pool.acquire(1_000).unwrap();
32//! pool.release(acquired, 2_000, false).unwrap();
33//! ```
34
35use std::collections::HashMap;
36use thiserror::Error;
37
38// ---------------------------------------------------------------------------
39// PRNG helper (no rand crate)
40// ---------------------------------------------------------------------------
41
42/// Simple xorshift64 PRNG used for tie-breaking and test randomness.
43#[inline]
44pub fn xorshift64(state: &mut u64) -> u64 {
45    let mut x = *state;
46    x ^= x << 13;
47    x ^= x >> 7;
48    x ^= x << 17;
49    *state = x;
50    x
51}
52
53// ---------------------------------------------------------------------------
54// ConnState
55// ---------------------------------------------------------------------------
56
57/// Lifecycle state of a single pooled connection.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum ConnState {
60    /// Available for acquisition.
61    Idle,
62    /// Currently borrowed by a caller.
63    InUse {
64        /// Microsecond timestamp when the connection was borrowed.
65        borrowed_at: u64,
66    },
67    /// Marked for graceful shutdown; no new acquires allowed.
68    Draining,
69    /// Permanently closed and may be removed.
70    Closed,
71    /// Health probe in progress.
72    HealthChecking,
73}
74
75// ---------------------------------------------------------------------------
76// AcquirePolicy
77// ---------------------------------------------------------------------------
78
79/// Strategy used to pick an idle connection during [`ConnectionPoolManager::acquire`].
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum AcquirePolicy {
82    /// Take the oldest idle connection (first-in, first-out by creation time).
83    Fifo,
84    /// Take the most recently created idle connection.
85    Lifo,
86    /// Take the connection with the lowest `use_count`.
87    LeastUsed,
88    /// Take the connection with the highest `health_score`.
89    HealthBest,
90    /// Cycle through idle connections in insertion order.
91    RoundRobin,
92}
93
94// ---------------------------------------------------------------------------
95// CpmPoolConfig  (Cpm prefix avoids collision with connection_pool::PoolConfig)
96// ---------------------------------------------------------------------------
97
98/// Configuration for [`ConnectionPoolManager`].
99#[derive(Debug, Clone)]
100pub struct CpmPoolConfig {
101    /// Minimum number of open connections to maintain.
102    pub min_connections: usize,
103    /// Maximum total connections allowed in the pool.
104    pub max_connections: usize,
105    /// Microseconds a connection may remain idle before eviction.
106    pub idle_timeout_us: u64,
107    /// Absolute connection lifetime in microseconds.
108    pub max_lifetime_us: u64,
109    /// How often health checks should run (informational; not auto-scheduled).
110    pub health_check_interval_us: u64,
111    /// Maximum microseconds `acquire` will wait before returning `AcquireTimeout`.
112    pub acquire_timeout_us: u64,
113    /// Acquisition selection policy.
114    pub policy: AcquirePolicy,
115}
116
117impl Default for CpmPoolConfig {
118    fn default() -> Self {
119        Self {
120            min_connections: 1,
121            max_connections: 16,
122            idle_timeout_us: 60_000_000,
123            max_lifetime_us: 3_600_000_000,
124            health_check_interval_us: 10_000_000,
125            acquire_timeout_us: 5_000_000,
126            policy: AcquirePolicy::HealthBest,
127        }
128    }
129}
130
131// ---------------------------------------------------------------------------
132// PooledConnection
133// ---------------------------------------------------------------------------
134
135/// A single managed connection inside [`ConnectionPoolManager`].
136#[derive(Debug, Clone)]
137pub struct PooledConnection {
138    /// Unique connection identifier (auto-increment).
139    pub id: u64,
140    /// Remote address string (e.g. `"192.168.1.1:4001"`).
141    pub peer_address: String,
142    /// Microsecond timestamp when this connection was created.
143    pub created_at: u64,
144    /// Microsecond timestamp of the last time this connection was used.
145    pub last_used: u64,
146    /// Total number of times this connection has been acquired.
147    pub use_count: u64,
148    /// Health score in `[0.0, 1.0]`; higher is healthier.
149    pub health_score: f64,
150    /// Arbitrary tags attached at creation time.
151    pub tags: Vec<String>,
152    /// Current lifecycle state.
153    pub state: ConnState,
154}
155
156impl PooledConnection {
157    fn new(id: u64, peer_address: String, tags: Vec<String>, created_at: u64) -> Self {
158        Self {
159            id,
160            peer_address,
161            created_at,
162            last_used: created_at,
163            use_count: 0,
164            health_score: 1.0,
165            tags,
166            state: ConnState::Idle,
167        }
168    }
169
170    /// Returns `true` if the connection is currently idle and can be acquired.
171    #[inline]
172    pub fn is_idle(&self) -> bool {
173        self.state == ConnState::Idle
174    }
175}
176
177// ---------------------------------------------------------------------------
178// CpmPoolStats  (Cpm prefix avoids collision with connection_pool::PoolStats)
179// ---------------------------------------------------------------------------
180
181/// Point-in-time statistics snapshot for a [`ConnectionPoolManager`].
182#[derive(Debug, Clone, Default)]
183pub struct CpmPoolStats {
184    /// Total connections currently tracked (all states).
185    pub total_connections: usize,
186    /// Connections in the `Idle` state.
187    pub idle_connections: usize,
188    /// Connections in the `InUse` state.
189    pub in_use_connections: usize,
190    /// Cumulative successful acquire operations.
191    pub total_acquires: u64,
192    /// Cumulative release operations.
193    pub total_releases: u64,
194    /// Cumulative acquire failures (pool exhausted or timeout).
195    pub acquire_failures: u64,
196    /// Cumulative health-check failures (score < 0.3).
197    pub health_check_failures: u64,
198    /// Average health score across all connections (NaN if no connections).
199    pub avg_health_score: f64,
200}
201
202// ---------------------------------------------------------------------------
203// PoolEvent
204// ---------------------------------------------------------------------------
205
206/// Asynchronous event emitted by pool operations.
207#[derive(Debug, Clone)]
208pub enum PoolEvent {
209    /// A new connection was added to the pool.
210    ConnectionAdded(u64),
211    /// A connection was removed from the pool.
212    ConnectionRemoved {
213        /// ID of the removed connection.
214        id: u64,
215        /// Human-readable reason string.
216        reason: String,
217    },
218    /// A health check reported a critically low score.
219    HealthCheckFailed {
220        /// Connection ID that failed the health check.
221        id: u64,
222        /// Observed score that triggered the failure.
223        score: f64,
224    },
225    /// An acquire call timed out.
226    AcquireTimeout,
227    /// All connections have been drained via [`ConnectionPoolManager::drain`].
228    PoolDrained,
229}
230
231// ---------------------------------------------------------------------------
232// CpmPoolError  (Cpm prefix avoids collision with connection_pool::PoolError)
233// ---------------------------------------------------------------------------
234
235/// Errors returned by [`ConnectionPoolManager`] operations.
236#[derive(Debug, Error)]
237pub enum CpmPoolError {
238    /// All connections are in-use and the pool is at maximum capacity.
239    #[error("connection pool exhausted (max {0} connections)")]
240    PoolExhausted(usize),
241
242    /// No connection with the supplied ID exists in the pool.
243    #[error("connection {0} not found in pool")]
244    ConnectionNotFound(u64),
245
246    /// Acquire timed out after the given number of microseconds.
247    #[error("acquire timed out after {0} µs")]
248    AcquireTimeout(u64),
249
250    /// Health check failed for the given connection ID.
251    #[error("health check failed for connection {0}")]
252    HealthCheckFailed(u64),
253
254    /// The supplied configuration is invalid.
255    #[error("invalid configuration: {0}")]
256    InvalidConfiguration(String),
257}
258
259// ---------------------------------------------------------------------------
260// ConnectionPoolManager
261// ---------------------------------------------------------------------------
262
263/// Adaptive connection pool with health checking, multiple acquisition policies,
264/// idle/lifetime eviction, event streaming, and dynamic resize.
265pub struct ConnectionPoolManager {
266    config: CpmPoolConfig,
267    connections: HashMap<u64, PooledConnection>,
268    next_id: u64,
269    round_robin_cursor: usize,
270    // Cumulative statistics.
271    total_acquires: u64,
272    total_releases: u64,
273    acquire_failures: u64,
274    health_check_failures: u64,
275    // Pending events to be drained by the caller.
276    pending_events: Vec<PoolEvent>,
277}
278
279impl ConnectionPoolManager {
280    /// Create a new pool with the given configuration.
281    ///
282    /// Returns [`CpmPoolError::InvalidConfiguration`] if `min > max` or `max == 0`.
283    pub fn new(config: CpmPoolConfig) -> Self {
284        Self {
285            config,
286            connections: HashMap::new(),
287            next_id: 1,
288            round_robin_cursor: 0,
289            total_acquires: 0,
290            total_releases: 0,
291            acquire_failures: 0,
292            health_check_failures: 0,
293            pending_events: Vec::new(),
294        }
295    }
296
297    /// Validate the current configuration.
298    pub fn validate_config(config: &CpmPoolConfig) -> Result<(), CpmPoolError> {
299        if config.max_connections == 0 {
300            return Err(CpmPoolError::InvalidConfiguration(
301                "max_connections must be > 0".to_string(),
302            ));
303        }
304        if config.min_connections > config.max_connections {
305            return Err(CpmPoolError::InvalidConfiguration(format!(
306                "min_connections ({}) > max_connections ({})",
307                config.min_connections, config.max_connections
308            )));
309        }
310        Ok(())
311    }
312
313    // -----------------------------------------------------------------------
314    // Core CRUD
315    // -----------------------------------------------------------------------
316
317    /// Add a new connection to the pool.
318    ///
319    /// Returns the new connection's unique ID, or [`CpmPoolError::PoolExhausted`] if
320    /// `max_connections` has been reached.
321    pub fn add_connection(
322        &mut self,
323        address: String,
324        tags: Vec<String>,
325    ) -> Result<u64, CpmPoolError> {
326        if self.connections.len() >= self.config.max_connections {
327            self.acquire_failures += 1;
328            return Err(CpmPoolError::PoolExhausted(self.config.max_connections));
329        }
330        let id = self.next_id;
331        self.next_id += 1;
332        let conn = PooledConnection::new(id, address, tags, 0);
333        self.connections.insert(id, conn);
334        self.pending_events.push(PoolEvent::ConnectionAdded(id));
335        Ok(id)
336    }
337
338    /// Add a connection with an explicit creation timestamp (useful in tests).
339    pub fn add_connection_at(
340        &mut self,
341        address: String,
342        tags: Vec<String>,
343        created_at: u64,
344    ) -> Result<u64, CpmPoolError> {
345        if self.connections.len() >= self.config.max_connections {
346            self.acquire_failures += 1;
347            return Err(CpmPoolError::PoolExhausted(self.config.max_connections));
348        }
349        let id = self.next_id;
350        self.next_id += 1;
351        let mut conn = PooledConnection::new(id, address, tags, created_at);
352        conn.last_used = created_at;
353        self.connections.insert(id, conn);
354        self.pending_events.push(PoolEvent::ConnectionAdded(id));
355        Ok(id)
356    }
357
358    /// Acquire an idle connection according to the configured [`AcquirePolicy`].
359    ///
360    /// Returns the ID of the acquired connection, or an error if:
361    /// - No idle connections exist and the pool is at capacity → [`CpmPoolError::PoolExhausted`]
362    /// - All connections are busy and no idle ones can be selected →
363    ///   [`CpmPoolError::AcquireTimeout`] (using `acquire_timeout_us` as the waited value)
364    pub fn acquire(&mut self, current_ts: u64) -> Result<u64, CpmPoolError> {
365        let chosen_id = self.select_idle(current_ts)?;
366
367        if let Some(conn) = self.connections.get_mut(&chosen_id) {
368            conn.state = ConnState::InUse {
369                borrowed_at: current_ts,
370            };
371            conn.use_count += 1;
372            conn.last_used = current_ts;
373        }
374        self.total_acquires += 1;
375        Ok(chosen_id)
376    }
377
378    /// Release a previously acquired connection back to `Idle`.
379    ///
380    /// If `health_degraded` is `true`, the health score is reduced by `0.1`
381    /// (floored at `0.0`).
382    pub fn release(
383        &mut self,
384        conn_id: u64,
385        current_ts: u64,
386        health_degraded: bool,
387    ) -> Result<(), CpmPoolError> {
388        let conn = self
389            .connections
390            .get_mut(&conn_id)
391            .ok_or(CpmPoolError::ConnectionNotFound(conn_id))?;
392
393        conn.state = ConnState::Idle;
394        conn.last_used = current_ts;
395        if health_degraded {
396            conn.health_score = (conn.health_score - 0.1_f64).max(0.0_f64);
397        }
398        self.total_releases += 1;
399        Ok(())
400    }
401
402    /// Permanently remove a connection from the pool.
403    pub fn remove_connection(&mut self, conn_id: u64) -> Result<(), CpmPoolError> {
404        if self.connections.remove(&conn_id).is_none() {
405            return Err(CpmPoolError::ConnectionNotFound(conn_id));
406        }
407        self.pending_events.push(PoolEvent::ConnectionRemoved {
408            id: conn_id,
409            reason: "explicit removal".to_string(),
410        });
411        Ok(())
412    }
413
414    // -----------------------------------------------------------------------
415    // Health
416    // -----------------------------------------------------------------------
417
418    /// Record the result of a health check for the given connection.
419    ///
420    /// If `score < 0.3`, emits a [`PoolEvent::HealthCheckFailed`] and returns it.
421    /// Otherwise returns [`PoolEvent::ConnectionAdded`] as a no-op sentinel
422    /// (callers should match on the returned variant).
423    pub fn health_check(
424        &mut self,
425        conn_id: u64,
426        score: f64,
427        current_ts: u64,
428    ) -> Result<PoolEvent, CpmPoolError> {
429        let conn = self
430            .connections
431            .get_mut(&conn_id)
432            .ok_or(CpmPoolError::ConnectionNotFound(conn_id))?;
433
434        conn.health_score = score.clamp(0.0, 1.0);
435        conn.last_used = current_ts;
436
437        if score < 0.3 {
438            self.health_check_failures += 1;
439            let event = PoolEvent::HealthCheckFailed { id: conn_id, score };
440            self.pending_events.push(event.clone());
441            Ok(event)
442        } else {
443            Ok(PoolEvent::ConnectionAdded(conn_id))
444        }
445    }
446
447    // -----------------------------------------------------------------------
448    // Maintenance
449    // -----------------------------------------------------------------------
450
451    /// Evict connections that have exceeded idle or absolute lifetime limits.
452    ///
453    /// Returns a list of [`PoolEvent::ConnectionRemoved`] events for each evicted
454    /// connection.
455    pub fn run_maintenance(&mut self, current_ts: u64) -> Vec<PoolEvent> {
456        let idle_timeout = self.config.idle_timeout_us;
457        let max_lifetime = self.config.max_lifetime_us;
458
459        // Collect IDs to evict (avoid borrow issues).
460        let mut to_evict: Vec<(u64, String)> = Vec::new();
461        for (id, conn) in &self.connections {
462            if conn.state == ConnState::Closed {
463                to_evict.push((*id, "already closed".to_string()));
464                continue;
465            }
466            // Lifetime eviction (applies to all non-InUse connections).
467            if matches!(conn.state, ConnState::Idle | ConnState::HealthChecking) {
468                let age = current_ts.saturating_sub(conn.created_at);
469                if age >= max_lifetime {
470                    to_evict.push((*id, "max lifetime exceeded".to_string()));
471                    continue;
472                }
473                // Idle timeout eviction.
474                let idle_time = current_ts.saturating_sub(conn.last_used);
475                if idle_time >= idle_timeout {
476                    to_evict.push((*id, "idle timeout exceeded".to_string()));
477                }
478            }
479        }
480
481        let mut events = Vec::with_capacity(to_evict.len());
482        for (id, reason) in to_evict {
483            self.connections.remove(&id);
484            let ev = PoolEvent::ConnectionRemoved { id, reason };
485            self.pending_events.push(ev.clone());
486            events.push(ev);
487        }
488        events
489    }
490
491    // -----------------------------------------------------------------------
492    // Resize
493    // -----------------------------------------------------------------------
494
495    /// Resize the pool's maximum connection limit.
496    ///
497    /// When shrinking, excess idle connections are drained (closed) immediately.
498    pub fn resize(&mut self, new_max: usize) -> Result<(), CpmPoolError> {
499        if new_max == 0 {
500            return Err(CpmPoolError::InvalidConfiguration(
501                "new_max must be > 0".to_string(),
502            ));
503        }
504        let old_max = self.config.max_connections;
505        self.config.max_connections = new_max;
506
507        if new_max < old_max {
508            // Drain idle connections until we are within the new limit.
509            let excess = self.connections.len().saturating_sub(new_max);
510            if excess > 0 {
511                let idle_ids: Vec<u64> = self
512                    .connections
513                    .values()
514                    .filter(|c| c.state == ConnState::Idle)
515                    .map(|c| c.id)
516                    .take(excess)
517                    .collect();
518                for id in idle_ids {
519                    self.connections.remove(&id);
520                    self.pending_events.push(PoolEvent::ConnectionRemoved {
521                        id,
522                        reason: "pool resize shrink".to_string(),
523                    });
524                }
525            }
526        }
527        Ok(())
528    }
529
530    // -----------------------------------------------------------------------
531    // Drain
532    // -----------------------------------------------------------------------
533
534    /// Close all connections in insertion order and return their IDs.
535    pub fn drain(&mut self) -> Vec<u64> {
536        let mut ids: Vec<u64> = self.connections.keys().copied().collect();
537        ids.sort_unstable(); // insertion order approximated by ascending ID.
538
539        for &id in &ids {
540            self.pending_events.push(PoolEvent::ConnectionRemoved {
541                id,
542                reason: "pool drain".to_string(),
543            });
544        }
545        self.connections.clear();
546        self.pending_events.push(PoolEvent::PoolDrained);
547        ids
548    }
549
550    // -----------------------------------------------------------------------
551    // Query helpers
552    // -----------------------------------------------------------------------
553
554    /// Return references to all connections that carry the given tag.
555    pub fn connections_with_tag(&self, tag: &str) -> Vec<&PooledConnection> {
556        self.connections
557            .values()
558            .filter(|c| c.tags.iter().any(|t| t == tag))
559            .collect()
560    }
561
562    /// Compute a statistics snapshot.
563    pub fn stats(&self) -> CpmPoolStats {
564        let total = self.connections.len();
565        let idle = self
566            .connections
567            .values()
568            .filter(|c| c.state == ConnState::Idle)
569            .count();
570        let in_use = self
571            .connections
572            .values()
573            .filter(|c| matches!(c.state, ConnState::InUse { .. }))
574            .count();
575
576        let avg_health = if total == 0 {
577            f64::NAN
578        } else {
579            let sum: f64 = self.connections.values().map(|c| c.health_score).sum();
580            sum / total as f64
581        };
582
583        CpmPoolStats {
584            total_connections: total,
585            idle_connections: idle,
586            in_use_connections: in_use,
587            total_acquires: self.total_acquires,
588            total_releases: self.total_releases,
589            acquire_failures: self.acquire_failures,
590            health_check_failures: self.health_check_failures,
591            avg_health_score: avg_health,
592        }
593    }
594
595    /// Drain and return all pending events.
596    pub fn drain_events(&mut self) -> Vec<PoolEvent> {
597        std::mem::take(&mut self.pending_events)
598    }
599
600    /// Returns a reference to the underlying connection map (read-only).
601    pub fn connections(&self) -> &HashMap<u64, PooledConnection> {
602        &self.connections
603    }
604
605    /// Returns the current configuration.
606    pub fn config(&self) -> &CpmPoolConfig {
607        &self.config
608    }
609
610    // -----------------------------------------------------------------------
611    // Policy-based idle selection (private)
612    // -----------------------------------------------------------------------
613
614    fn select_idle(&mut self, _current_ts: u64) -> Result<u64, CpmPoolError> {
615        // Build a list of idle connection IDs (and relevant metadata for sorting).
616        let idle: Vec<(u64, u64, u64, f64)> = self
617            .connections
618            .values()
619            .filter(|c| c.state == ConnState::Idle)
620            .map(|c| (c.id, c.created_at, c.use_count, c.health_score))
621            .collect();
622
623        if idle.is_empty() {
624            self.acquire_failures += 1;
625            if self.connections.len() >= self.config.max_connections {
626                return Err(CpmPoolError::PoolExhausted(self.config.max_connections));
627            }
628            // Pool has room but no idle connections — treat as timeout.
629            return Err(CpmPoolError::AcquireTimeout(self.config.acquire_timeout_us));
630        }
631
632        let chosen_id = match self.config.policy {
633            AcquirePolicy::Fifo => {
634                // Oldest created_at first.
635                idle.iter()
636                    .min_by_key(|&&(_, created_at, _, _)| created_at)
637                    .map(|&(id, _, _, _)| id)
638                    .expect("idle is non-empty")
639            }
640            AcquirePolicy::Lifo => {
641                // Newest created_at first.
642                idle.iter()
643                    .max_by_key(|&&(_, created_at, _, _)| created_at)
644                    .map(|&(id, _, _, _)| id)
645                    .expect("idle is non-empty")
646            }
647            AcquirePolicy::LeastUsed => idle
648                .iter()
649                .min_by_key(|&&(_, _, use_count, _)| use_count)
650                .map(|&(id, _, _, _)| id)
651                .expect("idle is non-empty"),
652            AcquirePolicy::HealthBest => {
653                // Highest health score; use ID as stable tie-breaker.
654                idle.iter()
655                    .max_by(|a, b| {
656                        a.3.partial_cmp(&b.3)
657                            .unwrap_or(std::cmp::Ordering::Equal)
658                            .then_with(|| a.0.cmp(&b.0))
659                    })
660                    .map(|&(id, _, _, _)| id)
661                    .expect("idle is non-empty")
662            }
663            AcquirePolicy::RoundRobin => {
664                // Sort by ID for stable ordering, then advance cursor.
665                let mut sorted: Vec<u64> = idle.iter().map(|&(id, _, _, _)| id).collect();
666                sorted.sort_unstable();
667                let idx = self.round_robin_cursor % sorted.len();
668                self.round_robin_cursor = self.round_robin_cursor.wrapping_add(1);
669                sorted[idx]
670            }
671        };
672
673        Ok(chosen_id)
674    }
675}
676
677// ===========================================================================
678// Tests
679// ===========================================================================
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    // -----------------------------------------------------------------------
686    // Helpers
687    // -----------------------------------------------------------------------
688
689    fn default_config() -> CpmPoolConfig {
690        CpmPoolConfig {
691            min_connections: 1,
692            max_connections: 10,
693            idle_timeout_us: 100_000,
694            max_lifetime_us: 1_000_000,
695            health_check_interval_us: 50_000,
696            acquire_timeout_us: 5_000,
697            policy: AcquirePolicy::Fifo,
698        }
699    }
700
701    fn make_pool() -> ConnectionPoolManager {
702        ConnectionPoolManager::new(default_config())
703    }
704
705    fn populate(pool: &mut ConnectionPoolManager, n: usize) -> Vec<u64> {
706        (0..n)
707            .map(|i| {
708                pool.add_connection(format!("127.0.0.1:{}", 9000 + i), vec![])
709                    .expect("add_connection failed")
710            })
711            .collect()
712    }
713
714    // -----------------------------------------------------------------------
715    // 1. add_connection
716    // -----------------------------------------------------------------------
717
718    #[test]
719    fn test_add_connection_basic() {
720        let mut pool = make_pool();
721        let id = pool
722            .add_connection("127.0.0.1:9000".to_string(), vec![])
723            .expect("test: add_connection should succeed");
724        assert_eq!(id, 1);
725        assert_eq!(pool.connections().len(), 1);
726    }
727
728    #[test]
729    fn test_add_connection_increments_id() {
730        let mut pool = make_pool();
731        let id1 = pool
732            .add_connection("a".to_string(), vec![])
733            .expect("test: add_connection a should succeed");
734        let id2 = pool
735            .add_connection("b".to_string(), vec![])
736            .expect("test: add_connection b should succeed");
737        assert!(id2 > id1);
738    }
739
740    #[test]
741    fn test_add_connection_emits_event() {
742        let mut pool = make_pool();
743        let id = pool
744            .add_connection("x".to_string(), vec![])
745            .expect("test: add_connection should succeed");
746        let events = pool.drain_events();
747        assert!(events
748            .iter()
749            .any(|e| matches!(e, PoolEvent::ConnectionAdded(i) if *i == id)));
750    }
751
752    #[test]
753    fn test_add_connection_at_capacity_fails() {
754        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
755            max_connections: 2,
756            ..default_config()
757        });
758        pool.add_connection("a".to_string(), vec![])
759            .expect("test: first add_connection should succeed");
760        pool.add_connection("b".to_string(), vec![])
761            .expect("test: second add_connection should succeed");
762        let err = pool.add_connection("c".to_string(), vec![]).unwrap_err();
763        assert!(matches!(err, CpmPoolError::PoolExhausted(_)));
764    }
765
766    #[test]
767    fn test_add_connection_new_state_is_idle() {
768        let mut pool = make_pool();
769        let id = pool
770            .add_connection("h".to_string(), vec![])
771            .expect("test: add_connection should succeed");
772        let conn = pool
773            .connections()
774            .get(&id)
775            .expect("test: connection should exist after add");
776        assert_eq!(conn.state, ConnState::Idle);
777    }
778
779    #[test]
780    fn test_add_connection_with_tags() {
781        let mut pool = make_pool();
782        let tags = vec!["region:eu".to_string(), "tier:1".to_string()];
783        let id = pool
784            .add_connection("t".to_string(), tags.clone())
785            .expect("test: add_connection with tags should succeed");
786        let conn = pool
787            .connections()
788            .get(&id)
789            .expect("test: connection should exist after add");
790        assert_eq!(conn.tags, tags);
791    }
792
793    // -----------------------------------------------------------------------
794    // 2. acquire / release
795    // -----------------------------------------------------------------------
796
797    #[test]
798    fn test_acquire_idle_connection() {
799        let mut pool = make_pool();
800        let id = pool
801            .add_connection("a".to_string(), vec![])
802            .expect("test: add_connection should succeed");
803        let acquired = pool
804            .acquire(100)
805            .expect("test: acquire should succeed with idle connection");
806        assert_eq!(acquired, id);
807        let conn = pool
808            .connections()
809            .get(&id)
810            .expect("test: connection should exist");
811        assert!(matches!(conn.state, ConnState::InUse { .. }));
812    }
813
814    #[test]
815    fn test_acquire_increments_use_count() {
816        let mut pool = make_pool();
817        let id = pool
818            .add_connection("a".to_string(), vec![])
819            .expect("test: add_connection should succeed");
820        pool.acquire(100)
821            .expect("test: first acquire should succeed");
822        pool.release(id, 200, false)
823            .expect("test: release should succeed");
824        pool.acquire(300)
825            .expect("test: second acquire should succeed");
826        let conn = pool
827            .connections()
828            .get(&id)
829            .expect("test: connection should exist");
830        assert_eq!(conn.use_count, 2);
831    }
832
833    #[test]
834    fn test_acquire_updates_last_used() {
835        let mut pool = make_pool();
836        let id = pool
837            .add_connection("a".to_string(), vec![])
838            .expect("test: add_connection should succeed");
839        pool.acquire(999).expect("test: acquire should succeed");
840        let conn = pool
841            .connections()
842            .get(&id)
843            .expect("test: connection should exist");
844        assert_eq!(conn.last_used, 999);
845    }
846
847    #[test]
848    fn test_acquire_no_idle_pool_full() {
849        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
850            max_connections: 1,
851            ..default_config()
852        });
853        let id = pool
854            .add_connection("a".to_string(), vec![])
855            .expect("test: add_connection should succeed");
856        pool.acquire(100)
857            .expect("test: first acquire should succeed"); // now InUse
858        let err = pool.acquire(200).unwrap_err();
859        assert!(matches!(err, CpmPoolError::PoolExhausted(_)));
860        // release and re-acquire succeeds
861        pool.release(id, 300, false)
862            .expect("test: release should succeed");
863        assert!(pool.acquire(400).is_ok());
864    }
865
866    #[test]
867    fn test_release_transitions_to_idle() {
868        let mut pool = make_pool();
869        let id = pool
870            .add_connection("a".to_string(), vec![])
871            .expect("test: add_connection should succeed");
872        pool.acquire(100).expect("test: acquire should succeed");
873        pool.release(id, 200, false)
874            .expect("test: release should succeed");
875        let conn = pool
876            .connections()
877            .get(&id)
878            .expect("test: connection should exist");
879        assert_eq!(conn.state, ConnState::Idle);
880    }
881
882    #[test]
883    fn test_release_health_degraded_reduces_score() {
884        let mut pool = make_pool();
885        let id = pool
886            .add_connection("a".to_string(), vec![])
887            .expect("test: add_connection should succeed");
888        pool.acquire(100).expect("test: acquire should succeed");
889        pool.release(id, 200, true)
890            .expect("test: release with health degraded should succeed");
891        let conn = pool
892            .connections()
893            .get(&id)
894            .expect("test: connection should exist");
895        assert!((conn.health_score - 0.9).abs() < 1e-9);
896    }
897
898    #[test]
899    fn test_release_health_floor_at_zero() {
900        let mut pool = make_pool();
901        let id = pool
902            .add_connection("a".to_string(), vec![])
903            .expect("test: add_connection should succeed");
904        // Degrade 11 times from 1.0 → floor at 0.0
905        for _ in 0..11 {
906            if pool
907                .connections()
908                .get(&id)
909                .expect("test: connection should exist")
910                .is_idle()
911            {
912                pool.acquire(1).expect("test: acquire should succeed");
913            }
914            pool.release(id, 2, true)
915                .expect("test: release should succeed");
916        }
917        let conn = pool
918            .connections()
919            .get(&id)
920            .expect("test: connection should exist");
921        assert_eq!(conn.health_score, 0.0);
922    }
923
924    #[test]
925    fn test_release_unknown_id_error() {
926        let mut pool = make_pool();
927        let err = pool.release(999, 100, false).unwrap_err();
928        assert!(matches!(err, CpmPoolError::ConnectionNotFound(999)));
929    }
930
931    // -----------------------------------------------------------------------
932    // 3. AcquirePolicy::Fifo
933    // -----------------------------------------------------------------------
934
935    #[test]
936    fn test_policy_fifo() {
937        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
938            policy: AcquirePolicy::Fifo,
939            ..default_config()
940        });
941        let id1 = pool
942            .add_connection_at("a".to_string(), vec![], 100)
943            .expect("test: add_connection_at a should succeed");
944        let id2 = pool
945            .add_connection_at("b".to_string(), vec![], 200)
946            .expect("test: add_connection_at b should succeed");
947        let acquired = pool
948            .acquire(300)
949            .expect("test: first acquire should succeed");
950        assert_eq!(acquired, id1, "FIFO should pick oldest (id1)");
951        pool.release(id1, 400, false)
952            .expect("test: release should succeed");
953        let acquired2 = pool
954            .acquire(500)
955            .expect("test: second acquire should succeed");
956        assert_eq!(acquired2, id1, "After release, id1 is oldest again");
957        let _ = acquired2;
958        let _ = id2;
959    }
960
961    // -----------------------------------------------------------------------
962    // 4. AcquirePolicy::Lifo
963    // -----------------------------------------------------------------------
964
965    #[test]
966    fn test_policy_lifo() {
967        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
968            policy: AcquirePolicy::Lifo,
969            ..default_config()
970        });
971        let _id1 = pool
972            .add_connection_at("a".to_string(), vec![], 100)
973            .expect("test: add_connection_at a should succeed");
974        let id2 = pool
975            .add_connection_at("b".to_string(), vec![], 200)
976            .expect("test: add_connection_at b should succeed");
977        let acquired = pool.acquire(300).expect("test: acquire should succeed");
978        assert_eq!(acquired, id2, "LIFO should pick newest (id2)");
979    }
980
981    // -----------------------------------------------------------------------
982    // 5. AcquirePolicy::LeastUsed
983    // -----------------------------------------------------------------------
984
985    #[test]
986    fn test_policy_least_used() {
987        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
988            policy: AcquirePolicy::LeastUsed,
989            ..default_config()
990        });
991        let id1 = pool
992            .add_connection("a".to_string(), vec![])
993            .expect("test: add_connection a should succeed");
994        let id2 = pool
995            .add_connection("b".to_string(), vec![])
996            .expect("test: add_connection b should succeed");
997        // Manually bump id1's use_count to 5 so id2 (0 uses) wins LeastUsed.
998        pool.connections
999            .get_mut(&id1)
1000            .expect("test: connection id1 should exist")
1001            .use_count = 5;
1002        // Now id2 has 0 uses → LeastUsed should pick id2.
1003        let acquired = pool.acquire(5).expect("test: acquire should succeed");
1004        assert_eq!(acquired, id2);
1005    }
1006
1007    // -----------------------------------------------------------------------
1008    // 6. AcquirePolicy::HealthBest
1009    // -----------------------------------------------------------------------
1010
1011    #[test]
1012    fn test_policy_health_best() {
1013        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1014            policy: AcquirePolicy::HealthBest,
1015            ..default_config()
1016        });
1017        let id1 = pool
1018            .add_connection("a".to_string(), vec![])
1019            .expect("test: add_connection a should succeed");
1020        let id2 = pool
1021            .add_connection("b".to_string(), vec![])
1022            .expect("test: add_connection b should succeed");
1023        // Lower id1's health.
1024        pool.health_check(id1, 0.4, 0)
1025            .expect("test: health_check id1 should succeed");
1026        pool.health_check(id2, 0.9, 0)
1027            .expect("test: health_check id2 should succeed");
1028        let acquired = pool.acquire(1).expect("test: acquire should succeed");
1029        assert_eq!(acquired, id2, "HealthBest should pick id2 (higher score)");
1030    }
1031
1032    // -----------------------------------------------------------------------
1033    // 7. AcquirePolicy::RoundRobin
1034    // -----------------------------------------------------------------------
1035
1036    #[test]
1037    fn test_policy_round_robin() {
1038        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1039            policy: AcquirePolicy::RoundRobin,
1040            ..default_config()
1041        });
1042        let id1 = pool
1043            .add_connection("a".to_string(), vec![])
1044            .expect("test: add_connection a should succeed");
1045        let id2 = pool
1046            .add_connection("b".to_string(), vec![])
1047            .expect("test: add_connection b should succeed");
1048        let id3 = pool
1049            .add_connection("c".to_string(), vec![])
1050            .expect("test: add_connection c should succeed");
1051
1052        // First acquire → cursor=0 → id1
1053        let a = pool.acquire(1).expect("test: first acquire should succeed");
1054        assert_eq!(a, id1);
1055        pool.release(id1, 2, false)
1056            .expect("test: release id1 should succeed");
1057
1058        // Second acquire: only id2, id3 are idle → sorted = [id2, id3], cursor=1 → id2
1059        let b = pool
1060            .acquire(3)
1061            .expect("test: second acquire should succeed");
1062        assert_eq!(b, id2);
1063        pool.release(id2, 4, false)
1064            .expect("test: release id2 should succeed");
1065
1066        // Third acquire: id1, id2, id3 all idle → sorted, cursor=2 → id3
1067        let c = pool.acquire(5).expect("test: third acquire should succeed");
1068        assert_eq!(c, id3);
1069        let _ = c;
1070    }
1071
1072    // -----------------------------------------------------------------------
1073    // 8. Health degradation
1074    // -----------------------------------------------------------------------
1075
1076    #[test]
1077    fn test_health_check_update() {
1078        let mut pool = make_pool();
1079        let id = pool
1080            .add_connection("a".to_string(), vec![])
1081            .expect("test: add_connection should succeed");
1082        pool.health_check(id, 0.75, 100)
1083            .expect("test: health_check should succeed");
1084        let conn = pool
1085            .connections()
1086            .get(&id)
1087            .expect("test: connection should exist");
1088        assert!((conn.health_score - 0.75).abs() < 1e-9);
1089    }
1090
1091    #[test]
1092    fn test_health_check_clamps_above_one() {
1093        let mut pool = make_pool();
1094        let id = pool
1095            .add_connection("a".to_string(), vec![])
1096            .expect("test: add_connection should succeed");
1097        pool.health_check(id, 1.5, 0)
1098            .expect("test: health_check with out-of-range score should succeed");
1099        assert!(
1100            (pool
1101                .connections()
1102                .get(&id)
1103                .expect("test: connection should exist")
1104                .health_score
1105                - 1.0)
1106                .abs()
1107                < 1e-9
1108        );
1109    }
1110
1111    #[test]
1112    fn test_health_check_clamps_below_zero() {
1113        let mut pool = make_pool();
1114        let id = pool
1115            .add_connection("a".to_string(), vec![])
1116            .expect("test: add_connection should succeed");
1117        pool.health_check(id, -0.5, 0)
1118            .expect("test: health_check with negative score should succeed");
1119        assert_eq!(
1120            pool.connections()
1121                .get(&id)
1122                .expect("test: connection should exist")
1123                .health_score,
1124            0.0
1125        );
1126    }
1127
1128    #[test]
1129    fn test_health_check_failed_event_below_threshold() {
1130        let mut pool = make_pool();
1131        let id = pool
1132            .add_connection("a".to_string(), vec![])
1133            .expect("test: add_connection should succeed");
1134        let ev = pool
1135            .health_check(id, 0.2, 0)
1136            .expect("test: health_check should succeed");
1137        assert!(matches!(ev, PoolEvent::HealthCheckFailed { id: i, .. } if i == id));
1138    }
1139
1140    #[test]
1141    fn test_health_check_no_failure_at_threshold() {
1142        let mut pool = make_pool();
1143        let id = pool
1144            .add_connection("a".to_string(), vec![])
1145            .expect("test: add_connection should succeed");
1146        // exactly 0.3 should NOT emit failure
1147        let ev = pool
1148            .health_check(id, 0.3, 0)
1149            .expect("test: health_check at threshold should succeed");
1150        assert!(!matches!(ev, PoolEvent::HealthCheckFailed { .. }));
1151    }
1152
1153    #[test]
1154    fn test_health_check_failure_increments_counter() {
1155        let mut pool = make_pool();
1156        let id = pool
1157            .add_connection("a".to_string(), vec![])
1158            .expect("test: add_connection should succeed");
1159        pool.health_check(id, 0.1, 0)
1160            .expect("test: health_check below threshold should succeed");
1161        assert_eq!(pool.stats().health_check_failures, 1);
1162    }
1163
1164    #[test]
1165    fn test_health_check_not_found_error() {
1166        let mut pool = make_pool();
1167        let err = pool.health_check(42, 0.5, 0).unwrap_err();
1168        assert!(matches!(err, CpmPoolError::ConnectionNotFound(42)));
1169    }
1170
1171    // -----------------------------------------------------------------------
1172    // 9. Maintenance — idle timeout
1173    // -----------------------------------------------------------------------
1174
1175    #[test]
1176    fn test_maintenance_idle_timeout() {
1177        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1178            idle_timeout_us: 1_000,
1179            max_lifetime_us: 999_999_999,
1180            ..default_config()
1181        });
1182        let id = pool
1183            .add_connection_at("a".to_string(), vec![], 0)
1184            .expect("test: add_connection_at should succeed");
1185        // last_used = 0; run maintenance at ts=2000 (> idle_timeout=1000)
1186        let events = pool.run_maintenance(2_000);
1187        assert!(!events.is_empty(), "should evict idle-timeout connection");
1188        assert!(events.iter().any(|e| matches!(
1189            e,
1190            PoolEvent::ConnectionRemoved { id: i, .. } if *i == id
1191        )));
1192        assert!(!pool.connections().contains_key(&id));
1193    }
1194
1195    #[test]
1196    fn test_maintenance_skips_in_use() {
1197        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1198            idle_timeout_us: 1_000,
1199            max_lifetime_us: 999_999_999,
1200            ..default_config()
1201        });
1202        let id = pool
1203            .add_connection_at("a".to_string(), vec![], 0)
1204            .expect("test: add_connection_at should succeed");
1205        pool.acquire(0).expect("test: acquire should succeed");
1206        let events = pool.run_maintenance(2_000);
1207        assert!(events.is_empty(), "InUse connections should not be evicted");
1208        assert!(pool.connections().contains_key(&id));
1209    }
1210
1211    // -----------------------------------------------------------------------
1212    // 10. Maintenance — lifetime timeout
1213    // -----------------------------------------------------------------------
1214
1215    #[test]
1216    fn test_maintenance_max_lifetime() {
1217        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1218            idle_timeout_us: 999_999_999,
1219            max_lifetime_us: 500,
1220            ..default_config()
1221        });
1222        let id = pool
1223            .add_connection_at("a".to_string(), vec![], 0)
1224            .expect("test: add_connection_at should succeed");
1225        // Connection created at 0; run at 600 → age 600 > max_lifetime 500
1226        let events = pool.run_maintenance(600);
1227        assert!(!events.is_empty());
1228        assert!(events.iter().any(|e| matches!(
1229            e,
1230            PoolEvent::ConnectionRemoved { id: i, .. } if *i == id
1231        )));
1232    }
1233
1234    #[test]
1235    fn test_maintenance_no_eviction_young_connection() {
1236        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1237            idle_timeout_us: 10_000,
1238            max_lifetime_us: 50_000,
1239            ..default_config()
1240        });
1241        pool.add_connection_at("a".to_string(), vec![], 1_000)
1242            .expect("test: add_connection_at should succeed");
1243        let events = pool.run_maintenance(2_000); // only 1000 µs old
1244        assert!(events.is_empty());
1245    }
1246
1247    #[test]
1248    fn test_maintenance_multiple_evictions() {
1249        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1250            idle_timeout_us: 100,
1251            max_lifetime_us: 999_999_999,
1252            ..default_config()
1253        });
1254        pool.add_connection_at("a".to_string(), vec![], 0)
1255            .expect("test: add_connection_at a should succeed");
1256        pool.add_connection_at("b".to_string(), vec![], 0)
1257            .expect("test: add_connection_at b should succeed");
1258        pool.add_connection_at("c".to_string(), vec![], 0)
1259            .expect("test: add_connection_at c should succeed");
1260        let events = pool.run_maintenance(200);
1261        assert_eq!(events.len(), 3);
1262    }
1263
1264    // -----------------------------------------------------------------------
1265    // 11. Resize
1266    // -----------------------------------------------------------------------
1267
1268    #[test]
1269    fn test_resize_grow() {
1270        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1271            max_connections: 3,
1272            ..default_config()
1273        });
1274        populate(&mut pool, 3);
1275        pool.resize(10).expect("test: resize grow should succeed");
1276        assert_eq!(pool.config().max_connections, 10);
1277        // Now we can add more.
1278        assert!(pool.add_connection("new".to_string(), vec![]).is_ok());
1279    }
1280
1281    #[test]
1282    fn test_resize_shrink_evicts_idle() {
1283        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1284            max_connections: 5,
1285            ..default_config()
1286        });
1287        populate(&mut pool, 5);
1288        pool.resize(3).expect("test: resize shrink should succeed");
1289        assert!(pool.connections().len() <= 3);
1290    }
1291
1292    #[test]
1293    fn test_resize_zero_fails() {
1294        let mut pool = make_pool();
1295        let err = pool.resize(0).unwrap_err();
1296        assert!(matches!(err, CpmPoolError::InvalidConfiguration(_)));
1297    }
1298
1299    #[test]
1300    fn test_resize_same_size_noop() {
1301        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1302            max_connections: 4,
1303            ..default_config()
1304        });
1305        populate(&mut pool, 4);
1306        pool.resize(4)
1307            .expect("test: resize same size should succeed");
1308        assert_eq!(pool.connections().len(), 4);
1309    }
1310
1311    // -----------------------------------------------------------------------
1312    // 12. Drain
1313    // -----------------------------------------------------------------------
1314
1315    #[test]
1316    fn test_drain_clears_all_connections() {
1317        let mut pool = make_pool();
1318        populate(&mut pool, 5);
1319        let ids = pool.drain();
1320        assert_eq!(ids.len(), 5);
1321        assert!(pool.connections().is_empty());
1322    }
1323
1324    #[test]
1325    fn test_drain_emits_pool_drained_event() {
1326        let mut pool = make_pool();
1327        populate(&mut pool, 2);
1328        pool.drain();
1329        let events = pool.drain_events();
1330        assert!(events.iter().any(|e| matches!(e, PoolEvent::PoolDrained)));
1331    }
1332
1333    #[test]
1334    fn test_drain_ids_sorted_ascending() {
1335        let mut pool = make_pool();
1336        let added = populate(&mut pool, 4);
1337        let drained = pool.drain();
1338        assert_eq!(drained, added);
1339    }
1340
1341    #[test]
1342    fn test_drain_empty_pool() {
1343        let mut pool = make_pool();
1344        let ids = pool.drain();
1345        assert!(ids.is_empty());
1346        let events = pool.drain_events();
1347        assert!(events.iter().any(|e| matches!(e, PoolEvent::PoolDrained)));
1348    }
1349
1350    // -----------------------------------------------------------------------
1351    // 13. Tag filtering
1352    // -----------------------------------------------------------------------
1353
1354    #[test]
1355    fn test_connections_with_tag_basic() {
1356        let mut pool = make_pool();
1357        pool.add_connection("a".to_string(), vec!["eu".to_string()])
1358            .expect("test: add_connection a with eu tag should succeed");
1359        pool.add_connection("b".to_string(), vec!["us".to_string()])
1360            .expect("test: add_connection b with us tag should succeed");
1361        pool.add_connection("c".to_string(), vec!["eu".to_string(), "tier1".to_string()])
1362            .expect("test: add_connection c with tags should succeed");
1363        let eu = pool.connections_with_tag("eu");
1364        assert_eq!(eu.len(), 2);
1365    }
1366
1367    #[test]
1368    fn test_connections_with_tag_no_match() {
1369        let mut pool = make_pool();
1370        pool.add_connection("a".to_string(), vec!["us".to_string()])
1371            .expect("test: add_connection should succeed");
1372        let result = pool.connections_with_tag("eu");
1373        assert!(result.is_empty());
1374    }
1375
1376    #[test]
1377    fn test_connections_with_tag_multiple_tags() {
1378        let mut pool = make_pool();
1379        let id = pool
1380            .add_connection(
1381                "a".to_string(),
1382                vec!["x".to_string(), "y".to_string(), "z".to_string()],
1383            )
1384            .expect("test: add_connection with multiple tags should succeed");
1385        assert_eq!(pool.connections_with_tag("x").len(), 1);
1386        assert_eq!(pool.connections_with_tag("y").len(), 1);
1387        assert_eq!(pool.connections_with_tag("z").len(), 1);
1388        let _ = id;
1389    }
1390
1391    // -----------------------------------------------------------------------
1392    // 14. Stats
1393    // -----------------------------------------------------------------------
1394
1395    #[test]
1396    fn test_stats_empty_pool() {
1397        let pool = make_pool();
1398        let s = pool.stats();
1399        assert_eq!(s.total_connections, 0);
1400        assert_eq!(s.idle_connections, 0);
1401        assert_eq!(s.in_use_connections, 0);
1402        assert!(s.avg_health_score.is_nan());
1403    }
1404
1405    #[test]
1406    fn test_stats_after_add() {
1407        let mut pool = make_pool();
1408        populate(&mut pool, 3);
1409        let s = pool.stats();
1410        assert_eq!(s.total_connections, 3);
1411        assert_eq!(s.idle_connections, 3);
1412        assert_eq!(s.in_use_connections, 0);
1413    }
1414
1415    #[test]
1416    fn test_stats_after_acquire() {
1417        let mut pool = make_pool();
1418        populate(&mut pool, 3);
1419        pool.acquire(1).expect("test: acquire should succeed");
1420        let s = pool.stats();
1421        assert_eq!(s.idle_connections, 2);
1422        assert_eq!(s.in_use_connections, 1);
1423        assert_eq!(s.total_acquires, 1);
1424    }
1425
1426    #[test]
1427    fn test_stats_avg_health() {
1428        let mut pool = make_pool();
1429        let id1 = pool
1430            .add_connection("a".to_string(), vec![])
1431            .expect("test: add_connection a should succeed");
1432        let id2 = pool
1433            .add_connection("b".to_string(), vec![])
1434            .expect("test: add_connection b should succeed");
1435        pool.health_check(id1, 0.6, 0)
1436            .expect("test: health_check id1 should succeed");
1437        pool.health_check(id2, 0.4, 0)
1438            .expect("test: health_check id2 should succeed");
1439        let s = pool.stats();
1440        assert!((s.avg_health_score - 0.5).abs() < 1e-9);
1441    }
1442
1443    #[test]
1444    fn test_stats_acquire_failures() {
1445        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1446            max_connections: 1,
1447            ..default_config()
1448        });
1449        let id = pool
1450            .add_connection("a".to_string(), vec![])
1451            .expect("test: add_connection should succeed");
1452        pool.acquire(1).expect("test: acquire should succeed");
1453        let _ = pool.acquire(2); // fails
1454        let s = pool.stats();
1455        assert_eq!(s.acquire_failures, 1);
1456        pool.release(id, 3, false)
1457            .expect("test: release should succeed");
1458    }
1459
1460    // -----------------------------------------------------------------------
1461    // 15. Error cases
1462    // -----------------------------------------------------------------------
1463
1464    #[test]
1465    fn test_remove_connection_ok() {
1466        let mut pool = make_pool();
1467        let id = pool
1468            .add_connection("a".to_string(), vec![])
1469            .expect("test: add_connection should succeed");
1470        pool.remove_connection(id)
1471            .expect("test: remove_connection should succeed");
1472        assert!(!pool.connections().contains_key(&id));
1473    }
1474
1475    #[test]
1476    fn test_remove_connection_not_found() {
1477        let mut pool = make_pool();
1478        let err = pool.remove_connection(42).unwrap_err();
1479        assert!(matches!(err, CpmPoolError::ConnectionNotFound(42)));
1480    }
1481
1482    #[test]
1483    fn test_acquire_pool_empty_timeout() {
1484        let mut pool = make_pool();
1485        // Pool has capacity but nothing idle.
1486        let err = pool.acquire(100).unwrap_err();
1487        assert!(matches!(err, CpmPoolError::AcquireTimeout(_)));
1488    }
1489
1490    #[test]
1491    fn test_invalid_config_zero_max() {
1492        let result = ConnectionPoolManager::validate_config(&CpmPoolConfig {
1493            max_connections: 0,
1494            ..default_config()
1495        });
1496        assert!(result.is_err());
1497    }
1498
1499    #[test]
1500    fn test_invalid_config_min_gt_max() {
1501        let result = ConnectionPoolManager::validate_config(&CpmPoolConfig {
1502            min_connections: 5,
1503            max_connections: 3,
1504            ..default_config()
1505        });
1506        assert!(result.is_err());
1507    }
1508
1509    // -----------------------------------------------------------------------
1510    // 16. drain_events
1511    // -----------------------------------------------------------------------
1512
1513    #[test]
1514    fn test_drain_events_clears_queue() {
1515        let mut pool = make_pool();
1516        pool.add_connection("a".to_string(), vec![])
1517            .expect("test: add_connection should succeed");
1518        let ev1 = pool.drain_events();
1519        assert!(!ev1.is_empty());
1520        let ev2 = pool.drain_events();
1521        assert!(ev2.is_empty());
1522    }
1523
1524    #[test]
1525    fn test_drain_events_removed_event() {
1526        let mut pool = make_pool();
1527        let id = pool
1528            .add_connection("a".to_string(), vec![])
1529            .expect("test: add_connection should succeed");
1530        pool.drain_events(); // clear add event
1531        pool.remove_connection(id)
1532            .expect("test: remove_connection should succeed");
1533        let events = pool.drain_events();
1534        assert!(events
1535            .iter()
1536            .any(|e| matches!(e, PoolEvent::ConnectionRemoved { id: i, .. } if *i == id)));
1537    }
1538
1539    // -----------------------------------------------------------------------
1540    // 17. xorshift64 PRNG
1541    // -----------------------------------------------------------------------
1542
1543    #[test]
1544    fn test_xorshift64_non_zero() {
1545        let mut state = 12345u64;
1546        let v = xorshift64(&mut state);
1547        assert_ne!(v, 0);
1548    }
1549
1550    #[test]
1551    fn test_xorshift64_sequence_distinct() {
1552        let mut state = 1u64;
1553        let a = xorshift64(&mut state);
1554        let b = xorshift64(&mut state);
1555        assert_ne!(a, b);
1556    }
1557
1558    #[test]
1559    fn test_xorshift64_deterministic() {
1560        let mut s1 = 42u64;
1561        let mut s2 = 42u64;
1562        for _ in 0..100 {
1563            assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1564        }
1565    }
1566
1567    // -----------------------------------------------------------------------
1568    // 18. ConnState PartialEq
1569    // -----------------------------------------------------------------------
1570
1571    #[test]
1572    fn test_connstate_eq() {
1573        assert_eq!(ConnState::Idle, ConnState::Idle);
1574        assert_ne!(ConnState::Idle, ConnState::Closed);
1575        assert_eq!(
1576            ConnState::InUse { borrowed_at: 10 },
1577            ConnState::InUse { borrowed_at: 10 }
1578        );
1579        assert_ne!(
1580            ConnState::InUse { borrowed_at: 10 },
1581            ConnState::InUse { borrowed_at: 20 }
1582        );
1583    }
1584
1585    // -----------------------------------------------------------------------
1586    // 19. Integration: add → acquire → release → health_check → maintenance
1587    // -----------------------------------------------------------------------
1588
1589    #[test]
1590    fn test_full_lifecycle() {
1591        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1592            max_connections: 3,
1593            idle_timeout_us: 10_000,
1594            max_lifetime_us: 100_000,
1595            ..default_config()
1596        });
1597        let id1 = pool
1598            .add_connection_at("a".to_string(), vec![], 0)
1599            .expect("test: add_connection_at a should succeed");
1600        let id2 = pool
1601            .add_connection_at("b".to_string(), vec![], 0)
1602            .expect("test: add_connection_at b should succeed");
1603
1604        // Acquire both.
1605        let a1 = pool
1606            .acquire(1_000)
1607            .expect("test: first acquire should succeed");
1608        let a2 = pool
1609            .acquire(1_000)
1610            .expect("test: second acquire should succeed");
1611
1612        // Release id1 with degraded health.
1613        pool.release(a1, 2_000, true)
1614            .expect("test: release a1 with health degraded should succeed");
1615        // Release id2 cleanly.
1616        pool.release(a2, 2_000, false)
1617            .expect("test: release a2 should succeed");
1618
1619        // Health check: put id1 in HealthChecking, get failure event.
1620        pool.connections
1621            .get_mut(&id1)
1622            .expect("test: connection id1 should exist")
1623            .state = ConnState::HealthChecking;
1624        pool.health_check(id1, 0.1, 3_000)
1625            .expect("test: health_check should succeed");
1626
1627        // Maintenance at 15000: idle_timeout = 10000, so both connections (last_used=2000)
1628        // should be evicted.
1629        let evicted = pool.run_maintenance(15_000);
1630        assert!(!evicted.is_empty());
1631        let _ = (id1, id2);
1632    }
1633
1634    // -----------------------------------------------------------------------
1635    // 20. resize + stats interaction
1636    // -----------------------------------------------------------------------
1637
1638    #[test]
1639    fn test_resize_shrink_stats_updated() {
1640        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1641            max_connections: 5,
1642            ..default_config()
1643        });
1644        populate(&mut pool, 5);
1645        pool.resize(2).expect("test: resize shrink should succeed");
1646        let s = pool.stats();
1647        assert!(s.total_connections <= 2);
1648    }
1649
1650    // -----------------------------------------------------------------------
1651    // 21. Multiple acquire-release cycles (stress)
1652    // -----------------------------------------------------------------------
1653
1654    #[test]
1655    fn test_multiple_acquire_release_cycles() {
1656        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1657            max_connections: 3,
1658            ..default_config()
1659        });
1660        let ids = populate(&mut pool, 3);
1661        for cycle in 0..10_u64 {
1662            let ts_base = cycle * 1000;
1663            let acquired: Vec<u64> = ids
1664                .iter()
1665                .map(|_| {
1666                    pool.acquire(ts_base)
1667                        .expect("test: acquire in cycle should succeed")
1668                })
1669                .collect();
1670            for &id in &acquired {
1671                pool.release(id, ts_base + 1, false)
1672                    .expect("test: release in cycle should succeed");
1673            }
1674        }
1675        let s = pool.stats();
1676        assert_eq!(s.total_acquires, 30);
1677        assert_eq!(s.total_releases, 30);
1678        assert_eq!(s.idle_connections, 3);
1679    }
1680
1681    // -----------------------------------------------------------------------
1682    // 22. HealthCheckFailed event in pending queue
1683    // -----------------------------------------------------------------------
1684
1685    #[test]
1686    fn test_health_check_failure_in_pending_events() {
1687        let mut pool = make_pool();
1688        let id = pool
1689            .add_connection("a".to_string(), vec![])
1690            .expect("test: add_connection should succeed");
1691        pool.drain_events();
1692        pool.health_check(id, 0.05, 0)
1693            .expect("test: health_check below threshold should succeed");
1694        let events = pool.drain_events();
1695        assert!(events
1696            .iter()
1697            .any(|e| matches!(e, PoolEvent::HealthCheckFailed { .. })));
1698    }
1699
1700    // -----------------------------------------------------------------------
1701    // 23. Pool exhausted counter
1702    // -----------------------------------------------------------------------
1703
1704    #[test]
1705    fn test_pool_exhausted_increments_failure_counter() {
1706        let mut pool = ConnectionPoolManager::new(CpmPoolConfig {
1707            max_connections: 1,
1708            ..default_config()
1709        });
1710        let id = pool
1711            .add_connection("a".to_string(), vec![])
1712            .expect("test: add_connection should succeed");
1713        pool.acquire(1).expect("test: acquire should succeed");
1714        let _ = pool.acquire(2); // pool exhausted
1715        assert_eq!(pool.stats().acquire_failures, 1);
1716        pool.release(id, 3, false)
1717            .expect("test: release should succeed");
1718    }
1719
1720    // -----------------------------------------------------------------------
1721    // 24. Ensure released connections are acquirable again
1722    // -----------------------------------------------------------------------
1723
1724    #[test]
1725    fn test_released_connection_reacquirable() {
1726        let mut pool = make_pool();
1727        let id = pool
1728            .add_connection("a".to_string(), vec![])
1729            .expect("test: add_connection should succeed");
1730        pool.acquire(1).expect("test: acquire should succeed");
1731        pool.release(id, 2, false)
1732            .expect("test: release should succeed");
1733        let reacquired = pool.acquire(3).expect("test: reacquire should succeed");
1734        assert_eq!(reacquired, id);
1735    }
1736
1737    // -----------------------------------------------------------------------
1738    // 25. add_connection_at populates timestamps correctly
1739    // -----------------------------------------------------------------------
1740
1741    #[test]
1742    fn test_add_connection_at_timestamps() {
1743        let mut pool = make_pool();
1744        let id = pool
1745            .add_connection_at("a".to_string(), vec![], 9_999)
1746            .expect("test: add_connection_at should succeed");
1747        let conn = pool
1748            .connections()
1749            .get(&id)
1750            .expect("test: connection should exist");
1751        assert_eq!(conn.created_at, 9_999);
1752        assert_eq!(conn.last_used, 9_999);
1753    }
1754}