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