Skip to main content

sqlmodel_pool/
lib.rs

1//! Connection pooling for SQLModel Rust using asupersync.
2//!
3//! `sqlmodel-pool` is the **connection lifecycle layer**. It provides a generic,
4//! budget-aware pool that integrates with structured concurrency and can wrap any
5//! `Connection` implementation.
6//!
7//! # Role In The Architecture
8//!
9//! - **Shared connection management**: reuse connections across tasks safely.
10//! - **Budget-aware acquisition**: respects `Cx` timeouts and cancellation.
11//! - **Health checks**: validates connections before handing them out.
12//! - **Metrics**: exposes stats for pool sizing and tuning.
13//!
14//! # Features
15//!
16//! - Generic over any `Connection` type
17//! - RAII-based connection return (connections returned on drop)
18//! - Timeout support via `Cx` context
19//! - Connection health validation
20//! - Idle and max lifetime tracking
21//! - Pool statistics
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use sqlmodel_pool::{Pool, PoolConfig};
27//!
28//! // Create a pool
29//! let config = PoolConfig::new(10)
30//!     .min_connections(2)
31//!     .acquire_timeout(5000);
32//!
33//! let pool = Pool::new(config, || async {
34//!     // Factory function to create new connections
35//!     PgConnection::connect(&cx, &pg_config).await
36//! });
37//!
38//! // Acquire a connection
39//! let conn = pool.acquire(&cx).await?;
40//!
41//! // Use the connection (automatically returned to pool on drop)
42//! conn.query(&cx, "SELECT 1", &[]).await?;
43//! ```
44
45pub mod replica;
46pub use replica::{ReplicaPool, ReplicaStrategy};
47
48pub mod sharding;
49pub use sharding::{ModuloShardChooser, QueryHints, ShardChooser, ShardedPool, ShardedPoolStats};
50
51use std::collections::VecDeque;
52use std::future::Future;
53use std::sync::atomic::{AtomicU64, Ordering};
54use std::sync::{Arc, Condvar, Mutex, Weak};
55use std::time::{Duration, Instant};
56
57use asupersync::{CancelReason, Cx, Outcome, runtime::RuntimeBuilder};
58use sqlmodel_core::error::{ConnectionError, ConnectionErrorKind, PoolError, PoolErrorKind};
59use sqlmodel_core::{Connection, Error};
60
61/// Connection pool configuration.
62#[derive(Debug, Clone)]
63pub struct PoolConfig {
64    /// Minimum number of connections to maintain
65    pub min_connections: usize,
66    /// Maximum number of connections allowed
67    pub max_connections: usize,
68    /// Connection idle timeout in milliseconds
69    pub idle_timeout_ms: u64,
70    /// Maximum time to wait for a connection in milliseconds
71    pub acquire_timeout_ms: u64,
72    /// Maximum lifetime of a connection in milliseconds
73    pub max_lifetime_ms: u64,
74    /// Test connections before giving them out
75    pub test_on_checkout: bool,
76    /// Test connections when returning them to the pool
77    pub test_on_return: bool,
78}
79
80impl Default for PoolConfig {
81    fn default() -> Self {
82        Self {
83            min_connections: 1,
84            max_connections: 10,
85            idle_timeout_ms: 600_000,   // 10 minutes
86            acquire_timeout_ms: 30_000, // 30 seconds
87            max_lifetime_ms: 1_800_000, // 30 minutes
88            test_on_checkout: true,
89            test_on_return: false,
90        }
91    }
92}
93
94impl PoolConfig {
95    /// Create a new pool configuration with the given max connections.
96    #[must_use]
97    pub fn new(max_connections: usize) -> Self {
98        Self {
99            max_connections,
100            ..Default::default()
101        }
102    }
103
104    /// Set minimum connections.
105    #[must_use]
106    pub fn min_connections(mut self, n: usize) -> Self {
107        self.min_connections = n;
108        self
109    }
110
111    /// Set idle timeout in milliseconds.
112    #[must_use]
113    pub fn idle_timeout(mut self, ms: u64) -> Self {
114        self.idle_timeout_ms = ms;
115        self
116    }
117
118    /// Set acquire timeout in milliseconds.
119    #[must_use]
120    pub fn acquire_timeout(mut self, ms: u64) -> Self {
121        self.acquire_timeout_ms = ms;
122        self
123    }
124
125    /// Set max lifetime in milliseconds.
126    #[must_use]
127    pub fn max_lifetime(mut self, ms: u64) -> Self {
128        self.max_lifetime_ms = ms;
129        self
130    }
131
132    /// Enable/disable test on checkout.
133    #[must_use]
134    pub fn test_on_checkout(mut self, enabled: bool) -> Self {
135        self.test_on_checkout = enabled;
136        self
137    }
138
139    /// Enable/disable test on return.
140    #[must_use]
141    pub fn test_on_return(mut self, enabled: bool) -> Self {
142        self.test_on_return = enabled;
143        self
144    }
145}
146
147/// Pool statistics.
148#[derive(Debug, Clone, Default)]
149pub struct PoolStats {
150    /// Total number of connections (active + idle)
151    pub total_connections: usize,
152    /// Number of idle connections
153    pub idle_connections: usize,
154    /// Number of active connections (currently in use)
155    pub active_connections: usize,
156    /// Number of pending acquire requests
157    pub pending_requests: usize,
158    /// Total number of connections created
159    pub connections_created: u64,
160    /// Total number of connections closed
161    pub connections_closed: u64,
162    /// Total number of successful acquires
163    pub acquires: u64,
164    /// Total number of acquire timeouts
165    pub timeouts: u64,
166}
167
168/// Metadata about a pooled connection.
169#[derive(Debug)]
170struct ConnectionMeta<C> {
171    /// The actual connection
172    conn: C,
173    /// When this connection was created
174    created_at: Instant,
175    /// When this connection was last used
176    last_used: Instant,
177}
178
179impl<C> ConnectionMeta<C> {
180    fn new(conn: C) -> Self {
181        let now = Instant::now();
182        Self {
183            conn,
184            created_at: now,
185            last_used: now,
186        }
187    }
188
189    fn touch(&mut self) {
190        self.last_used = Instant::now();
191    }
192
193    fn age(&self) -> Duration {
194        self.created_at.elapsed()
195    }
196
197    fn idle_time(&self) -> Duration {
198        self.last_used.elapsed()
199    }
200}
201
202/// Internal pool state shared between pool and connections.
203struct PoolInner<C: Connection> {
204    /// Pool configuration
205    config: PoolConfig,
206    /// Idle connections available for use
207    idle: VecDeque<ConnectionMeta<C>>,
208    /// Number of connections currently checked out
209    active_count: usize,
210    /// Total number of connections (idle + active)
211    total_count: usize,
212    /// Number of waiters in the queue
213    waiter_count: usize,
214    /// Whether the pool has been closed
215    closed: bool,
216}
217
218impl<C: Connection> PoolInner<C> {
219    fn new(config: PoolConfig) -> Self {
220        Self {
221            config,
222            idle: VecDeque::new(),
223            active_count: 0,
224            total_count: 0,
225            waiter_count: 0,
226            closed: false,
227        }
228    }
229
230    fn can_create_new(&self) -> bool {
231        !self.closed && self.total_count < self.config.max_connections
232    }
233
234    fn stats(&self) -> PoolStats {
235        PoolStats {
236            total_connections: self.total_count,
237            idle_connections: self.idle.len(),
238            active_connections: self.active_count,
239            pending_requests: self.waiter_count,
240            ..Default::default()
241        }
242    }
243}
244
245/// Shared state wrapper with condition variable for notification.
246struct PoolShared<C: Connection> {
247    /// Protected pool state
248    inner: Mutex<PoolInner<C>>,
249    /// Notifies waiters when connections become available
250    conn_available: Condvar,
251    /// Statistics counters (atomic for lock-free reads)
252    connections_created: AtomicU64,
253    connections_closed: AtomicU64,
254    acquires: AtomicU64,
255    timeouts: AtomicU64,
256}
257
258impl<C: Connection> PoolShared<C> {
259    fn new(config: PoolConfig) -> Self {
260        Self {
261            inner: Mutex::new(PoolInner::new(config)),
262            conn_available: Condvar::new(),
263            connections_created: AtomicU64::new(0),
264            connections_closed: AtomicU64::new(0),
265            acquires: AtomicU64::new(0),
266            timeouts: AtomicU64::new(0),
267        }
268    }
269
270    /// Lock the inner mutex, recovering from poisoning for read-only access.
271    ///
272    /// A poisoned mutex occurs when a thread panicked while holding the lock.
273    /// The data inside is still valid for reading, so we recover by logging
274    /// and using `into_inner()` to get the guard.
275    ///
276    /// This should only be used for read-only operations where the data is
277    /// always valid regardless of whether a previous operation completed.
278    fn lock_or_recover(&self) -> std::sync::MutexGuard<'_, PoolInner<C>> {
279        self.inner.lock().unwrap_or_else(|poisoned| {
280            tracing::error!(
281                "Pool mutex poisoned; recovering for read-only access. \
282                 A thread panicked while holding the lock."
283            );
284            poisoned.into_inner()
285        })
286    }
287
288    /// Lock the inner mutex, returning an error if poisoned.
289    ///
290    /// Use this for mutation operations where the pool state may be inconsistent
291    /// after a panic. Unlike `lock_or_recover()`, this propagates the error
292    /// to the caller.
293    #[allow(clippy::result_large_err)] // Error type is large by design for rich diagnostics
294    fn lock_or_error(
295        &self,
296        operation: &'static str,
297    ) -> Result<std::sync::MutexGuard<'_, PoolInner<C>>, Error> {
298        self.inner
299            .lock()
300            .map_err(|_| Error::Pool(PoolError::poisoned(operation)))
301    }
302}
303
304fn close_connection_blocking<C: Connection>(conn: C, context: &'static str) {
305    let runtime = match RuntimeBuilder::current_thread().build() {
306        Ok(runtime) => runtime,
307        Err(error) => {
308            tracing::warn!(
309                context,
310                error = %error,
311                "failed to build runtime while closing pooled connection"
312            );
313            drop(conn);
314            return;
315        }
316    };
317    let cx = Cx::for_testing();
318    if let Err(error) = runtime.block_on(async { conn.close_for_pool(&cx).await }) {
319        tracing::warn!(
320            context,
321            error = %error,
322            "failed to close pooled connection explicitly"
323        );
324    }
325}
326
327fn close_connection_metas<C: Connection>(
328    metas: impl IntoIterator<Item = ConnectionMeta<C>>,
329    context: &'static str,
330) {
331    for meta in metas {
332        close_connection_blocking(meta.conn, context);
333    }
334}
335
336/// A connection pool for database connections.
337///
338/// The pool manages a collection of connections, reusing them across
339/// requests to avoid the overhead of establishing new connections.
340///
341/// # Type Parameters
342///
343/// - `C`: The connection type, must implement `Connection`
344///
345/// # Cancellation
346///
347/// Pool operations respect cancellation via the `Cx` context:
348/// - `acquire` will return early if cancellation is requested
349/// - Connections are properly cleaned up on cancellation
350pub struct Pool<C: Connection> {
351    shared: Arc<PoolShared<C>>,
352}
353
354impl<C: Connection> Pool<C> {
355    /// Create a new connection pool with the given configuration.
356    #[must_use]
357    pub fn new(config: PoolConfig) -> Self {
358        Self {
359            shared: Arc::new(PoolShared::new(config)),
360        }
361    }
362
363    /// Get the pool configuration.
364    #[must_use]
365    pub fn config(&self) -> PoolConfig {
366        let inner = self.shared.lock_or_recover();
367        inner.config.clone()
368    }
369
370    /// Get the current pool statistics.
371    #[must_use]
372    pub fn stats(&self) -> PoolStats {
373        let inner = self.shared.lock_or_recover();
374        let mut stats = inner.stats();
375        stats.connections_created = self.shared.connections_created.load(Ordering::Relaxed);
376        stats.connections_closed = self.shared.connections_closed.load(Ordering::Relaxed);
377        stats.acquires = self.shared.acquires.load(Ordering::Relaxed);
378        stats.timeouts = self.shared.timeouts.load(Ordering::Relaxed);
379        stats
380    }
381
382    /// Check if the pool is at capacity.
383    #[must_use]
384    pub fn at_capacity(&self) -> bool {
385        let inner = self.shared.lock_or_recover();
386        inner.total_count >= inner.config.max_connections
387    }
388
389    /// Check if the pool has been closed.
390    #[must_use]
391    pub fn is_closed(&self) -> bool {
392        let inner = self.shared.lock_or_recover();
393        inner.closed
394    }
395
396    /// Acquire a connection from the pool.
397    ///
398    /// This method will:
399    /// 1. Return an idle connection if one is available
400    /// 2. Create a new connection if below capacity
401    /// 3. Wait for a connection to become available (up to timeout)
402    ///
403    /// # Errors
404    ///
405    /// Returns an error if:
406    /// - The pool is closed
407    /// - The acquire timeout is exceeded
408    /// - Cancellation is requested via the `Cx` context
409    /// - Connection validation fails (if `test_on_checkout` is enabled)
410    pub async fn acquire<F, Fut>(&self, cx: &Cx, factory: F) -> Outcome<PooledConnection<C>, Error>
411    where
412        F: Fn() -> Fut,
413        Fut: Future<Output = Outcome<C, Error>>,
414    {
415        let deadline = Instant::now() + Duration::from_millis(self.config().acquire_timeout_ms);
416        let test_on_checkout = self.config().test_on_checkout;
417        let max_lifetime = Duration::from_millis(self.config().max_lifetime_ms);
418        let idle_timeout = Duration::from_millis(self.config().idle_timeout_ms);
419
420        loop {
421            // Check cancellation
422            if cx.is_cancel_requested() {
423                return Outcome::Cancelled(CancelReason::user("pool acquire cancelled"));
424            }
425
426            // Check timeout
427            if Instant::now() >= deadline {
428                self.shared.timeouts.fetch_add(1, Ordering::Relaxed);
429                return Outcome::Err(Error::Pool(PoolError {
430                    kind: PoolErrorKind::Timeout,
431                    message: "acquire timeout: no connections available".to_string(),
432                    source: None,
433                }));
434            }
435
436            // Try to get an idle connection or determine if we can create new
437            let (action, retired) = {
438                let mut inner = match self.shared.lock_or_error("acquire") {
439                    Ok(guard) => guard,
440                    Err(e) => return Outcome::Err(e),
441                };
442                let mut retired = Vec::new();
443
444                let action = if inner.closed {
445                    AcquireAction::PoolClosed
446                } else {
447                    // Try to get an idle connection
448                    let mut found_conn = None;
449                    while let Some(mut meta) = inner.idle.pop_front() {
450                        // Check if connection is too old
451                        if meta.age() > max_lifetime {
452                            inner.total_count -= 1;
453                            self.shared
454                                .connections_closed
455                                .fetch_add(1, Ordering::Relaxed);
456                            retired.push(meta);
457                            continue;
458                        }
459
460                        // Check if connection has been idle too long
461                        if meta.idle_time() > idle_timeout {
462                            inner.total_count -= 1;
463                            self.shared
464                                .connections_closed
465                                .fetch_add(1, Ordering::Relaxed);
466                            retired.push(meta);
467                            continue;
468                        }
469
470                        // Found a valid connection
471                        meta.touch();
472                        inner.active_count += 1;
473                        found_conn = Some(meta);
474                        break;
475                    }
476
477                    if let Some(meta) = found_conn {
478                        AcquireAction::ValidateExisting(meta)
479                    } else if inner.can_create_new() {
480                        // No idle connections, can we create new?
481                        inner.total_count += 1;
482                        inner.active_count += 1;
483                        AcquireAction::CreateNew
484                    } else {
485                        // Must wait
486                        inner.waiter_count += 1;
487                        AcquireAction::Wait
488                    }
489                };
490                (action, retired)
491            };
492
493            // Teardown may perform driver I/O. Keep it outside the pool mutex
494            // so one slow close cannot block returns, acquires, or shutdown.
495            for meta in retired {
496                if let Err(error) = meta.conn.close_for_pool(cx).await {
497                    tracing::warn!(
498                        error = %error,
499                        "failed to close expired pooled connection"
500                    );
501                }
502            }
503
504            match action {
505                AcquireAction::PoolClosed => {
506                    return Outcome::Err(Error::Pool(PoolError {
507                        kind: PoolErrorKind::Closed,
508                        message: "pool has been closed".to_string(),
509                        source: None,
510                    }));
511                }
512                AcquireAction::ValidateExisting(meta) => {
513                    // Validate and wrap the connection (lock is released)
514                    return self.validate_and_wrap(cx, meta, test_on_checkout).await;
515                }
516                AcquireAction::CreateNew => {
517                    // Create new connection outside of lock
518                    match factory().await {
519                        Outcome::Ok(conn) => {
520                            self.shared
521                                .connections_created
522                                .fetch_add(1, Ordering::Relaxed);
523                            self.shared.acquires.fetch_add(1, Ordering::Relaxed);
524                            let meta = ConnectionMeta::new(conn);
525                            return Outcome::Ok(PooledConnection::new(
526                                meta,
527                                Arc::downgrade(&self.shared),
528                            ));
529                        }
530                        Outcome::Err(e) => {
531                            // Failed to create, decrement counts
532                            if let Ok(mut inner) = self.shared.lock_or_error("acquire_cleanup") {
533                                inner.total_count -= 1;
534                                inner.active_count -= 1;
535                            }
536                            // Even if we can't decrement counts, still return the original error
537                            return Outcome::Err(e);
538                        }
539                        Outcome::Cancelled(reason) => {
540                            if let Ok(mut inner) = self.shared.lock_or_error("acquire_cleanup") {
541                                inner.total_count -= 1;
542                                inner.active_count -= 1;
543                            }
544                            return Outcome::Cancelled(reason);
545                        }
546                        Outcome::Panicked(info) => {
547                            if let Ok(mut inner) = self.shared.lock_or_error("acquire_cleanup") {
548                                inner.total_count -= 1;
549                                inner.active_count -= 1;
550                            }
551                            return Outcome::Panicked(info);
552                        }
553                    }
554                }
555                AcquireAction::Wait => {
556                    // Wait for a connection to become available
557                    let remaining = deadline.saturating_duration_since(Instant::now());
558                    if remaining.is_zero() {
559                        if let Ok(mut inner) = self.shared.lock_or_error("acquire_timeout") {
560                            inner.waiter_count -= 1;
561                        }
562                        self.shared.timeouts.fetch_add(1, Ordering::Relaxed);
563                        return Outcome::Err(Error::Pool(PoolError {
564                            kind: PoolErrorKind::Timeout,
565                            message: "acquire timeout: no connections available".to_string(),
566                            source: None,
567                        }));
568                    }
569
570                    // Wait with timeout (use shorter interval for cancellation checks)
571                    let wait_time = remaining.min(Duration::from_millis(100));
572                    {
573                        let inner = match self.shared.lock_or_error("acquire_wait") {
574                            Ok(guard) => guard,
575                            Err(e) => return Outcome::Err(e),
576                        };
577                        // wait_timeout can also return a poisoned error, handle it
578                        let _ = self
579                            .shared
580                            .conn_available
581                            .wait_timeout(inner, wait_time)
582                            .map_err(|_| {
583                                tracing::error!("Pool mutex poisoned during wait_timeout");
584                            });
585                    }
586
587                    // Decrement waiter count after waking
588                    {
589                        if let Ok(mut inner) = self.shared.lock_or_error("acquire_wake") {
590                            inner.waiter_count = inner.waiter_count.saturating_sub(1);
591                        }
592                    }
593
594                    // Loop back to try again
595                }
596            }
597        }
598    }
599
600    /// Validate a connection and wrap it in a PooledConnection.
601    async fn validate_and_wrap(
602        &self,
603        cx: &Cx,
604        meta: ConnectionMeta<C>,
605        test_on_checkout: bool,
606    ) -> Outcome<PooledConnection<C>, Error> {
607        if test_on_checkout {
608            // Validate the connection
609            match meta.conn.ping(cx).await {
610                Outcome::Ok(()) => {
611                    self.shared.acquires.fetch_add(1, Ordering::Relaxed);
612                    Outcome::Ok(PooledConnection::new(meta, Arc::downgrade(&self.shared)))
613                }
614                Outcome::Err(_) | Outcome::Cancelled(_) | Outcome::Panicked(_) => {
615                    // Connection is invalid, decrement counts and try again
616                    {
617                        if let Ok(mut inner) = self.shared.lock_or_error("validate_cleanup") {
618                            inner.total_count -= 1;
619                            inner.active_count -= 1;
620                        }
621                    }
622                    self.shared
623                        .connections_closed
624                        .fetch_add(1, Ordering::Relaxed);
625                    if let Err(error) = meta.conn.close_for_pool(cx).await {
626                        tracing::warn!(
627                            error = %error,
628                            "failed to close pooled connection after checkout validation"
629                        );
630                    }
631                    // Return error - caller should retry
632                    Outcome::Err(Error::Connection(ConnectionError {
633                        kind: ConnectionErrorKind::Disconnected,
634                        message: "connection validation failed".to_string(),
635                        source: None,
636                    }))
637                }
638            }
639        } else {
640            self.shared.acquires.fetch_add(1, Ordering::Relaxed);
641            Outcome::Ok(PooledConnection::new(meta, Arc::downgrade(&self.shared)))
642        }
643    }
644
645    /// Close the pool, preventing new connections and closing all idle connections.
646    ///
647    /// If the pool mutex is poisoned, this logs an error but still wakes waiters.
648    pub fn clear_idle(&self) {
649        if let Ok(mut inner) = self.shared.inner.lock() {
650            let idle = inner.idle.drain(..).collect::<Vec<_>>();
651            let idle_count = idle.len();
652            inner.total_count -= idle_count;
653            self.shared
654                .connections_closed
655                .fetch_add(idle_count as u64, Ordering::Relaxed);
656            drop(inner);
657            close_connection_metas(idle, "pool clear_idle");
658        }
659    }
660
661    pub fn close(&self) {
662        match self.shared.inner.lock() {
663            Ok(mut inner) => {
664                inner.closed = true;
665
666                // Close all idle connections
667                let idle = inner.idle.drain(..).collect::<Vec<_>>();
668                let idle_count = idle.len();
669                inner.total_count -= idle_count;
670                self.shared
671                    .connections_closed
672                    .fetch_add(idle_count as u64, Ordering::Relaxed);
673                drop(inner);
674                close_connection_metas(idle, "pool close");
675            }
676            Err(poisoned) => {
677                // Recover from poisoning - we still want to mark the pool as closed
678                // and wake waiters even if counts may be inconsistent.
679                tracing::error!(
680                    "Pool mutex poisoned during close; attempting recovery. \
681                     Pool state may be inconsistent."
682                );
683                let mut inner = poisoned.into_inner();
684                inner.closed = true;
685                let idle = inner.idle.drain(..).collect::<Vec<_>>();
686                let idle_count = idle.len();
687                inner.total_count -= idle_count;
688                self.shared
689                    .connections_closed
690                    .fetch_add(idle_count as u64, Ordering::Relaxed);
691                drop(inner);
692                close_connection_metas(idle, "pool close poisoned");
693            }
694        }
695
696        // Wake all waiters so they see the pool is closed
697        self.shared.conn_available.notify_all();
698    }
699
700    /// Get the number of idle connections.
701    #[must_use]
702    pub fn idle_count(&self) -> usize {
703        let inner = self.shared.lock_or_recover();
704        inner.idle.len()
705    }
706
707    /// Get the number of active connections.
708    #[must_use]
709    pub fn active_count(&self) -> usize {
710        let inner = self.shared.lock_or_recover();
711        inner.active_count
712    }
713
714    /// Get the total number of connections.
715    #[must_use]
716    pub fn total_count(&self) -> usize {
717        let inner = self.shared.lock_or_recover();
718        inner.total_count
719    }
720}
721
722impl<C: Connection> Drop for Pool<C> {
723    fn drop(&mut self) {
724        self.close();
725    }
726}
727
728/// Action to take when acquiring a connection.
729enum AcquireAction<C> {
730    /// Pool is closed
731    PoolClosed,
732    /// Found an existing connection to validate
733    ValidateExisting(ConnectionMeta<C>),
734    /// Create a new connection
735    CreateNew,
736    /// Wait for a connection to become available
737    Wait,
738}
739
740/// A connection borrowed from the pool.
741///
742/// When dropped, the connection is automatically returned to the pool.
743/// The connection can be used via `Deref` and `DerefMut`.
744pub struct PooledConnection<C: Connection> {
745    /// The connection metadata (Some while held, None after return)
746    meta: Option<ConnectionMeta<C>>,
747    /// Weak reference to pool for returning
748    pool: Weak<PoolShared<C>>,
749}
750
751impl<C: Connection> PooledConnection<C> {
752    fn new(meta: ConnectionMeta<C>, pool: Weak<PoolShared<C>>) -> Self {
753        Self {
754            meta: Some(meta),
755            pool,
756        }
757    }
758
759    /// Detach this connection from the pool.
760    ///
761    /// The connection will not be returned to the pool when dropped.
762    /// This is useful when you need to close a connection explicitly.
763    pub fn detach(mut self) -> C {
764        if let Some(pool) = self.pool.upgrade() {
765            // Try to update pool counters, but don't panic if mutex is poisoned.
766            // The connection is being detached anyway, so counts being off is acceptable.
767            match pool.inner.lock() {
768                Ok(mut inner) => {
769                    inner.total_count -= 1;
770                    inner.active_count -= 1;
771                    pool.connections_closed.fetch_add(1, Ordering::Relaxed);
772                }
773                Err(_poisoned) => {
774                    tracing::error!(
775                        "Pool mutex poisoned during detach; pool counters will be inconsistent"
776                    );
777                    // Still increment the atomic counter for tracking
778                    pool.connections_closed.fetch_add(1, Ordering::Relaxed);
779                }
780            }
781        }
782        self.meta.take().expect("connection already detached").conn
783    }
784
785    /// Get the age of this connection (time since creation).
786    #[must_use]
787    pub fn age(&self) -> Duration {
788        self.meta.as_ref().map_or(Duration::ZERO, |m| m.age())
789    }
790
791    /// Get the idle time of this connection (time since last use).
792    #[must_use]
793    pub fn idle_time(&self) -> Duration {
794        self.meta.as_ref().map_or(Duration::ZERO, |m| m.idle_time())
795    }
796}
797
798impl<C: Connection> std::ops::Deref for PooledConnection<C> {
799    type Target = C;
800
801    fn deref(&self) -> &Self::Target {
802        &self
803            .meta
804            .as_ref()
805            .expect("connection already returned to pool")
806            .conn
807    }
808}
809
810impl<C: Connection> std::ops::DerefMut for PooledConnection<C> {
811    fn deref_mut(&mut self) -> &mut Self::Target {
812        &mut self
813            .meta
814            .as_mut()
815            .expect("connection already returned to pool")
816            .conn
817    }
818}
819
820impl<C: Connection> Drop for PooledConnection<C> {
821    fn drop(&mut self) {
822        if let Some(mut meta) = self.meta.take() {
823            meta.touch(); // Update last used time
824            if let Some(pool) = self.pool.upgrade() {
825                // Return to pool - but if mutex is poisoned, we must not panic in Drop.
826                // Instead, log the error and leak the connection.
827                let mut inner = match pool.inner.lock() {
828                    Ok(guard) => guard,
829                    Err(_poisoned) => {
830                        tracing::error!(
831                            "Pool mutex poisoned during connection return; \
832                             connection will be closed instead of returned. A thread panicked while holding the lock."
833                        );
834                        close_connection_blocking(meta.conn, "pooled connection drop poisoned");
835                        return;
836                    }
837                };
838
839                if inner.closed {
840                    inner.total_count -= 1;
841                    inner.active_count -= 1;
842                    pool.connections_closed.fetch_add(1, Ordering::Relaxed);
843                    drop(inner);
844                    close_connection_blocking(meta.conn, "pooled connection drop closed pool");
845                    return;
846                }
847
848                // Check max lifetime
849                let max_lifetime = Duration::from_millis(inner.config.max_lifetime_ms);
850                if meta.age() > max_lifetime {
851                    inner.total_count -= 1;
852                    inner.active_count -= 1;
853                    pool.connections_closed.fetch_add(1, Ordering::Relaxed);
854                    drop(inner);
855                    close_connection_blocking(meta.conn, "pooled connection drop max lifetime");
856                    return;
857                }
858
859                inner.active_count -= 1;
860                inner.idle.push_back(meta);
861
862                drop(inner);
863                pool.conn_available.notify_one();
864            } else {
865                close_connection_blocking(meta.conn, "pooled connection drop missing pool");
866            }
867        }
868    }
869}
870
871impl<C: Connection + std::fmt::Debug> std::fmt::Debug for PooledConnection<C> {
872    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873        f.debug_struct("PooledConnection")
874            .field("conn", &self.meta.as_ref().map(|m| &m.conn))
875            .field("age", &self.age())
876            .field("idle_time", &self.idle_time())
877            .finish_non_exhaustive()
878    }
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use sqlmodel_core::connection::{IsolationLevel, PreparedStatement, TransactionOps};
885    use sqlmodel_core::{Row, Value};
886    use std::sync::atomic::{AtomicBool, AtomicUsize};
887
888    /// A mock connection for testing pool behavior.
889    #[derive(Debug)]
890    struct MockConnection {
891        id: u32,
892        ping_should_fail: Arc<AtomicBool>,
893        /// Incremented each time the pool retires this connection via
894        /// `close_for_pool` (as opposed to a caller-owned `close`).
895        pool_close_calls: Arc<AtomicUsize>,
896        /// Optional probe used to prove the pool mutex is not held while the
897        /// retirement hook runs.
898        pool_shared: Option<Weak<PoolShared<MockConnection>>>,
899        pool_lock_was_free: Option<Arc<AtomicBool>>,
900    }
901
902    impl MockConnection {
903        fn new(id: u32) -> Self {
904            Self {
905                id,
906                ping_should_fail: Arc::new(AtomicBool::new(false)),
907                pool_close_calls: Arc::new(AtomicUsize::new(0)),
908                pool_shared: None,
909                pool_lock_was_free: None,
910            }
911        }
912
913        #[allow(dead_code)]
914        fn with_ping_behavior(id: u32, should_fail: Arc<AtomicBool>) -> Self {
915            Self {
916                id,
917                ping_should_fail: should_fail,
918                pool_close_calls: Arc::new(AtomicUsize::new(0)),
919                pool_shared: None,
920                pool_lock_was_free: None,
921            }
922        }
923
924        fn with_pool_close_counter(id: u32, pool_close_calls: Arc<AtomicUsize>) -> Self {
925            Self {
926                id,
927                ping_should_fail: Arc::new(AtomicBool::new(false)),
928                pool_close_calls,
929                pool_shared: None,
930                pool_lock_was_free: None,
931            }
932        }
933
934        fn with_pool_close_probe(
935            id: u32,
936            pool_close_calls: Arc<AtomicUsize>,
937            pool_shared: Weak<PoolShared<MockConnection>>,
938            pool_lock_was_free: Arc<AtomicBool>,
939        ) -> Self {
940            Self {
941                id,
942                ping_should_fail: Arc::new(AtomicBool::new(false)),
943                pool_close_calls,
944                pool_shared: Some(pool_shared),
945                pool_lock_was_free: Some(pool_lock_was_free),
946            }
947        }
948    }
949
950    /// Mock transaction for MockConnection.
951    struct MockTx;
952
953    // These test doubles deliberately mirror the trait's async spelling; the
954    // bodies are immediate because no real driver I/O occurs.
955    #[allow(clippy::unused_async_trait_impl)]
956    impl TransactionOps for MockTx {
957        async fn query(&self, _cx: &Cx, _sql: &str, _params: &[Value]) -> Outcome<Vec<Row>, Error> {
958            Outcome::Ok(vec![])
959        }
960
961        async fn query_one(
962            &self,
963            _cx: &Cx,
964            _sql: &str,
965            _params: &[Value],
966        ) -> Outcome<Option<Row>, Error> {
967            Outcome::Ok(None)
968        }
969
970        async fn execute(&self, _cx: &Cx, _sql: &str, _params: &[Value]) -> Outcome<u64, Error> {
971            Outcome::Ok(0)
972        }
973
974        async fn savepoint(&self, _cx: &Cx, _name: &str) -> Outcome<(), Error> {
975            Outcome::Ok(())
976        }
977
978        async fn rollback_to(&self, _cx: &Cx, _name: &str) -> Outcome<(), Error> {
979            Outcome::Ok(())
980        }
981
982        async fn release(&self, _cx: &Cx, _name: &str) -> Outcome<(), Error> {
983            Outcome::Ok(())
984        }
985
986        async fn commit(self, _cx: &Cx) -> Outcome<(), Error> {
987            Outcome::Ok(())
988        }
989
990        async fn rollback(self, _cx: &Cx) -> Outcome<(), Error> {
991            Outcome::Ok(())
992        }
993    }
994
995    #[allow(clippy::unused_async_trait_impl)]
996    impl Connection for MockConnection {
997        type Tx<'conn> = MockTx;
998
999        async fn query(&self, _cx: &Cx, _sql: &str, _params: &[Value]) -> Outcome<Vec<Row>, Error> {
1000            Outcome::Ok(vec![])
1001        }
1002
1003        async fn query_one(
1004            &self,
1005            _cx: &Cx,
1006            _sql: &str,
1007            _params: &[Value],
1008        ) -> Outcome<Option<Row>, Error> {
1009            Outcome::Ok(None)
1010        }
1011
1012        async fn execute(&self, _cx: &Cx, _sql: &str, _params: &[Value]) -> Outcome<u64, Error> {
1013            Outcome::Ok(0)
1014        }
1015
1016        async fn insert(&self, _cx: &Cx, _sql: &str, _params: &[Value]) -> Outcome<i64, Error> {
1017            Outcome::Ok(0)
1018        }
1019
1020        async fn batch(
1021            &self,
1022            _cx: &Cx,
1023            _statements: &[(String, Vec<Value>)],
1024        ) -> Outcome<Vec<u64>, Error> {
1025            Outcome::Ok(vec![])
1026        }
1027
1028        async fn begin(&self, _cx: &Cx) -> Outcome<Self::Tx<'_>, Error> {
1029            Outcome::Ok(MockTx)
1030        }
1031
1032        async fn begin_with(
1033            &self,
1034            _cx: &Cx,
1035            _isolation: IsolationLevel,
1036        ) -> Outcome<Self::Tx<'_>, Error> {
1037            Outcome::Ok(MockTx)
1038        }
1039
1040        async fn prepare(&self, _cx: &Cx, _sql: &str) -> Outcome<PreparedStatement, Error> {
1041            Outcome::Ok(PreparedStatement::new(1, String::new(), 0))
1042        }
1043
1044        async fn query_prepared(
1045            &self,
1046            _cx: &Cx,
1047            _stmt: &PreparedStatement,
1048            _params: &[Value],
1049        ) -> Outcome<Vec<Row>, Error> {
1050            Outcome::Ok(vec![])
1051        }
1052
1053        async fn execute_prepared(
1054            &self,
1055            _cx: &Cx,
1056            _stmt: &PreparedStatement,
1057            _params: &[Value],
1058        ) -> Outcome<u64, Error> {
1059            Outcome::Ok(0)
1060        }
1061
1062        async fn ping(&self, _cx: &Cx) -> Outcome<(), Error> {
1063            if self.ping_should_fail.load(Ordering::Relaxed) {
1064                Outcome::Err(Error::Connection(ConnectionError {
1065                    kind: ConnectionErrorKind::Disconnected,
1066                    message: "mock ping failed".to_string(),
1067                    source: None,
1068                }))
1069            } else {
1070                Outcome::Ok(())
1071            }
1072        }
1073
1074        async fn close(self, _cx: &Cx) -> Result<(), Error> {
1075            Ok(())
1076        }
1077
1078        async fn close_for_pool(self, _cx: &Cx) -> Result<(), Error> {
1079            if let (Some(pool_shared), Some(pool_lock_was_free)) =
1080                (self.pool_shared.as_ref(), self.pool_lock_was_free.as_ref())
1081            {
1082                let mutex_is_available = pool_shared
1083                    .upgrade()
1084                    .is_none_or(|shared| shared.inner.try_lock().is_ok());
1085                pool_lock_was_free.store(mutex_is_available, Ordering::Relaxed);
1086            }
1087            self.pool_close_calls.fetch_add(1, Ordering::Relaxed);
1088            Ok(())
1089        }
1090    }
1091
1092    #[test]
1093    fn test_config_default() {
1094        let config = PoolConfig::default();
1095        assert_eq!(config.min_connections, 1);
1096        assert_eq!(config.max_connections, 10);
1097        assert_eq!(config.idle_timeout_ms, 600_000);
1098        assert_eq!(config.acquire_timeout_ms, 30_000);
1099        assert_eq!(config.max_lifetime_ms, 1_800_000);
1100        assert!(config.test_on_checkout);
1101        assert!(!config.test_on_return);
1102    }
1103
1104    #[test]
1105    fn test_config_builder() {
1106        let config = PoolConfig::new(20)
1107            .min_connections(5)
1108            .idle_timeout(60_000)
1109            .acquire_timeout(5_000)
1110            .max_lifetime(300_000)
1111            .test_on_checkout(false)
1112            .test_on_return(true);
1113
1114        assert_eq!(config.min_connections, 5);
1115        assert_eq!(config.max_connections, 20);
1116        assert_eq!(config.idle_timeout_ms, 60_000);
1117        assert_eq!(config.acquire_timeout_ms, 5_000);
1118        assert_eq!(config.max_lifetime_ms, 300_000);
1119        assert!(!config.test_on_checkout);
1120        assert!(config.test_on_return);
1121    }
1122
1123    #[test]
1124    fn test_config_clone() {
1125        let config = PoolConfig::new(15).min_connections(3);
1126        let cloned = config.clone();
1127        assert_eq!(config.max_connections, cloned.max_connections);
1128        assert_eq!(config.min_connections, cloned.min_connections);
1129    }
1130
1131    #[test]
1132    fn test_stats_default() {
1133        let stats = PoolStats::default();
1134        assert_eq!(stats.total_connections, 0);
1135        assert_eq!(stats.idle_connections, 0);
1136        assert_eq!(stats.active_connections, 0);
1137        assert_eq!(stats.pending_requests, 0);
1138        assert_eq!(stats.connections_created, 0);
1139        assert_eq!(stats.connections_closed, 0);
1140        assert_eq!(stats.acquires, 0);
1141        assert_eq!(stats.timeouts, 0);
1142    }
1143
1144    #[test]
1145    fn test_stats_clone() {
1146        let stats = PoolStats {
1147            total_connections: 5,
1148            acquires: 100,
1149            ..Default::default()
1150        };
1151        let cloned = stats.clone();
1152        assert_eq!(stats.total_connections, cloned.total_connections);
1153        assert_eq!(stats.acquires, cloned.acquires);
1154    }
1155
1156    #[test]
1157    fn test_connection_meta_timing() {
1158        use std::thread;
1159
1160        // Create a dummy type for testing
1161        struct DummyConn;
1162
1163        let meta = ConnectionMeta::new(DummyConn);
1164        let initial_age = meta.age();
1165
1166        // Small sleep to ensure time passes
1167        thread::sleep(Duration::from_millis(10));
1168
1169        // Age should have increased
1170        assert!(meta.age() > initial_age);
1171        assert!(meta.idle_time() > Duration::ZERO);
1172    }
1173
1174    #[test]
1175    fn test_connection_meta_touch() {
1176        use std::thread;
1177
1178        struct DummyConn;
1179
1180        let mut meta = ConnectionMeta::new(DummyConn);
1181
1182        // Small sleep to build up some idle time
1183        thread::sleep(Duration::from_millis(10));
1184        let idle_before_touch = meta.idle_time();
1185        assert!(idle_before_touch > Duration::ZERO);
1186
1187        // Touch should reset idle time
1188        meta.touch();
1189        let idle_after_touch = meta.idle_time();
1190
1191        // After touch, idle time should be very small (less than before)
1192        assert!(idle_after_touch < idle_before_touch);
1193    }
1194
1195    #[test]
1196    fn test_pool_new() {
1197        let config = PoolConfig::new(5);
1198        let pool: Pool<MockConnection> = Pool::new(config);
1199
1200        // New pool should be empty
1201        assert_eq!(pool.idle_count(), 0);
1202        assert_eq!(pool.active_count(), 0);
1203        assert_eq!(pool.total_count(), 0);
1204        assert!(!pool.is_closed());
1205        assert!(!pool.at_capacity());
1206    }
1207
1208    #[test]
1209    fn test_pool_config() {
1210        let config = PoolConfig::new(7).min_connections(2);
1211        let pool: Pool<MockConnection> = Pool::new(config);
1212
1213        let retrieved_config = pool.config();
1214        assert_eq!(retrieved_config.max_connections, 7);
1215        assert_eq!(retrieved_config.min_connections, 2);
1216    }
1217
1218    #[test]
1219    fn test_pool_stats_initial() {
1220        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1221
1222        let stats = pool.stats();
1223        assert_eq!(stats.total_connections, 0);
1224        assert_eq!(stats.idle_connections, 0);
1225        assert_eq!(stats.active_connections, 0);
1226        assert_eq!(stats.pending_requests, 0);
1227        assert_eq!(stats.connections_created, 0);
1228        assert_eq!(stats.connections_closed, 0);
1229        assert_eq!(stats.acquires, 0);
1230        assert_eq!(stats.timeouts, 0);
1231    }
1232
1233    #[test]
1234    fn test_pool_close() {
1235        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1236
1237        assert!(!pool.is_closed());
1238        pool.close();
1239        assert!(pool.is_closed());
1240    }
1241
1242    #[test]
1243    fn test_pool_close_routes_through_close_for_pool() {
1244        let pool_close_calls = Arc::new(AtomicUsize::new(0));
1245        let pool_lock_was_free = Arc::new(AtomicBool::new(false));
1246        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(2));
1247
1248        // Seed one idle connection whose `close_for_pool` override records
1249        // the call, proving pool teardown uses the driver's pool-close path
1250        // rather than the ordinary `close`.
1251        {
1252            let mut inner = pool
1253                .shared
1254                .inner
1255                .lock()
1256                .expect("pool mutex should not be poisoned");
1257            inner.total_count = 1;
1258            inner
1259                .idle
1260                .push_back(ConnectionMeta::new(MockConnection::with_pool_close_probe(
1261                    1,
1262                    Arc::clone(&pool_close_calls),
1263                    Arc::downgrade(&pool.shared),
1264                    Arc::clone(&pool_lock_was_free),
1265                )));
1266        }
1267
1268        pool.close();
1269
1270        assert!(pool.is_closed());
1271        assert_eq!(pool_close_calls.load(Ordering::Relaxed), 1);
1272        assert!(pool_lock_was_free.load(Ordering::Relaxed));
1273    }
1274
1275    #[test]
1276    fn test_expired_idle_connection_routes_through_close_for_pool() {
1277        let pool_close_calls = Arc::new(AtomicUsize::new(0));
1278        let pool: Pool<MockConnection> =
1279            Pool::new(PoolConfig::new(2).max_lifetime(1).test_on_checkout(false));
1280        let mut expired = ConnectionMeta::new(MockConnection::with_pool_close_counter(
1281            1,
1282            Arc::clone(&pool_close_calls),
1283        ));
1284        expired.created_at = Instant::now()
1285            .checked_sub(Duration::from_secs(1))
1286            .expect("one second must fit before the current instant");
1287        {
1288            let mut inner = pool
1289                .shared
1290                .inner
1291                .lock()
1292                .expect("pool mutex should not be poisoned");
1293            inner.total_count = 1;
1294            inner.idle.push_back(expired);
1295        }
1296
1297        let runtime = RuntimeBuilder::current_thread()
1298            .build()
1299            .expect("build test runtime");
1300        let cx = Cx::for_testing();
1301        let acquired =
1302            runtime.block_on(pool.acquire(&cx, || async { Outcome::Ok(MockConnection::new(2)) }));
1303
1304        assert!(matches!(acquired, Outcome::Ok(_)));
1305        assert_eq!(pool_close_calls.load(Ordering::Relaxed), 1);
1306    }
1307
1308    #[test]
1309    fn test_failed_validation_routes_through_close_for_pool() {
1310        let pool_close_calls = Arc::new(AtomicUsize::new(0));
1311        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(2).test_on_checkout(true));
1312        let failed = MockConnection::with_pool_close_counter(1, Arc::clone(&pool_close_calls));
1313        failed.ping_should_fail.store(true, Ordering::Relaxed);
1314        {
1315            let mut inner = pool
1316                .shared
1317                .inner
1318                .lock()
1319                .expect("pool mutex should not be poisoned");
1320            inner.total_count = 1;
1321            inner.idle.push_back(ConnectionMeta::new(failed));
1322        }
1323
1324        let runtime = RuntimeBuilder::current_thread()
1325            .build()
1326            .expect("build test runtime");
1327        let cx = Cx::for_testing();
1328        let acquired =
1329            runtime.block_on(pool.acquire(&cx, || async { Outcome::Ok(MockConnection::new(2)) }));
1330
1331        assert!(matches!(acquired, Outcome::Err(_)));
1332        assert_eq!(pool_close_calls.load(Ordering::Relaxed), 1);
1333    }
1334
1335    #[test]
1336    fn test_pool_inner_can_create_new() {
1337        let mut inner = PoolInner::<MockConnection>::new(PoolConfig::new(3));
1338
1339        // Initially can create new
1340        assert!(inner.can_create_new());
1341
1342        // At capacity
1343        inner.total_count = 3;
1344        assert!(!inner.can_create_new());
1345
1346        // Below capacity again
1347        inner.total_count = 2;
1348        assert!(inner.can_create_new());
1349
1350        // Closed pool
1351        inner.closed = true;
1352        assert!(!inner.can_create_new());
1353    }
1354
1355    #[test]
1356    fn test_pool_inner_stats() {
1357        let mut inner = PoolInner::<MockConnection>::new(PoolConfig::new(10));
1358
1359        inner.total_count = 5;
1360        inner.active_count = 3;
1361        inner.waiter_count = 2;
1362        inner
1363            .idle
1364            .push_back(ConnectionMeta::new(MockConnection::new(1)));
1365        inner
1366            .idle
1367            .push_back(ConnectionMeta::new(MockConnection::new(2)));
1368
1369        let stats = inner.stats();
1370        assert_eq!(stats.total_connections, 5);
1371        assert_eq!(stats.idle_connections, 2);
1372        assert_eq!(stats.active_connections, 3);
1373        assert_eq!(stats.pending_requests, 2);
1374    }
1375
1376    #[test]
1377    fn test_pooled_connection_age_and_idle_time() {
1378        use std::thread;
1379
1380        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1381
1382        // Properly initialize pool state as if acquire happened
1383        {
1384            let mut inner = pool.shared.inner.lock().unwrap();
1385            inner.total_count = 1;
1386            inner.active_count = 1;
1387        }
1388
1389        let meta = ConnectionMeta::new(MockConnection::new(1));
1390        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1391
1392        // Should have some small positive age
1393        assert!(pooled.age() >= Duration::ZERO);
1394
1395        thread::sleep(Duration::from_millis(5));
1396        assert!(pooled.age() > Duration::ZERO);
1397    }
1398
1399    #[test]
1400    fn test_pooled_connection_detach() {
1401        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1402
1403        // Manually add a connection to simulate acquire
1404        {
1405            let mut inner = pool.shared.inner.lock().unwrap();
1406            inner.total_count = 1;
1407            inner.active_count = 1;
1408        }
1409
1410        let meta = ConnectionMeta::new(MockConnection::new(42));
1411        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1412
1413        // Verify counts before detach
1414        assert_eq!(pool.total_count(), 1);
1415        assert_eq!(pool.active_count(), 1);
1416
1417        // Detach returns the connection
1418        let conn = pooled.detach();
1419        assert_eq!(conn.id, 42);
1420
1421        // After detach, counts should be decremented
1422        assert_eq!(pool.total_count(), 0);
1423        assert_eq!(pool.active_count(), 0);
1424
1425        // connections_closed should be incremented
1426        let stats = pool.stats();
1427        assert_eq!(stats.connections_closed, 1);
1428    }
1429
1430    #[test]
1431    fn test_pooled_connection_drop_returns_to_pool() {
1432        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1433
1434        // Manually set up pool state as if we acquired a connection
1435        {
1436            let mut inner = pool.shared.inner.lock().unwrap();
1437            inner.total_count = 1;
1438            inner.active_count = 1;
1439        }
1440
1441        let meta = ConnectionMeta::new(MockConnection::new(1));
1442        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1443
1444        // While held, active=1, idle=0
1445        assert_eq!(pool.active_count(), 1);
1446        assert_eq!(pool.idle_count(), 0);
1447
1448        // Drop the connection
1449        drop(pooled);
1450
1451        // After drop, active=0, idle=1 (returned to pool)
1452        assert_eq!(pool.active_count(), 0);
1453        assert_eq!(pool.idle_count(), 1);
1454        assert_eq!(pool.total_count(), 1); // Total unchanged
1455    }
1456
1457    #[test]
1458    fn test_pooled_connection_drop_when_pool_closed() {
1459        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1460
1461        // Set up pool state
1462        {
1463            let mut inner = pool.shared.inner.lock().unwrap();
1464            inner.total_count = 1;
1465            inner.active_count = 1;
1466        }
1467
1468        let meta = ConnectionMeta::new(MockConnection::new(1));
1469        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1470
1471        // Close the pool while connection is out
1472        pool.close();
1473
1474        // Drop the connection
1475        drop(pooled);
1476
1477        // Connection should not be returned to idle (pool is closed)
1478        assert_eq!(pool.idle_count(), 0);
1479        assert_eq!(pool.active_count(), 0);
1480        assert_eq!(pool.total_count(), 0);
1481
1482        // Connection was closed
1483        assert_eq!(pool.stats().connections_closed, 1);
1484    }
1485
1486    #[test]
1487    fn test_pooled_connection_deref() {
1488        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1489
1490        // Properly initialize pool state as if acquire happened
1491        {
1492            let mut inner = pool.shared.inner.lock().unwrap();
1493            inner.total_count = 1;
1494            inner.active_count = 1;
1495        }
1496
1497        let meta = ConnectionMeta::new(MockConnection::new(99));
1498        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1499
1500        // Deref should give access to the connection's id
1501        assert_eq!(pooled.id, 99);
1502    }
1503
1504    #[test]
1505    fn test_pooled_connection_deref_mut() {
1506        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1507
1508        // Properly initialize pool state as if acquire happened
1509        {
1510            let mut inner = pool.shared.inner.lock().unwrap();
1511            inner.total_count = 1;
1512            inner.active_count = 1;
1513        }
1514
1515        let meta = ConnectionMeta::new(MockConnection::new(1));
1516        let mut pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1517
1518        // DerefMut should allow mutation
1519        pooled.id = 50;
1520        assert_eq!(pooled.id, 50);
1521    }
1522
1523    #[test]
1524    fn test_pooled_connection_debug() {
1525        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1526
1527        // Properly initialize pool state as if acquire happened
1528        {
1529            let mut inner = pool.shared.inner.lock().unwrap();
1530            inner.total_count = 1;
1531            inner.active_count = 1;
1532        }
1533
1534        let meta = ConnectionMeta::new(MockConnection::new(1));
1535        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1536
1537        let debug_str = format!("{:?}", pooled);
1538        assert!(debug_str.contains("PooledConnection"));
1539        assert!(debug_str.contains("age"));
1540    }
1541
1542    #[test]
1543    fn test_pool_at_capacity() {
1544        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(2));
1545
1546        assert!(!pool.at_capacity());
1547
1548        // Simulate connections being created
1549        {
1550            let mut inner = pool.shared.inner.lock().unwrap();
1551            inner.total_count = 1;
1552        }
1553        assert!(!pool.at_capacity());
1554
1555        {
1556            let mut inner = pool.shared.inner.lock().unwrap();
1557            inner.total_count = 2;
1558        }
1559        assert!(pool.at_capacity());
1560    }
1561
1562    #[test]
1563    fn test_acquire_action_enum() {
1564        // Verify the enum variants exist and can be pattern-matched
1565        let closed: AcquireAction<MockConnection> = AcquireAction::PoolClosed;
1566        assert!(matches!(closed, AcquireAction::PoolClosed));
1567
1568        let create: AcquireAction<MockConnection> = AcquireAction::CreateNew;
1569        assert!(matches!(create, AcquireAction::CreateNew));
1570
1571        let wait: AcquireAction<MockConnection> = AcquireAction::Wait;
1572        assert!(matches!(wait, AcquireAction::Wait));
1573
1574        let meta = ConnectionMeta::new(MockConnection::new(1));
1575        let validate: AcquireAction<MockConnection> = AcquireAction::ValidateExisting(meta);
1576        assert!(matches!(validate, AcquireAction::ValidateExisting(_)));
1577    }
1578
1579    #[test]
1580    fn test_pool_shared_atomic_counters() {
1581        let shared = PoolShared::<MockConnection>::new(PoolConfig::new(5));
1582
1583        // Initial values should be 0
1584        assert_eq!(shared.connections_created.load(Ordering::Relaxed), 0);
1585        assert_eq!(shared.connections_closed.load(Ordering::Relaxed), 0);
1586        assert_eq!(shared.acquires.load(Ordering::Relaxed), 0);
1587        assert_eq!(shared.timeouts.load(Ordering::Relaxed), 0);
1588
1589        // Test incrementing
1590        shared.connections_created.fetch_add(1, Ordering::Relaxed);
1591        shared.connections_closed.fetch_add(2, Ordering::Relaxed);
1592        shared.acquires.fetch_add(10, Ordering::Relaxed);
1593        shared.timeouts.fetch_add(3, Ordering::Relaxed);
1594
1595        assert_eq!(shared.connections_created.load(Ordering::Relaxed), 1);
1596        assert_eq!(shared.connections_closed.load(Ordering::Relaxed), 2);
1597        assert_eq!(shared.acquires.load(Ordering::Relaxed), 10);
1598        assert_eq!(shared.timeouts.load(Ordering::Relaxed), 3);
1599    }
1600
1601    #[test]
1602    fn test_pool_close_clears_idle() {
1603        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1604
1605        // Add some idle connections
1606        {
1607            let mut inner = pool.shared.inner.lock().unwrap();
1608            inner.total_count = 3;
1609            inner
1610                .idle
1611                .push_back(ConnectionMeta::new(MockConnection::new(1)));
1612            inner
1613                .idle
1614                .push_back(ConnectionMeta::new(MockConnection::new(2)));
1615            inner
1616                .idle
1617                .push_back(ConnectionMeta::new(MockConnection::new(3)));
1618        }
1619
1620        assert_eq!(pool.idle_count(), 3);
1621        assert_eq!(pool.total_count(), 3);
1622
1623        pool.close();
1624
1625        // After close, idle connections should be cleared
1626        assert_eq!(pool.idle_count(), 0);
1627        assert_eq!(pool.total_count(), 0);
1628        assert!(pool.is_closed());
1629
1630        // connections_closed should reflect the 3 idle connections
1631        assert_eq!(pool.stats().connections_closed, 3);
1632    }
1633
1634    // ==================== Lock Poisoning Safety Tests ====================
1635    //
1636    // These tests verify that the pool correctly handles mutex poisoning,
1637    // which occurs when a thread panics while holding the lock.
1638    //
1639    // Tier 1 (mutations): Return Error if poisoned
1640    // Tier 2 (read-only): Recover and return valid data
1641    // Tier 3 (Drop): Log error and leak connection (don't panic)
1642
1643    /// Helper to poison a pool's mutex by panicking while holding the lock.
1644    ///
1645    /// Returns the pool with a poisoned mutex.
1646    fn poison_pool_mutex() -> Pool<MockConnection> {
1647        use std::panic;
1648        use std::thread;
1649
1650        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1651
1652        // Set up some valid state before poisoning
1653        {
1654            let mut inner = pool.shared.inner.lock().unwrap();
1655            inner.total_count = 2;
1656            inner.active_count = 1;
1657            inner
1658                .idle
1659                .push_back(ConnectionMeta::new(MockConnection::new(1)));
1660        }
1661
1662        // Spawn a thread that will panic while holding the lock
1663        let shared_clone = Arc::clone(&pool.shared);
1664        let handle = thread::spawn(move || {
1665            let _guard = shared_clone.inner.lock().unwrap();
1666            // Panic while holding the lock - this poisons the mutex
1667            panic!("intentional panic to poison mutex");
1668        });
1669
1670        // Wait for the thread to panic (ignore the panic result)
1671        let _ = handle.join();
1672
1673        // Verify the mutex is now poisoned
1674        assert!(pool.shared.inner.lock().is_err());
1675
1676        pool
1677    }
1678
1679    // -------------------- Tier 2: Read-Only Methods --------------------
1680
1681    #[test]
1682    fn test_config_after_poisoning_returns_valid_data() {
1683        let pool = poison_pool_mutex();
1684
1685        // config() should recover and return the configuration
1686        let config = pool.config();
1687        assert_eq!(config.max_connections, 5);
1688    }
1689
1690    #[test]
1691    fn test_stats_after_poisoning_returns_valid_data() {
1692        let pool = poison_pool_mutex();
1693
1694        // stats() should recover and return valid statistics
1695        let stats = pool.stats();
1696        // The state before poisoning was: total=2, active=1, idle=1
1697        assert_eq!(stats.total_connections, 2);
1698        assert_eq!(stats.active_connections, 1);
1699        assert_eq!(stats.idle_connections, 1);
1700    }
1701
1702    #[test]
1703    fn test_at_capacity_after_poisoning() {
1704        let pool = poison_pool_mutex();
1705
1706        // at_capacity() should recover and return correct value
1707        // Pool has 2 connections, max is 5, so not at capacity
1708        assert!(!pool.at_capacity());
1709    }
1710
1711    #[test]
1712    fn test_is_closed_after_poisoning() {
1713        let pool = poison_pool_mutex();
1714
1715        // is_closed() should recover and return correct value
1716        assert!(!pool.is_closed());
1717    }
1718
1719    #[test]
1720    fn test_idle_count_after_poisoning() {
1721        let pool = poison_pool_mutex();
1722
1723        // idle_count() should recover and return correct value
1724        assert_eq!(pool.idle_count(), 1);
1725    }
1726
1727    #[test]
1728    fn test_active_count_after_poisoning() {
1729        let pool = poison_pool_mutex();
1730
1731        // active_count() should recover and return correct value
1732        assert_eq!(pool.active_count(), 1);
1733    }
1734
1735    #[test]
1736    fn test_total_count_after_poisoning() {
1737        let pool = poison_pool_mutex();
1738
1739        // total_count() should recover and return correct value
1740        assert_eq!(pool.total_count(), 2);
1741    }
1742
1743    // -------------------- Tier 1: Mutation Methods --------------------
1744
1745    #[test]
1746    fn test_lock_or_error_returns_error_when_poisoned() {
1747        use std::thread;
1748
1749        let shared = Arc::new(PoolShared::<MockConnection>::new(PoolConfig::new(5)));
1750
1751        // Poison the mutex
1752        let shared_clone = Arc::clone(&shared);
1753        let handle = thread::spawn(move || {
1754            let _guard = shared_clone.inner.lock().unwrap();
1755            panic!("intentional panic to poison mutex");
1756        });
1757        let _ = handle.join();
1758
1759        // lock_or_error should return an error
1760        let result = shared.lock_or_error("test_operation");
1761
1762        // Verify it's a pool poisoning error
1763        match result {
1764            Err(Error::Pool(pool_err)) => {
1765                assert!(matches!(pool_err.kind, PoolErrorKind::Poisoned));
1766                assert!(pool_err.message.contains("poisoned"));
1767            }
1768            Err(other) => panic!("Expected Pool error, got: {:?}", other),
1769            Ok(_) => panic!("Expected error, got Ok"),
1770        }
1771    }
1772
1773    #[test]
1774    fn test_lock_or_recover_succeeds_when_poisoned() {
1775        use std::thread;
1776
1777        let shared = Arc::new(PoolShared::<MockConnection>::new(PoolConfig::new(5)));
1778
1779        // Set up some state
1780        {
1781            let mut inner = shared.inner.lock().unwrap();
1782            inner.total_count = 42;
1783        }
1784
1785        // Poison the mutex
1786        let shared_clone = Arc::clone(&shared);
1787        let handle = thread::spawn(move || {
1788            let _guard = shared_clone.inner.lock().unwrap();
1789            panic!("intentional panic to poison mutex");
1790        });
1791        let _ = handle.join();
1792
1793        // Verify mutex is poisoned
1794        assert!(shared.inner.lock().is_err());
1795
1796        // lock_or_recover should still succeed and provide access to data
1797        let inner = shared.lock_or_recover();
1798        assert_eq!(inner.total_count, 42);
1799    }
1800
1801    #[test]
1802    fn test_close_after_poisoning_recovers_and_closes() {
1803        let pool = poison_pool_mutex();
1804
1805        // close() should recover from poisoning and still close the pool
1806        pool.close();
1807
1808        // After close, the pool should be marked as closed
1809        assert!(pool.is_closed());
1810
1811        // Idle connections should be cleared
1812        assert_eq!(pool.idle_count(), 0);
1813    }
1814
1815    // -------------------- Tier 3: Drop Safety --------------------
1816
1817    #[test]
1818    fn test_drop_pooled_connection_after_poisoning_does_not_panic() {
1819        use std::panic;
1820        use std::thread;
1821
1822        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1823
1824        // Set up a connection that's "checked out"
1825        {
1826            let mut inner = pool.shared.inner.lock().unwrap();
1827            inner.total_count = 1;
1828            inner.active_count = 1;
1829        }
1830
1831        // Create a pooled connection
1832        let meta = ConnectionMeta::new(MockConnection::new(1));
1833        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1834
1835        // Poison the mutex by panicking in another thread
1836        let shared_clone = Arc::clone(&pool.shared);
1837        let handle = thread::spawn(move || {
1838            let _guard = shared_clone.inner.lock().unwrap();
1839            panic!("intentional panic to poison mutex");
1840        });
1841        let _ = handle.join();
1842
1843        // Verify mutex is poisoned
1844        assert!(pool.shared.inner.lock().is_err());
1845
1846        // Drop the pooled connection - should NOT panic
1847        // The connection will be leaked, but that's the correct behavior
1848        let drop_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
1849            drop(pooled);
1850        }));
1851
1852        // Dropping should not panic
1853        assert!(
1854            drop_result.is_ok(),
1855            "Dropping PooledConnection after mutex poisoning should not panic"
1856        );
1857    }
1858
1859    #[test]
1860    fn test_detach_after_poisoning_does_not_panic() {
1861        use std::panic;
1862        use std::thread;
1863
1864        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1865
1866        // Set up a connection that's "checked out"
1867        {
1868            let mut inner = pool.shared.inner.lock().unwrap();
1869            inner.total_count = 1;
1870            inner.active_count = 1;
1871        }
1872
1873        // Create a pooled connection
1874        let meta = ConnectionMeta::new(MockConnection::new(42));
1875        let pooled = PooledConnection::new(meta, Arc::downgrade(&pool.shared));
1876
1877        // Poison the mutex
1878        let shared_clone = Arc::clone(&pool.shared);
1879        let handle = thread::spawn(move || {
1880            let _guard = shared_clone.inner.lock().unwrap();
1881            panic!("intentional panic to poison mutex");
1882        });
1883        let _ = handle.join();
1884
1885        // Verify mutex is poisoned
1886        assert!(pool.shared.inner.lock().is_err());
1887
1888        // Detach should not panic, even though it can't update counters
1889        let detach_result = panic::catch_unwind(panic::AssertUnwindSafe(|| pooled.detach()));
1890
1891        assert!(
1892            detach_result.is_ok(),
1893            "detach() after mutex poisoning should not panic"
1894        );
1895
1896        // Should still get the connection back
1897        let conn = detach_result.unwrap();
1898        assert_eq!(conn.id, 42);
1899    }
1900
1901    // -------------------- Integration: Pool Survives Thread Panic --------------------
1902
1903    #[test]
1904    fn test_pool_survives_thread_panic_during_acquire() {
1905        use std::thread;
1906
1907        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1908        let pool_arc = Arc::new(pool);
1909
1910        // Simulate a thread that acquires, does work, then panics
1911        // The connection should be leaked but pool should remain usable for reads
1912        let pool_clone = Arc::clone(&pool_arc);
1913        let handle = thread::spawn(move || {
1914            // Manually simulate having acquired a connection
1915            {
1916                let mut inner = pool_clone.shared.inner.lock().unwrap();
1917                inner.total_count = 1;
1918                inner.active_count = 1;
1919            }
1920
1921            // Panic while holding the pool's internal mutex to simulate a poisoned lock.
1922            // This models an internal panic in pool bookkeeping, not user code.
1923            let _guard = pool_clone.shared.inner.lock().unwrap();
1924            panic!("simulated panic during database operation");
1925        });
1926
1927        // Wait for thread to panic
1928        let _ = handle.join();
1929
1930        // Pool's mutex is now poisoned, but read-only methods should still work
1931        assert_eq!(pool_arc.total_count(), 1);
1932        assert_eq!(pool_arc.config().max_connections, 5);
1933
1934        // Stats should be recoverable
1935        let stats = pool_arc.stats();
1936        assert_eq!(stats.total_connections, 1);
1937    }
1938
1939    #[test]
1940    fn test_pool_close_after_thread_panic() {
1941        use std::thread;
1942
1943        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1944
1945        // Add some idle connections
1946        {
1947            let mut inner = pool.shared.inner.lock().unwrap();
1948            inner.total_count = 2;
1949            inner
1950                .idle
1951                .push_back(ConnectionMeta::new(MockConnection::new(1)));
1952            inner
1953                .idle
1954                .push_back(ConnectionMeta::new(MockConnection::new(2)));
1955        }
1956
1957        // Poison the mutex
1958        let shared_clone = Arc::clone(&pool.shared);
1959        let handle = thread::spawn(move || {
1960            let _guard = shared_clone.inner.lock().unwrap();
1961            panic!("intentional panic");
1962        });
1963        let _ = handle.join();
1964
1965        // close() should recover and still work
1966        pool.close();
1967
1968        // Pool should be closed and idle connections cleared
1969        assert!(pool.is_closed());
1970        assert_eq!(pool.idle_count(), 0);
1971    }
1972
1973    #[test]
1974    fn test_multiple_reads_after_poisoning() {
1975        let pool = poison_pool_mutex();
1976
1977        // Multiple read operations should all succeed
1978        for _ in 0..10 {
1979            let _ = pool.config();
1980            let _ = pool.stats();
1981            let _ = pool.at_capacity();
1982            let _ = pool.is_closed();
1983            let _ = pool.idle_count();
1984            let _ = pool.active_count();
1985            let _ = pool.total_count();
1986        }
1987
1988        // All reads should have recovered successfully
1989        assert_eq!(pool.total_count(), 2);
1990    }
1991
1992    #[test]
1993    fn test_waiters_count_after_poisoning() {
1994        use std::thread;
1995
1996        let pool: Pool<MockConnection> = Pool::new(PoolConfig::new(5));
1997
1998        // Set up waiter count
1999        {
2000            let mut inner = pool.shared.inner.lock().unwrap();
2001            inner.waiter_count = 3;
2002        }
2003
2004        // Poison the mutex
2005        let shared_clone = Arc::clone(&pool.shared);
2006        let handle = thread::spawn(move || {
2007            let _guard = shared_clone.inner.lock().unwrap();
2008            panic!("intentional panic");
2009        });
2010        let _ = handle.join();
2011
2012        // stats() should recover and show correct waiter count
2013        let stats = pool.stats();
2014        assert_eq!(stats.pending_requests, 3);
2015    }
2016}