Skip to main content

connpool/pools/
sync_pool.rs

1//! `ConnectionPool`: a thread-safe, blocking connection pool.
2//!
3//! A single mutex guards a tiny amount of state (an idle `Vec`, a checked-out `HashMap`, a slot
4//! counter). All I/O — create, validate, reset, close — happens outside that lock, so a slow
5//! backend never blocks unrelated callers. A slot is reserved under the lock before creating,
6//! which makes `max_size` a hard cap. The retirement rules live in
7//! [`lifecycle`](crate::policy::lifecycle); this file is plumbing.
8//!
9//! `T` is pooled by **value identity** (`Clone + Eq + Hash`) — use a cheap handle type such as
10//! `Arc<Conn>` or an id; this is the Rust analog of the Go port's `comparable` constraint.
11
12use std::collections::HashMap;
13use std::error::Error;
14use std::hash::Hash;
15use std::panic::{AssertUnwindSafe, catch_unwind};
16use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
17use std::thread::JoinHandle;
18use std::time::Duration;
19
20use crate::core::clock::{Clock, SystemClock};
21use crate::core::connection::{CloseReason, PooledConnection};
22use crate::core::errors::{FactoryError, PoolError};
23use crate::core::timeouts::{Timeout, remaining};
24use crate::factories::sync_factory::ConnectionFactory;
25use crate::observability::events::{Listeners, PoolListener};
26use crate::observability::stats::{PoolStats, StatsCollector, StatsGauges};
27use crate::policy::config::PoolConfig;
28use crate::policy::lifecycle::{
29    borrow_reason, needs_validation, new_connection, pop_idle, rotation_reason, sweep,
30};
31use crate::resilience::breaker::{BreakerState, CircuitBreaker};
32use crate::rng::{Rng, SystemRng};
33
34/// Options for constructing a pool. All fields are optional.
35pub struct PoolOptions<T> {
36    /// Pool name (defaults to `"pool"`).
37    pub name: Option<String>,
38    /// Time source (defaults to [`SystemClock`]).
39    pub clock: Option<Arc<dyn Clock>>,
40    /// Jitter RNG (defaults to [`SystemRng`]).
41    pub rng: Option<Box<dyn Rng>>,
42    /// Listeners added in addition to the built-in stats collector.
43    pub listeners: Vec<Arc<dyn PoolListener<T>>>,
44}
45
46impl<T> Default for PoolOptions<T> {
47    fn default() -> Self {
48        Self {
49            name: None,
50            clock: None,
51            rng: None,
52            listeners: Vec::new(),
53        }
54    }
55}
56
57struct State<T> {
58    idle: Vec<PooledConnection<T>>,
59    checked_out: HashMap<T, PooledConnection<T>>,
60    size: usize,
61    waiters: usize,
62    closed: bool,
63    opened: bool,
64}
65
66struct MaintStop {
67    flag: Mutex<bool>,
68    cv: Condvar,
69}
70
71pub(crate) struct Inner<T> {
72    name: String,
73    factory: Arc<dyn ConnectionFactory<T>>,
74    cfg: PoolConfig,
75    clock: Arc<dyn Clock>,
76    rng: Mutex<Box<dyn Rng>>,
77
78    state: Mutex<State<T>>,
79    cond: Condvar,
80
81    stats: Arc<StatsCollector<T>>,
82    events: Listeners<T>,
83    breaker: OnceLock<CircuitBreaker>,
84
85    stop: Arc<MaintStop>,
86    handle: Mutex<Option<JoinHandle<()>>>,
87}
88
89/// A thread-safe, blocking connection pool. Cheaply clonable (clones share one pool).
90pub struct ConnectionPool<T>(Arc<Inner<T>>);
91
92impl<T> Clone for ConnectionPool<T> {
93    fn clone(&self) -> Self {
94        ConnectionPool(self.0.clone())
95    }
96}
97
98impl<T> ConnectionPool<T>
99where
100    T: Clone + Eq + Hash + Send + Sync + 'static,
101{
102    /// Builds a pool, validating `config`.
103    pub fn new(
104        factory: impl ConnectionFactory<T> + 'static,
105        config: PoolConfig,
106        options: Option<PoolOptions<T>>,
107    ) -> Result<Self, PoolError> {
108        config.validate()?;
109        let options = options.unwrap_or_default();
110        let clock: Arc<dyn Clock> = options.clock.unwrap_or_else(|| Arc::new(SystemClock::new()));
111        let rng: Box<dyn Rng> = options.rng.unwrap_or_else(|| Box::new(SystemRng::new()));
112        let name = options.name.unwrap_or_else(|| "pool".to_string());
113
114        let stats = Arc::new(StatsCollector::new());
115        let mut listeners: Vec<Arc<dyn PoolListener<T>>> = vec![stats.clone()];
116        listeners.extend(options.listeners);
117        let events = Listeners::new(listeners);
118
119        let inner = Arc::new(Inner {
120            name,
121            factory: Arc::new(factory),
122            cfg: config,
123            clock,
124            rng: Mutex::new(rng),
125            state: Mutex::new(State {
126                idle: Vec::new(),
127                checked_out: HashMap::new(),
128                size: 0,
129                waiters: 0,
130                closed: false,
131                opened: false,
132            }),
133            cond: Condvar::new(),
134            stats,
135            events,
136            breaker: OnceLock::new(),
137            stop: Arc::new(MaintStop {
138                flag: Mutex::new(false),
139                cv: Condvar::new(),
140            }),
141            handle: Mutex::new(None),
142        });
143
144        if let Some(bc) = inner.cfg.breaker {
145            let weak = Arc::downgrade(&inner);
146            let cb = Box::new(move |old: BreakerState, new: BreakerState| {
147                if let Some(inner) = weak.upgrade() {
148                    inner.events.breaker_change(old, new);
149                }
150            });
151            let breaker = CircuitBreaker::new(bc, Some(inner.clock.clone()), Some(cb));
152            let _ = inner.breaker.set(breaker);
153        }
154
155        Ok(ConnectionPool(inner))
156    }
157
158    /// The pool's name.
159    pub fn name(&self) -> &str {
160        &self.0.name
161    }
162
163    /// Warms up `min_size` connections and starts the background maintenance thread. Optional:
164    /// without it the pool is purely lazy. Idempotent.
165    pub fn open(&self) -> Result<(), PoolError> {
166        let start_thread = {
167            let mut g = self.0.state.lock().unwrap();
168            if g.closed {
169                return Err(PoolError::Closed(format!("pool {:?} is closed", self.0.name)));
170            }
171            if g.opened {
172                return Ok(());
173            }
174            g.opened = true;
175            self.0.cfg.maintenance_interval
176        };
177        self.0.fill_to_min();
178        if let Some(interval) = start_thread {
179            self.start_maintenance(interval);
180        }
181        Ok(())
182    }
183
184    fn start_maintenance(&self, interval: f64) {
185        let weak: Weak<Inner<T>> = Arc::downgrade(&self.0);
186        let stop = self.0.stop.clone();
187        let handle = std::thread::spawn(move || {
188            loop {
189                {
190                    let g = stop.flag.lock().unwrap();
191                    let (g, _) = stop
192                        .cv
193                        .wait_timeout_while(g, Duration::from_secs_f64(interval), |s| !*s)
194                        .unwrap();
195                    if *g {
196                        return;
197                    }
198                }
199                match weak.upgrade() {
200                    Some(inner) => inner.run_maintenance_safe(),
201                    None => return,
202                }
203            }
204        });
205        *self.0.handle.lock().unwrap() = Some(handle);
206    }
207
208    /// Closes idle connections now; checked-out ones close when released. Blocked `acquire`
209    /// callers get a [`PoolError::Closed`]. Idempotent.
210    ///
211    /// `timeout` (`None` = don't wait) drains gracefully: wait up to that many seconds for
212    /// in-use connections to return before giving up (they still close on release). `0` returns
213    /// at once.
214    pub fn close(&self, timeout: Option<f64>) {
215        self.0.close(timeout);
216    }
217
218    /// Whether [`close`](Self::close) has been called.
219    pub fn is_closed(&self) -> bool {
220        self.0.state.lock().unwrap().closed
221    }
222
223    /// Gets a connection. [`Timeout::Unset`] uses `config.acquire_timeout`; [`Timeout::Infinite`]
224    /// waits forever; `Timeout::Of(0.0)` never waits.
225    pub fn acquire(&self, timeout: Timeout) -> Result<T, PoolError> {
226        self.0.acquire(timeout)
227    }
228
229    /// Returns a connection to the pool for reuse.
230    pub fn release(&self, conn: T) -> Result<(), PoolError> {
231        self.0.release(conn, false, None)
232    }
233
234    /// Returns a connection but forces it to be closed instead of reused.
235    pub fn discard(&self, conn: T) -> Result<(), PoolError> {
236        self.0.release(conn, true, None)
237    }
238
239    /// Returns a connection, signalling that the caller's work raised `error`; the pool
240    /// discards the connection if the factory deems the error fatal or validation fails.
241    pub fn release_with_error(
242        &self,
243        conn: T,
244        error: &(dyn Error + 'static),
245    ) -> Result<(), PoolError> {
246        self.0.release(conn, false, Some(error))
247    }
248
249    /// Borrows a connection for the duration of `f`, releasing it afterwards.
250    pub fn with_connection<R>(
251        &self,
252        timeout: Timeout,
253        f: impl FnOnce(&T) -> R,
254    ) -> Result<R, PoolError> {
255        let conn = self.acquire(timeout)?;
256        let out = f(&conn);
257        self.release(conn)?;
258        Ok(out)
259    }
260
261    /// An immutable snapshot of pool counters and gauges.
262    pub fn stats(&self) -> PoolStats {
263        self.0.stats_snapshot()
264    }
265
266    /// Performs one maintenance sweep (evict expired/over-idle, optionally validate idle, refill
267    /// to `min_size`, flag leaks). Runs periodically after [`open`](Self::open); public for tests
268    /// and cron-style use.
269    pub fn run_maintenance(&self) {
270        self.0.run_maintenance();
271    }
272}
273
274enum Taken<T> {
275    Reused(T),
276    Reserved,
277}
278
279impl<T> Inner<T>
280where
281    T: Clone + Eq + Hash + Send + Sync + 'static,
282{
283    fn is_closed(&self) -> bool {
284        self.state.lock().unwrap().closed
285    }
286
287    fn signal(&self) {
288        self.cond.notify_all();
289    }
290
291    fn new_conn(&self, raw: T, now: f64) -> PooledConnection<T> {
292        let mut rng = self.rng.lock().unwrap();
293        new_connection(raw, &self.cfg, now, &mut **rng)
294    }
295
296    fn retry_delay(&self, attempt: u32) -> f64 {
297        let mut rng = self.rng.lock().unwrap();
298        self.cfg.retry.delay_for(attempt, &mut **rng)
299    }
300
301    fn close(&self, timeout: Option<f64>) {
302        let idle = {
303            let mut g = self.state.lock().unwrap();
304            if g.closed {
305                return;
306            }
307            g.closed = true;
308            let idle = std::mem::take(&mut g.idle);
309            self.cond.notify_all();
310            if let Some(t) = timeout {
311                let deadline = self.clock.monotonic() + t;
312                while !g.checked_out.is_empty() {
313                    let left = deadline - self.clock.monotonic();
314                    if left <= 0.0 {
315                        break;
316                    }
317                    let (g2, _) = self
318                        .cond
319                        .wait_timeout(g, Duration::from_secs_f64(left))
320                        .unwrap();
321                    g = g2;
322                }
323            }
324            idle
325        };
326        {
327            *self.stop.flag.lock().unwrap() = true;
328            self.stop.cv.notify_all();
329        }
330        if let Some(h) = self.handle.lock().unwrap().take() {
331            let _ = h.join();
332        }
333        for pooled in idle {
334            self.destroy_detached(pooled, CloseReason::PoolClosed);
335        }
336    }
337
338    fn acquire(&self, timeout: Timeout) -> Result<T, PoolError> {
339        let started = self.clock.monotonic();
340        let deadline = timeout.compute_deadline(self.cfg.acquire_timeout, started);
341        let key = match self.acquire_loop(deadline) {
342            Ok(k) => k,
343            Err(e) => {
344                if matches!(e, PoolError::Timeout(_)) {
345                    let waited = self.clock.monotonic() - started;
346                    self.events.safe(|l| l.on_acquire_timeout(waited));
347                }
348                return Err(e);
349            }
350        };
351        let now = self.clock.monotonic();
352        let (raw, ev, wait) = {
353            let mut g = self.state.lock().unwrap();
354            let pooled = g.checked_out.get_mut(&key).expect("acquired conn present");
355            pooled.mark_checked_out(now);
356            (pooled.raw.clone(), pooled.clone(), now - started)
357        };
358        self.events.safe(|l| l.on_acquired(&ev, wait));
359        Ok(raw)
360    }
361
362    fn acquire_loop(&self, deadline: Option<f64>) -> Result<T, PoolError> {
363        loop {
364            match self.take_idle_or_reserve(deadline)? {
365                Taken::Reserved => {
366                    let (pooled, created_in) = self.create(deadline)?;
367                    let key = self.register_new(pooled, created_in)?;
368                    return Ok(key);
369                }
370                Taken::Reused(key) => match self.check_borrow(&key) {
371                    None => return Ok(key),
372                    Some(reason) => self.destroy_by_key(&key, reason),
373                },
374            }
375        }
376    }
377
378    fn take_idle_or_reserve(&self, deadline: Option<f64>) -> Result<Taken<T>, PoolError> {
379        let mut g = self.state.lock().unwrap();
380        loop {
381            if g.closed {
382                return Err(PoolError::Closed(format!("pool {:?} is closed", self.name)));
383            }
384            if let Some(conn) = pop_idle(&mut g.idle, self.cfg.idle_strategy) {
385                let key = conn.raw.clone();
386                g.checked_out.insert(key.clone(), conn);
387                return Ok(Taken::Reused(key));
388            }
389            if g.size < self.cfg.max_size {
390                g.size += 1;
391                return Ok(Taken::Reserved);
392            }
393            let left = remaining(deadline, self.clock.monotonic());
394            if let Some(l) = left {
395                if l <= 0.0 {
396                    return Err(PoolError::Timeout(format!(
397                        "pool {:?}: no connection available (max_size={})",
398                        self.name, self.cfg.max_size
399                    )));
400                }
401            }
402            if let Some(max) = self.cfg.max_waiters {
403                if g.waiters >= max {
404                    return Err(PoolError::Exhausted(format!(
405                        "pool {:?}: waiter queue full (max_waiters={})",
406                        self.name, max
407                    )));
408                }
409            }
410            g.waiters += 1;
411            g = match left {
412                None => self.cond.wait(g).unwrap(),
413                Some(l) => {
414                    let (g2, _) = self
415                        .cond
416                        .wait_timeout(g, Duration::from_secs_f64(l.max(0.0)))
417                        .unwrap();
418                    g2
419                }
420            };
421            g.waiters -= 1;
422        }
423    }
424
425    fn check_borrow(&self, key: &T) -> Option<CloseReason> {
426        let now = self.clock.monotonic();
427        let (reason, need_validate, raw) = {
428            let g = self.state.lock().unwrap();
429            let pooled = g.checked_out.get(key).expect("conn present");
430            (
431                borrow_reason(pooled, &self.cfg, now),
432                needs_validation(pooled, &self.cfg, now),
433                pooled.raw.clone(),
434            )
435        };
436        if let Some(r) = reason {
437            return Some(r);
438        }
439        if need_validate && !self.is_healthy(&raw) {
440            return Some(CloseReason::Unhealthy);
441        }
442        None
443    }
444
445    fn release(
446        &self,
447        conn: T,
448        discard: bool,
449        error: Option<&(dyn Error + 'static)>,
450    ) -> Result<(), PoolError> {
451        let pooled = {
452            let mut g = self.state.lock().unwrap();
453            g.checked_out.remove(&conn)
454        };
455        let pooled = match pooled {
456            Some(p) => p,
457            None => {
458                return Err(PoolError::InvalidRelease(
459                    "object is not checked out from this pool (double release?)".to_string(),
460                ));
461            }
462        };
463        let now = self.clock.monotonic();
464        let hold = pooled.hold_time(now);
465        let reason = self.check_release(&pooled, &conn, discard, error, now);
466        self.events.safe(|l| l.on_released(&pooled, hold));
467        match reason {
468            Some(r) => self.destroy_detached(pooled, r),
469            None => {
470                if let Err(p) = self.park(pooled, now) {
471                    self.destroy_detached(p, CloseReason::PoolClosed);
472                }
473            }
474        }
475        Ok(())
476    }
477
478    fn check_release(
479        &self,
480        pooled: &PooledConnection<T>,
481        conn: &T,
482        discard: bool,
483        error: Option<&(dyn Error + 'static)>,
484        now: f64,
485    ) -> Option<CloseReason> {
486        if discard {
487            return Some(CloseReason::Discarded);
488        }
489        if let Some(e) = error {
490            if self.factory.is_fatal_error(e) || !self.is_healthy(conn) {
491                return Some(CloseReason::Error);
492            }
493        }
494        if let Some(r) = rotation_reason(pooled, &self.cfg, now) {
495            return Some(r);
496        }
497        if self.reset_conn(conn).is_err() {
498            eprintln!(
499                "connpool: pool {:?}: reset failed; discarding {}",
500                self.name, pooled
501            );
502            return Some(CloseReason::ResetFailed);
503        }
504        None
505    }
506
507    fn park(&self, mut pooled: PooledConnection<T>, now: f64) -> Result<(), PooledConnection<T>> {
508        let mut g = self.state.lock().unwrap();
509        if g.closed {
510            return Err(pooled);
511        }
512        pooled.mark_returned(now);
513        g.idle.push(pooled);
514        self.signal();
515        Ok(())
516    }
517
518    fn create(&self, deadline: Option<f64>) -> Result<(PooledConnection<T>, f64), PoolError> {
519        let started = self.clock.monotonic();
520        let raw = match self.create_raw(deadline) {
521            Ok(r) => r,
522            Err(e) => {
523                self.free_slot();
524                return Err(e);
525            }
526        };
527        let now = self.clock.monotonic();
528        Ok((self.new_conn(raw, now), now - started))
529    }
530
531    fn register_new(
532        &self,
533        pooled: PooledConnection<T>,
534        created_in: f64,
535    ) -> Result<T, PoolError> {
536        let key = pooled.raw.clone();
537        let ev;
538        {
539            let mut g = self.state.lock().unwrap();
540            if g.closed {
541                drop(g);
542                self.events.safe(|l| l.on_connection_created(&pooled, created_in));
543                self.destroy_detached(pooled, CloseReason::PoolClosed);
544                return Err(PoolError::Closed(format!("pool {:?} is closed", self.name)));
545            }
546            if g.checked_out.contains_key(&key) || g.idle.iter().any(|c| c.raw == key) {
547                drop(g);
548                self.free_slot();
549                return Err(PoolError::ConnectionCreate {
550                    message: format!(
551                        "pool {:?}: factory.create() returned an object the pool already manages; each create must return a distinct connection",
552                        self.name
553                    ),
554                    source: None,
555                });
556            }
557            ev = pooled.clone();
558            g.checked_out.insert(key.clone(), pooled);
559        }
560        self.events.safe(|l| l.on_connection_created(&ev, created_in));
561        Ok(key)
562    }
563
564    fn create_raw(&self, deadline: Option<f64>) -> Result<T, PoolError> {
565        if let Some(b) = self.breaker.get() {
566            b.before_call()?;
567        }
568        match self.create_with_retry(deadline) {
569            Ok(raw) => {
570                if let Some(b) = self.breaker.get() {
571                    b.record_success();
572                }
573                Ok(raw)
574            }
575            Err(e) => {
576                if let Some(b) = self.breaker.get() {
577                    if matches!(e, PoolError::ConnectionCreate { .. }) {
578                        b.record_failure();
579                    } else {
580                        b.record_abort();
581                    }
582                }
583                Err(e)
584            }
585        }
586    }
587
588    fn create_with_retry(&self, deadline: Option<f64>) -> Result<T, PoolError> {
589        let retry = &self.cfg.retry;
590        let mut attempt = 0u32;
591        loop {
592            if self.is_closed() {
593                return Err(PoolError::Closed(format!("pool {:?} is closed", self.name)));
594            }
595            attempt += 1;
596            match self.create_one() {
597                Ok(raw) => return Ok(raw),
598                Err(e) => {
599                    self.events.safe(|l| l.on_create_failed(&*e));
600                    if attempt >= retry.max_attempts || !retry.should_retry(&*e) {
601                        return Err(PoolError::ConnectionCreate {
602                            message: format!(
603                                "pool {:?}: could not create connection after {} attempt(s): {}",
604                                self.name, attempt, e
605                            ),
606                            source: Some(e),
607                        });
608                    }
609                    let delay = self.retry_delay(attempt);
610                    let left = remaining(deadline, self.clock.monotonic());
611                    if let Some(l) = left {
612                        if delay >= l {
613                            return Err(PoolError::Timeout(format!(
614                                "pool {:?}: acquire deadline reached while retrying creation",
615                                self.name
616                            )));
617                        }
618                    }
619                    self.clock.sleep(delay);
620                }
621            }
622        }
623    }
624
625    fn create_one(&self) -> Result<T, FactoryError> {
626        match catch_unwind(AssertUnwindSafe(|| self.factory.create())) {
627            Ok(r) => r,
628            Err(_) => Err("create panicked".into()),
629        }
630    }
631
632    fn is_healthy(&self, raw: &T) -> bool {
633        catch_unwind(AssertUnwindSafe(|| self.factory.validate(raw))).unwrap_or(false)
634    }
635
636    fn reset_conn(&self, conn: &T) -> Result<(), FactoryError> {
637        match catch_unwind(AssertUnwindSafe(|| self.factory.reset(conn))) {
638            Ok(r) => r,
639            Err(_) => Err("reset panicked".into()),
640        }
641    }
642
643    fn free_slot(&self) {
644        let mut g = self.state.lock().unwrap();
645        g.size -= 1;
646        self.signal();
647    }
648
649    fn destroy_by_key(&self, key: &T, reason: CloseReason) {
650        let pooled = {
651            let mut g = self.state.lock().unwrap();
652            g.checked_out.remove(key)
653        };
654        if let Some(p) = pooled {
655            self.destroy_detached(p, reason);
656        }
657    }
658
659    fn destroy_detached(&self, pooled: PooledConnection<T>, reason: CloseReason) {
660        self.close_conn(&pooled);
661        {
662            let mut g = self.state.lock().unwrap();
663            g.size -= 1;
664            self.signal();
665        }
666        self.events.safe(|l| l.on_connection_closed(&pooled, reason));
667    }
668
669    fn close_conn(&self, pooled: &PooledConnection<T>) {
670        let raw = &pooled.raw;
671        let result = catch_unwind(AssertUnwindSafe(|| self.factory.close(raw)));
672        match result {
673            Ok(Ok(())) => {}
674            Ok(Err(e)) => eprintln!("connpool: pool {:?}: error closing {}: {}", self.name, pooled, e),
675            Err(_) => eprintln!("connpool: pool {:?}: panic closing {}", self.name, pooled),
676        }
677    }
678
679    fn fill_to_min(&self) {
680        loop {
681            {
682                let mut g = self.state.lock().unwrap();
683                if g.closed || g.size >= self.cfg.min_size {
684                    return;
685                }
686                g.size += 1;
687            }
688            match self.create(None) {
689                Err(e) => {
690                    eprintln!("connpool: pool {:?}: warm-up failed: {}", self.name, e);
691                    return;
692                }
693                Ok((pooled, created_in)) => {
694                    self.events.safe(|l| l.on_connection_created(&pooled, created_in));
695                    if let Err(p) = self.park(pooled, self.clock.monotonic()) {
696                        self.destroy_detached(p, CloseReason::PoolClosed);
697                    }
698                }
699            }
700        }
701    }
702
703    fn run_maintenance(&self) {
704        let doomed = {
705            let mut g = self.state.lock().unwrap();
706            if g.closed {
707                return;
708            }
709            let idle = std::mem::take(&mut g.idle);
710            let (keep, doomed) = sweep(idle, g.size, &self.cfg, self.clock.monotonic());
711            g.idle = keep;
712            doomed
713        };
714        doomed
715            .into_iter()
716            .for_each(|(p, reason)| self.destroy_detached(p, reason));
717        self.health_check_idle();
718        self.fill_to_min();
719        self.check_leaks();
720    }
721
722    fn run_maintenance_safe(&self) {
723        if catch_unwind(AssertUnwindSafe(|| self.run_maintenance())).is_err() {
724            eprintln!("connpool: pool {:?}: maintenance pass failed", self.name);
725        }
726    }
727
728    fn health_check_idle(&self) {
729        if !self.cfg.validate_idle {
730            return;
731        }
732        let keys: Vec<T> = {
733            let mut g = self.state.lock().unwrap();
734            let batch = std::mem::take(&mut g.idle);
735            let mut keys = Vec::with_capacity(batch.len());
736            for conn in batch {
737                let key = conn.raw.clone();
738                g.checked_out.insert(key.clone(), conn);
739                keys.push(key);
740            }
741            keys
742        };
743        for key in keys {
744            let raw = {
745                let g = self.state.lock().unwrap();
746                g.checked_out.get(&key).expect("reserved").raw.clone()
747            };
748            let healthy = self.is_healthy(&raw);
749            let doomed = {
750                let mut g = self.state.lock().unwrap();
751                let pooled = g.checked_out.remove(&key).expect("reserved");
752                if healthy && !g.closed {
753                    g.idle.push(pooled);
754                    self.signal();
755                    None
756                } else {
757                    Some(pooled)
758                }
759            };
760            if let Some(p) = doomed {
761                let reason = if healthy {
762                    CloseReason::PoolClosed
763                } else {
764                    CloseReason::Unhealthy
765                };
766                self.destroy_detached(p, reason);
767            }
768        }
769    }
770
771    fn check_leaks(&self) {
772        let timeout = match self.cfg.leak_detection_timeout {
773            Some(t) => t,
774            None => return,
775        };
776        let now = self.clock.monotonic();
777        let leaked: Vec<(PooledConnection<T>, f64)> = {
778            let mut g = self.state.lock().unwrap();
779            let mut out = Vec::new();
780            for pooled in g.checked_out.values_mut() {
781                if !pooled.leak_reported && pooled.hold_time(now) >= timeout {
782                    pooled.leak_reported = true;
783                    let held = pooled.hold_time(now);
784                    out.push((pooled.clone(), held));
785                }
786            }
787            out
788        };
789        for (pooled, held) in leaked {
790            eprintln!(
791                "connpool: pool {:?}: possible connection leak: {} held for {:.1}s",
792                self.name, pooled, held
793            );
794            self.events.safe(|l| l.on_connection_leak(&pooled, held));
795        }
796    }
797
798    fn stats_snapshot(&self) -> PoolStats {
799        let (name, max_size, size, idle, in_use, waiting) = {
800            let g = self.state.lock().unwrap();
801            (
802                self.name.clone(),
803                self.cfg.max_size,
804                g.size,
805                g.idle.len(),
806                g.checked_out.len(),
807                g.waiters,
808            )
809        };
810        let breaker_state = self.breaker.get().map(|b| b.state().as_str().to_string());
811        self.stats.snapshot(StatsGauges {
812            name,
813            max_size,
814            size,
815            idle,
816            in_use,
817            waiting,
818            breaker_state,
819        })
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826    use crate::resilience::breaker::BreakerConfig;
827    use crate::resilience::retry::RetryPolicy;
828    use crate::test_support::*;
829    use std::sync::atomic::Ordering;
830    use std::sync::Arc;
831
832    /// A config with background maintenance and breaker off (the default for most tests).
833    fn plain(max_size: usize) -> PoolConfig {
834        PoolConfig {
835            max_size,
836            maintenance_interval: None,
837            breaker: None,
838            ..Default::default()
839        }
840    }
841
842    #[test]
843    fn acquire_release_reuses_same_connection() {
844        let h = Harness::new();
845        let pool = h.pool(plain(2));
846        let a = pool.acquire(Timeout::Unset).unwrap();
847        let id_a = a.id;
848        pool.release(a).unwrap();
849        let b = pool.acquire(Timeout::Unset).unwrap();
850        assert_eq!(b.id, id_a, "LIFO reuse returns same connection");
851        pool.release(b).unwrap();
852        let s = pool.stats();
853        assert_eq!(s.created, 1);
854        assert_eq!(s.acquired, 2);
855        assert_eq!(s.released, 2);
856        assert_eq!(pool.name(), "test");
857    }
858
859    #[test]
860    fn fifo_strategy_rotates() {
861        let h = Harness::new();
862        let pool = h.pool(PoolConfig {
863            idle_strategy: crate::policy::config::IdleStrategy::Fifo,
864            ..plain(3)
865        });
866        let a = pool.acquire(Timeout::Unset).unwrap();
867        let b = pool.acquire(Timeout::Unset).unwrap();
868        pool.release(a).unwrap();
869        pool.release(b).unwrap();
870        // FIFO hands back the oldest (a) first.
871        let c = pool.acquire(Timeout::Unset).unwrap();
872        assert_eq!(c.id, a.id);
873    }
874
875    #[test]
876    fn max_size_is_a_hard_cap_and_zero_timeout_fails_fast() {
877        let h = Harness::new();
878        let pool = h.pool(plain(1));
879        let a = pool.acquire(Timeout::Unset).unwrap();
880        let err = pool.acquire(Timeout::Of(0.0)).unwrap_err();
881        assert!(matches!(err, PoolError::Timeout(_)));
882        assert_eq!(pool.stats().timeouts, 1);
883        pool.release(a).unwrap();
884    }
885
886    #[test]
887    fn max_waiters_backpressure() {
888        let h = Harness::new();
889        let pool = h.pool(PoolConfig {
890            max_waiters: Some(0),
891            ..plain(1)
892        });
893        let a = pool.acquire(Timeout::Unset).unwrap();
894        // Full pool, infinite timeout, zero waiter slots -> fail fast.
895        assert!(matches!(
896            pool.acquire(Timeout::Infinite),
897            Err(PoolError::Exhausted(_))
898        ));
899        pool.release(a).unwrap();
900    }
901
902    #[test]
903    fn blocked_waiter_is_woken_on_release() {
904        let h = Harness::new();
905        let pool = h.pool(plain(1));
906        let a = pool.acquire(Timeout::Unset).unwrap();
907        let p2 = pool.clone();
908        let handle = std::thread::spawn(move || p2.acquire(Timeout::Infinite).map(|c| c.id));
909        // Give the waiter a moment to block, then release.
910        std::thread::sleep(std::time::Duration::from_millis(30));
911        pool.release(a).unwrap();
912        let got = handle.join().unwrap().unwrap();
913        assert_eq!(got, a.id);
914        pool.release(Conn::new(got)).unwrap();
915    }
916
917    #[test]
918    fn timed_wait_eventually_times_out() {
919        let h = Harness::new();
920        // Real clock so the acquire deadline actually elapses during the condvar wait.
921        let pool = ConnectionPool::new(
922            h.factory(),
923            plain(1),
924            Some(PoolOptions {
925                name: Some("rt".into()),
926                clock: None,
927                rng: None,
928                listeners: vec![],
929            }),
930        )
931        .unwrap();
932        let a = pool.acquire(Timeout::Unset).unwrap();
933        let err = pool.acquire(Timeout::Of(0.05)).unwrap_err();
934        assert!(matches!(err, PoolError::Timeout(_)));
935        pool.release(a).unwrap();
936    }
937
938    #[test]
939    fn invalid_release_detected() {
940        let h = Harness::new();
941        let pool = h.pool(plain(10));
942        assert!(matches!(
943            pool.release(Conn::new(9999)).unwrap_err(),
944            PoolError::InvalidRelease(_)
945        ));
946    }
947
948    #[test]
949    fn with_connection_runs_and_releases() {
950        let h = Harness::new();
951        let pool = h.pool(plain(10));
952        let out = pool.with_connection(Timeout::Unset, |c| c.id).unwrap();
953        assert!(out >= 1);
954        assert_eq!(pool.stats().released, 1);
955        // with_connection propagates an acquire error.
956        let full = h.pool(plain(1));
957        let _held = full.acquire(Timeout::Unset).unwrap();
958        assert!(full.with_connection(Timeout::Of(0.0), |c| c.id).is_err());
959    }
960
961    #[test]
962    fn config_validation_surfaces_on_new() {
963        let h = Harness::new();
964        let result = ConnectionPool::new(h.factory(), PoolConfig { max_size: 0, ..Default::default() }, None);
965        assert!(matches!(result, Err(PoolError::Config(_))));
966    }
967
968    #[test]
969    fn discard_and_error_release_close_connection() {
970        let h = Harness::new();
971        let pool = h.pool(plain(5));
972        let a = pool.acquire(Timeout::Unset).unwrap();
973        pool.discard(a).unwrap();
974        assert_eq!(h.fac.closed_ids(), vec![a.id]);
975
976        // non-fatal error but validate still ok -> reused (not closed)
977        let b = pool.acquire(Timeout::Unset).unwrap();
978        let err: FactoryError = "oops".into();
979        pool.release_with_error(b, &*err).unwrap();
980        assert_eq!(h.fac.closed_ids().len(), 1);
981
982        // error + validation fails -> closed as Error
983        h.fac.set_validate_ok(false);
984        let c = pool.acquire(Timeout::Unset).unwrap();
985        let err2: FactoryError = "bad".into();
986        pool.release_with_error(c, &*err2).unwrap();
987        assert!(h.fac.closed_ids().contains(&c.id));
988        let s = pool.stats();
989        assert_eq!(s.closed_by_reason.get("discarded"), Some(&1));
990        assert_eq!(s.closed_by_reason.get("error"), Some(&1));
991    }
992
993    #[test]
994    fn fatal_error_closes_without_validation() {
995        let h = Harness::new();
996        h.fac.set_fatal(true);
997        let pool = h.pool(plain(5));
998        let a = pool.acquire(Timeout::Unset).unwrap();
999        let err: FactoryError = "fatal".into();
1000        pool.release_with_error(a, &*err).unwrap();
1001        assert!(h.fac.closed_ids().contains(&a.id));
1002    }
1003
1004    #[test]
1005    fn reset_failure_discards_on_release() {
1006        let h = Harness::new();
1007        h.fac.set_reset_ok(false);
1008        let pool = h.pool(plain(5));
1009        let a = pool.acquire(Timeout::Unset).unwrap();
1010        pool.release(a).unwrap();
1011        assert!(h.fac.closed_ids().contains(&a.id));
1012        assert_eq!(pool.stats().closed_by_reason.get("reset_failed"), Some(&1));
1013    }
1014
1015    #[test]
1016    fn rotation_on_release_when_expired_or_overused() {
1017        let h = Harness::new();
1018        // max_uses=1 -> after one checkout the connection rotates on release.
1019        let pool = h.pool(PoolConfig {
1020            max_uses: Some(1),
1021            max_lifetime: None,
1022            ..plain(5)
1023        });
1024        let a = pool.acquire(Timeout::Unset).unwrap();
1025        pool.release(a).unwrap();
1026        assert!(h.fac.closed_ids().contains(&a.id));
1027        assert_eq!(pool.stats().closed_by_reason.get("max_uses"), Some(&1));
1028    }
1029
1030    #[test]
1031    fn stale_idle_connection_is_replaced_on_acquire() {
1032        let h = Harness::new();
1033        let pool = h.pool(PoolConfig {
1034            max_lifetime: Some(100.0),
1035            lifetime_jitter: 0.0,
1036            max_idle_time: None,
1037            validate_after_idle: None,
1038            ..plain(5)
1039        });
1040        let a = pool.acquire(Timeout::Unset).unwrap();
1041        pool.release(a).unwrap();
1042        // Age past lifetime -> next acquire finds it expired, destroys, creates fresh.
1043        h.clock.advance(200.0);
1044        let b = pool.acquire(Timeout::Unset).unwrap();
1045        assert_ne!(b.id, a.id);
1046        assert!(h.fac.closed_ids().contains(&a.id));
1047    }
1048
1049    #[test]
1050    fn validate_on_checkout_replaces_unhealthy() {
1051        let h = Harness::new();
1052        let pool = h.pool(PoolConfig {
1053            validate_after_idle: Some(5.0),
1054            max_lifetime: None,
1055            max_idle_time: None,
1056            ..plain(5)
1057        });
1058        let a = pool.acquire(Timeout::Unset).unwrap();
1059        pool.release(a).unwrap();
1060        h.clock.advance(10.0); // idle long enough to trigger validation
1061        h.fac.set_validate_ok(false); // it's dead
1062        let b = pool.acquire(Timeout::Unset).unwrap();
1063        assert_ne!(b.id, a.id);
1064        assert_eq!(pool.stats().closed_by_reason.get("unhealthy"), Some(&1));
1065    }
1066
1067    #[test]
1068    fn create_failure_after_retries_surfaces_connection_create_error() {
1069        let h = Harness::new();
1070        h.fac.fail_next_creates(10);
1071        let pool = h.pool(PoolConfig {
1072            retry: RetryPolicy {
1073                max_attempts: 3,
1074                base_delay: 0.0,
1075                ..Default::default()
1076            },
1077            ..plain(2)
1078        });
1079        let err = pool.acquire(Timeout::Unset).unwrap_err();
1080        assert!(matches!(err, PoolError::ConnectionCreate { .. }));
1081        assert_eq!(pool.stats().create_failures, 3);
1082        // the reserved slot was freed, so a later success works
1083        h.fac.fail_next_creates(0);
1084        assert!(pool.acquire(Timeout::Unset).is_ok());
1085    }
1086
1087    #[test]
1088    fn create_retry_then_success() {
1089        let h = Harness::new();
1090        h.fac.fail_next_creates(1);
1091        let pool = h.pool(PoolConfig {
1092            retry: RetryPolicy {
1093                max_attempts: 3,
1094                base_delay: 0.0,
1095                ..Default::default()
1096            },
1097            ..plain(2)
1098        });
1099        let a = pool.acquire(Timeout::Unset).unwrap();
1100        assert_eq!(pool.stats().create_failures, 1);
1101        pool.release(a).unwrap();
1102    }
1103
1104    #[test]
1105    fn retry_delay_exceeding_deadline_times_out() {
1106        let h = Harness::new();
1107        h.fac.fail_next_creates(10);
1108        let pool = h.pool(PoolConfig {
1109            acquire_timeout: Some(0.05),
1110            retry: RetryPolicy {
1111                max_attempts: 5,
1112                base_delay: 100.0,
1113                jitter: 0.0,
1114                ..Default::default()
1115            },
1116            ..plain(2)
1117        });
1118        // First attempt fails; the backoff (100s) exceeds the 0.05s deadline -> Timeout.
1119        let err = pool.acquire(Timeout::Unset).unwrap_err();
1120        assert!(matches!(err, PoolError::Timeout(_)));
1121    }
1122
1123    #[test]
1124    fn create_panic_is_contained() {
1125        let h = Harness::new();
1126        h.fac.set_create_panic(true);
1127        let pool = h.pool(PoolConfig {
1128            retry: RetryPolicy::no_retry(),
1129            ..plain(2)
1130        });
1131        assert!(matches!(
1132            pool.acquire(Timeout::Unset),
1133            Err(PoolError::ConnectionCreate { .. })
1134        ));
1135    }
1136
1137    #[test]
1138    fn duplicate_object_rejected() {
1139        let h = Harness::new();
1140        h.fac.force_id(Some(42));
1141        let pool = h.pool(plain(5));
1142        let _a = pool.acquire(Timeout::Unset).unwrap(); // id 42 checked out
1143        // next create also returns 42 -> duplicate while 42 is managed
1144        let err = pool.acquire(Timeout::Unset).unwrap_err();
1145        assert!(matches!(err, PoolError::ConnectionCreate { .. }));
1146    }
1147
1148    #[test]
1149    fn breaker_trips_then_blocks_then_recovers() {
1150        let h = Harness::new();
1151        h.fac.fail_next_creates(100);
1152        let pool = h.pool(PoolConfig {
1153            retry: RetryPolicy::no_retry(),
1154            maintenance_interval: None,
1155            breaker: Some(BreakerConfig {
1156                failure_threshold: 1,
1157                recovery_timeout: 10.0,
1158                half_open_max_calls: 1,
1159                success_threshold: 1,
1160            }),
1161            ..Default::default()
1162        });
1163        // one failed creation trips the breaker
1164        assert!(matches!(
1165            pool.acquire(Timeout::Unset),
1166            Err(PoolError::ConnectionCreate { .. })
1167        ));
1168        // now fast-fails via the breaker
1169        assert!(matches!(
1170            pool.acquire(Timeout::Unset),
1171            Err(PoolError::CircuitOpen { .. })
1172        ));
1173        assert_eq!(pool.stats().breaker_state.as_deref(), Some("open"));
1174        // recover + succeed
1175        h.clock.advance(10.0);
1176        h.fac.fail_next_creates(0);
1177        let a = pool.acquire(Timeout::Unset).unwrap();
1178        assert_eq!(pool.stats().breaker_state.as_deref(), Some("closed"));
1179        pool.release(a).unwrap();
1180    }
1181
1182    #[test]
1183    fn breaker_abort_does_not_trip() {
1184        let h = Harness::new();
1185        h.fac.fail_next_creates(100);
1186        let pool = h.pool(PoolConfig {
1187            acquire_timeout: Some(0.05),
1188            retry: RetryPolicy {
1189                max_attempts: 5,
1190                base_delay: 100.0,
1191                jitter: 0.0,
1192                ..Default::default()
1193            },
1194            maintenance_interval: None,
1195            breaker: Some(BreakerConfig {
1196                failure_threshold: 1,
1197                ..Default::default()
1198            }),
1199            ..Default::default()
1200        });
1201        // retry backoff exceeds deadline -> Timeout -> breaker records an abort, not a failure
1202        assert!(matches!(pool.acquire(Timeout::Unset), Err(PoolError::Timeout(_))));
1203        assert_eq!(pool.stats().breaker_state.as_deref(), Some("closed"));
1204    }
1205
1206    #[test]
1207    fn open_warms_up_and_close_drains() {
1208        let h = Harness::new();
1209        let rec = Recorder::new();
1210        let pool = h.pool_with(
1211            PoolConfig {
1212                min_size: 2,
1213                maintenance_interval: None,
1214                breaker: None,
1215                ..Default::default()
1216            },
1217            vec![rec.clone()],
1218        );
1219        pool.open().unwrap();
1220        assert!(pool.open().is_ok()); // idempotent
1221        assert_eq!(pool.stats().idle, 2);
1222        assert_eq!(rec.created.load(Ordering::SeqCst), 2);
1223        pool.close(Some(0.0));
1224        pool.close(None); // idempotent
1225        assert!(pool.is_closed());
1226        assert_eq!(rec.closed.load(Ordering::SeqCst), 2);
1227        // open after close errors
1228        assert!(matches!(pool.open(), Err(PoolError::Closed(_))));
1229    }
1230
1231    #[test]
1232    fn close_drains_checked_out_with_timeout() {
1233        let h = Harness::new();
1234        let pool = h.pool(plain(2));
1235        let a = pool.acquire(Timeout::Unset).unwrap();
1236        let p2 = pool.clone();
1237        let closer = std::thread::spawn(move || p2.close(Some(5.0)));
1238        std::thread::sleep(std::time::Duration::from_millis(30));
1239        pool.release(a).unwrap(); // lets the drain complete
1240        closer.join().unwrap();
1241        assert!(pool.is_closed());
1242    }
1243
1244    #[test]
1245    fn release_into_closed_pool_closes_connection() {
1246        let h = Harness::new();
1247        let pool = h.pool(plain(2));
1248        let a = pool.acquire(Timeout::Unset).unwrap();
1249        pool.close(None);
1250        // connection still checked out; releasing now must close it (pool_closed)
1251        pool.release(a).unwrap();
1252        assert!(h.fac.closed_ids().contains(&a.id));
1253    }
1254
1255    #[test]
1256    fn maintenance_sweeps_and_refills() {
1257        let h = Harness::new();
1258        let pool = h.pool(PoolConfig {
1259            min_size: 1,
1260            max_lifetime: Some(100.0),
1261            lifetime_jitter: 0.0,
1262            max_idle_time: Some(50.0),
1263            validate_after_idle: None,
1264            maintenance_interval: None,
1265            breaker: None,
1266            ..Default::default()
1267        });
1268        pool.open().unwrap();
1269        let first_idle = pool.stats().idle;
1270        assert_eq!(first_idle, 1);
1271        // age out the warm connection; maintenance evicts then refills to min_size
1272        h.clock.advance(200.0);
1273        pool.run_maintenance();
1274        let s = pool.stats();
1275        assert_eq!(s.idle, 1); // refilled
1276        assert!(s.closed >= 1); // old one evicted (expired)
1277        pool.close(None);
1278    }
1279
1280    #[test]
1281    fn maintenance_noop_when_closed() {
1282        let h = Harness::new();
1283        let pool = h.pool(plain(2));
1284        pool.close(None);
1285        pool.run_maintenance(); // early-returns, no panic
1286    }
1287
1288    #[test]
1289    fn health_check_idle_evicts_unhealthy() {
1290        let h = Harness::new();
1291        let pool = h.pool(PoolConfig {
1292            validate_idle: true,
1293            max_lifetime: None,
1294            max_idle_time: None,
1295            validate_after_idle: None,
1296            maintenance_interval: None,
1297            breaker: None,
1298            ..Default::default()
1299        });
1300        let a = pool.acquire(Timeout::Unset).unwrap();
1301        pool.release(a).unwrap();
1302        // healthy pass keeps it
1303        pool.run_maintenance();
1304        assert_eq!(pool.stats().idle, 1);
1305        // unhealthy pass evicts it
1306        h.fac.set_validate_ok(false);
1307        pool.run_maintenance();
1308        assert_eq!(pool.stats().idle, 0);
1309        assert_eq!(pool.stats().closed_by_reason.get("unhealthy"), Some(&1));
1310    }
1311
1312    #[test]
1313    fn leak_detection_warns_once() {
1314        let h = Harness::new();
1315        let rec = Recorder::new();
1316        let pool = h.pool_with(
1317            PoolConfig {
1318                leak_detection_timeout: Some(10.0),
1319                maintenance_interval: None,
1320                breaker: None,
1321                ..Default::default()
1322            },
1323            vec![rec.clone()],
1324        );
1325        let a = pool.acquire(Timeout::Unset).unwrap();
1326        h.clock.advance(20.0); // held longer than the leak threshold
1327        pool.run_maintenance();
1328        pool.run_maintenance(); // already reported -> not counted twice
1329        assert_eq!(rec.leaks.load(Ordering::SeqCst), 1);
1330        pool.release(a).unwrap();
1331    }
1332
1333    #[test]
1334    fn background_maintenance_thread_runs_and_stops() {
1335        let h = Harness::new();
1336        let pool = h.pool(PoolConfig {
1337            min_size: 0,
1338            maintenance_interval: Some(0.01),
1339            breaker: None,
1340            ..Default::default()
1341        });
1342        pool.open().unwrap();
1343        std::thread::sleep(std::time::Duration::from_millis(40)); // let the ticker fire
1344        pool.close(None); // signals + joins the thread
1345        assert!(pool.is_closed());
1346    }
1347
1348    #[test]
1349    fn listener_panic_does_not_break_pool() {
1350        struct Boom;
1351        impl PoolListener<Conn> for Boom {
1352            fn on_acquired(&self, _c: &PooledConnection<Conn>, _w: f64) {
1353                panic!("listener boom");
1354            }
1355        }
1356        let h = Harness::new();
1357        let pool = h.pool_with(plain(2), vec![Arc::new(Boom)]);
1358        let a = pool.acquire(Timeout::Unset).unwrap(); // panic swallowed
1359        pool.release(a).unwrap();
1360    }
1361
1362    #[test]
1363    fn breaker_state_change_reaches_listeners() {
1364        let h = Harness::new();
1365        let rec = Recorder::new();
1366        h.fac.fail_next_creates(100);
1367        let pool = h.pool_with(
1368            PoolConfig {
1369                retry: RetryPolicy::no_retry(),
1370                maintenance_interval: None,
1371                breaker: Some(BreakerConfig { failure_threshold: 1, ..Default::default() }),
1372                ..Default::default()
1373            },
1374            vec![rec.clone()],
1375        );
1376        let _ = pool.acquire(Timeout::Unset); // fails -> breaker trips -> listener notified
1377        assert!(!rec.breaker_changes.lock().unwrap().is_empty());
1378    }
1379
1380    #[test]
1381    fn waiter_under_max_waiters_blocks_then_succeeds() {
1382        let h = Harness::new();
1383        let pool = h.pool(PoolConfig { max_waiters: Some(5), ..plain(1) });
1384        let a = pool.acquire(Timeout::Unset).unwrap();
1385        let p2 = pool.clone();
1386        let waiter = std::thread::spawn(move || p2.acquire(Timeout::Infinite).map(|c| c.id));
1387        std::thread::sleep(std::time::Duration::from_millis(30));
1388        pool.release(a).unwrap();
1389        assert_eq!(waiter.join().unwrap().unwrap(), a.id);
1390        pool.release(Conn::new(a.id)).unwrap();
1391    }
1392
1393    #[test]
1394    fn retry_sleeps_without_deadline() {
1395        let h = Harness::new();
1396        h.fac.fail_next_creates(1);
1397        let pool = h.pool(PoolConfig {
1398            acquire_timeout: None, // no deadline -> remaining() is None in the retry loop
1399            retry: RetryPolicy { max_attempts: 3, base_delay: 1.0, jitter: 0.0, ..Default::default() },
1400            ..plain(2)
1401        });
1402        let a = pool.acquire(Timeout::Unset).unwrap(); // first create fails, sleeps, then ok
1403        pool.release(a).unwrap();
1404    }
1405
1406    #[test]
1407    fn validate_panic_on_checkout_treated_unhealthy() {
1408        let h = Harness::new();
1409        let pool = h.pool(PoolConfig {
1410            validate_after_idle: Some(5.0),
1411            max_lifetime: None,
1412            max_idle_time: None,
1413            ..plain(5)
1414        });
1415        let a = pool.acquire(Timeout::Unset).unwrap();
1416        pool.release(a).unwrap();
1417        h.clock.advance(10.0);
1418        h.fac.set_validate_panic(true);
1419        let b = pool.acquire(Timeout::Unset).unwrap(); // panic -> unhealthy -> fresh conn
1420        assert_ne!(b.id, a.id);
1421        pool.release(b).unwrap();
1422    }
1423
1424    #[test]
1425    fn reset_panic_discards() {
1426        let h = Harness::new();
1427        h.fac.set_reset_panic(true);
1428        let pool = h.pool(plain(5));
1429        let a = pool.acquire(Timeout::Unset).unwrap();
1430        pool.release(a).unwrap();
1431        assert_eq!(pool.stats().closed_by_reason.get("reset_failed"), Some(&1));
1432    }
1433
1434    #[test]
1435    fn close_errors_and_panics_are_logged() {
1436        let h = Harness::new();
1437        h.fac.set_close_err(true);
1438        let pool = h.pool(plain(5));
1439        let a = pool.acquire(Timeout::Unset).unwrap();
1440        pool.discard(a).unwrap(); // close returns Err -> logged, still counted closed
1441        assert_eq!(pool.stats().closed, 1);
1442
1443        h.fac.set_close_err(false);
1444        h.fac.set_close_panic(true);
1445        let b = pool.acquire(Timeout::Unset).unwrap();
1446        pool.discard(b).unwrap(); // close panics -> caught + logged
1447        assert_eq!(pool.stats().closed, 2);
1448    }
1449
1450    #[test]
1451    fn warmup_failure_is_logged_and_stops() {
1452        let h = Harness::new();
1453        h.fac.fail_next_creates(100);
1454        let pool = h.pool(PoolConfig {
1455            min_size: 2,
1456            retry: RetryPolicy::no_retry(),
1457            maintenance_interval: None,
1458            breaker: None,
1459            ..Default::default()
1460        });
1461        pool.open().unwrap(); // warm-up fails on first create -> logged, returns
1462        assert_eq!(pool.stats().idle, 0);
1463    }
1464
1465    #[test]
1466    fn pool_closed_during_create_registers_as_closed() {
1467        // A factory that closes the pool from inside create(), after producing a connection.
1468        let slot: Arc<Mutex<Option<ConnectionPool<Conn>>>> = Arc::new(Mutex::new(None));
1469        let s2 = slot.clone();
1470        let factory = crate::factories::sync_factory::FnFactory::new(move || {
1471            if let Some(p) = s2.lock().unwrap().as_ref() {
1472                p.close(None);
1473            }
1474            Ok::<_, FactoryError>(Conn::new(7))
1475        });
1476        let pool = ConnectionPool::new(factory, plain(2), None).unwrap();
1477        *slot.lock().unwrap() = Some(pool.clone());
1478        // create succeeds then register_new sees the pool closed.
1479        assert!(matches!(pool.acquire(Timeout::Unset), Err(PoolError::Closed(_))));
1480    }
1481
1482    #[test]
1483    fn pool_closed_mid_retry_loop() {
1484        // A factory that closes the pool then fails, so the retry loop's closed-check fires.
1485        let slot: Arc<Mutex<Option<ConnectionPool<Conn>>>> = Arc::new(Mutex::new(None));
1486        let s2 = slot.clone();
1487        let factory = crate::factories::sync_factory::FnFactory::new(move || {
1488            if let Some(p) = s2.lock().unwrap().as_ref() {
1489                p.close(None);
1490            }
1491            Err::<Conn, FactoryError>("boom".into())
1492        });
1493        let pool = ConnectionPool::new(
1494            factory,
1495            PoolConfig {
1496                retry: RetryPolicy { max_attempts: 3, base_delay: 0.0, ..Default::default() },
1497                ..plain(2)
1498            },
1499            None,
1500        )
1501        .unwrap();
1502        *slot.lock().unwrap() = Some(pool.clone());
1503        assert!(matches!(pool.acquire(Timeout::Unset), Err(PoolError::Closed(_))));
1504    }
1505
1506    #[test]
1507    fn maintenance_panic_is_contained() {
1508        let h = Harness::new();
1509        let pool = h.pool(plain(2));
1510        // Poison the state mutex, then run the guarded maintenance entry point.
1511        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1512            let _g = pool.0.state.lock().unwrap();
1513            panic!("poison");
1514        }));
1515        pool.0.run_maintenance_safe(); // catches the poisoned-lock panic, logs, returns
1516    }
1517
1518    #[test]
1519    fn close_with_timeout_gives_up_when_deadline_passed() {
1520        let h = Harness::new();
1521        let pool = h.pool(plain(2));
1522        let a = pool.acquire(Timeout::Unset).unwrap();
1523        // immediate drain deadline with a connection still out -> loop breaks at once
1524        pool.close(Some(0.0));
1525        assert!(pool.is_closed());
1526        pool.release(a).unwrap(); // still releasable; closes the conn
1527    }
1528
1529    #[test]
1530    fn blocked_waiter_wakes_as_closed_when_pool_closes() {
1531        let h = Harness::new();
1532        let pool = h.pool(plain(1));
1533        let a = pool.acquire(Timeout::Unset).unwrap();
1534        let p2 = pool.clone();
1535        let waiter = std::thread::spawn(move || p2.acquire(Timeout::Infinite));
1536        std::thread::sleep(std::time::Duration::from_millis(30));
1537        pool.close(None); // wakes the waiter, which re-checks closed
1538        let res = waiter.join().unwrap();
1539        assert!(matches!(res, Err(PoolError::Closed(_))));
1540        pool.release(a).unwrap();
1541    }
1542
1543    #[test]
1544    fn fill_to_min_park_fails_when_closed_mid_create() {
1545        // A factory that closes the pool from inside create() (returning Ok) so fill_to_min's
1546        // park sees the pool closed and destroys the just-built connection.
1547        let slot: Arc<Mutex<Option<ConnectionPool<Conn>>>> = Arc::new(Mutex::new(None));
1548        let s2 = slot.clone();
1549        let factory = crate::factories::sync_factory::FnFactory::new(move || {
1550            if let Some(p) = s2.lock().unwrap().as_ref() {
1551                p.close(None);
1552            }
1553            Ok::<_, FactoryError>(Conn::new(7))
1554        });
1555        let pool = ConnectionPool::new(
1556            factory,
1557            PoolConfig {
1558                min_size: 1,
1559                retry: RetryPolicy::no_retry(),
1560                maintenance_interval: None,
1561                breaker: None,
1562                ..Default::default()
1563            },
1564            None,
1565        )
1566        .unwrap();
1567        *slot.lock().unwrap() = Some(pool.clone());
1568        pool.open().unwrap(); // warm-up creates one, pool closes, park fails -> destroyed
1569        assert!(pool.is_closed());
1570    }
1571
1572    #[test]
1573    fn health_check_closed_mid_validation_uses_pool_closed_reason() {
1574        // validate() closes the pool and reports healthy, so health_check_idle evicts with
1575        // PoolClosed (healthy-but-closed branch).
1576        let slot: Arc<Mutex<Option<ConnectionPool<Conn>>>> = Arc::new(Mutex::new(None));
1577        let close_on_validate = Arc::new(std::sync::atomic::AtomicBool::new(false));
1578        let s2 = slot.clone();
1579        let flag = close_on_validate.clone();
1580        let next = Arc::new(std::sync::atomic::AtomicU64::new(0));
1581        let factory = crate::factories::sync_factory::FnFactory::new(move || {
1582            Ok::<_, FactoryError>(Conn::new(next.fetch_add(1, Ordering::SeqCst) + 1))
1583        })
1584        .with_validate(move |_c| {
1585            if flag.load(Ordering::SeqCst) {
1586                s2.lock().unwrap().as_ref().unwrap().close(None);
1587            }
1588            true
1589        });
1590        let pool = ConnectionPool::new(
1591            factory,
1592            PoolConfig {
1593                validate_idle: true,
1594                validate_after_idle: None,
1595                max_lifetime: None,
1596                max_idle_time: None,
1597                maintenance_interval: None,
1598                breaker: None,
1599                ..Default::default()
1600            },
1601            None,
1602        )
1603        .unwrap();
1604        *slot.lock().unwrap() = Some(pool.clone());
1605        let a = pool.acquire(Timeout::Unset).unwrap();
1606        pool.release(a).unwrap(); // one idle conn
1607        close_on_validate.store(true, Ordering::SeqCst);
1608        pool.run_maintenance(); // health check validates -> closes pool -> healthy-but-closed
1609        assert!(pool.is_closed());
1610    }
1611
1612    #[test]
1613    fn default_options_when_none() {
1614        // None options -> default name/clock/rng (PoolOptions::default path).
1615        let h = Harness::new();
1616        let pool = ConnectionPool::new(h.factory(), plain(2), None).unwrap();
1617        assert_eq!(pool.name(), "pool");
1618        let a = pool.acquire(Timeout::Unset).unwrap();
1619        pool.release(a).unwrap();
1620    }
1621
1622    #[test]
1623    fn dropping_pool_without_close_stops_sweeper() {
1624        let h = Harness::new();
1625        let pool = h.pool(PoolConfig {
1626            maintenance_interval: Some(0.01),
1627            breaker: None,
1628            ..Default::default()
1629        });
1630        pool.open().unwrap();
1631        drop(pool); // Inner::drop signals the sweeper to stop
1632        std::thread::sleep(std::time::Duration::from_millis(30));
1633    }
1634}