Skip to main content

pool_mod/
pool.rs

1//! The pool itself: [`Pool`] and its [`Builder`].
2
3use std::collections::VecDeque;
4use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
5use std::time::{Duration, Instant};
6
7use crate::config::PoolConfig;
8use crate::error::Error;
9use crate::manager::Manager;
10use crate::object::Pooled;
11use crate::status::Status;
12
13/// Acquire a mutex guard, recovering the data even if a previous holder panicked.
14///
15/// The pool only mutates plain counters and a queue while the lock is held, never
16/// running user code, so the protected state is always consistent at an unlock
17/// point. Honouring poison would convert an unrelated panic elsewhere into a
18/// permanent, unrecoverable pool outage, which is the worse failure mode.
19#[inline]
20pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
21    mutex.lock().unwrap_or_else(PoisonError::into_inner)
22}
23
24/// A resource resting in the idle set, tagged with the timestamps the pool uses
25/// to enforce `idle_timeout` and `max_lifetime`.
26pub(crate) struct Idle<R> {
27    pub(crate) resource: R,
28    pub(crate) created_at: Instant,
29    pub(crate) last_used: Instant,
30}
31
32/// The mutable inner state guarded by the pool's mutex.
33struct State<R> {
34    idle: VecDeque<Idle<R>>,
35    /// Resources the pool currently owns: idle, checked out, or mid-creation.
36    total: usize,
37    closed: bool,
38}
39
40/// The decision reached while holding the lock, carried out once it is released.
41///
42/// Only outcomes that require running user code — and therefore must not hold the
43/// lock — escape the locked region; waiting and immediate errors are handled in
44/// place.
45enum Action<R> {
46    Reuse(Idle<R>),
47    Create,
48}
49
50/// Shared pool state behind an [`Arc`]. Every [`Pool`] handle and every live
51/// [`Pooled`] guard holds one of these.
52pub(crate) struct PoolInner<M: Manager> {
53    pub(crate) manager: M,
54    config: PoolConfig,
55    state: Mutex<State<M::Resource>>,
56    available: Condvar,
57}
58
59impl<M: Manager> PoolInner<M> {
60    /// Take a resource out of the pool, blocking until one is free, a slot opens
61    /// for a fresh one, or the deadline passes.
62    fn acquire(
63        &self,
64        deadline: Option<Instant>,
65    ) -> Result<(M::Resource, Instant), Error<M::Error>> {
66        loop {
67            let action = {
68                let mut state = lock(&self.state);
69                loop {
70                    if state.closed {
71                        return Err(Error::Closed);
72                    }
73                    if let Some(idle) = state.idle.pop_front() {
74                        break Action::Reuse(idle);
75                    }
76                    if state.total < self.config.max_size {
77                        state.total += 1;
78                        break Action::Create;
79                    }
80                    // Saturated: wait for a check-in or a close, then re-evaluate.
81                    match deadline {
82                        None => {
83                            state = self
84                                .available
85                                .wait(state)
86                                .unwrap_or_else(PoisonError::into_inner);
87                        }
88                        Some(dl) => {
89                            let now = Instant::now();
90                            if now >= dl {
91                                return Err(Error::Timeout);
92                            }
93                            let (guard, _) = self
94                                .available
95                                .wait_timeout(state, dl - now)
96                                .unwrap_or_else(PoisonError::into_inner);
97                            state = guard;
98                        }
99                    }
100                }
101            };
102
103            match action {
104                Action::Reuse(idle) => {
105                    if let Some(prepared) = self.prepare(idle) {
106                        return Ok(prepared);
107                    }
108                    // The idle resource was stale or invalid and has been dropped;
109                    // release its slot and try again from the top.
110                    self.release_slot();
111                }
112                Action::Create => match self.manager.create() {
113                    Ok(resource) => return Ok((resource, Instant::now())),
114                    Err(source) => {
115                        self.release_slot();
116                        return Err(Error::Backend(source));
117                    }
118                },
119            }
120        }
121    }
122
123    /// Apply lifetime, idle-timeout, and validation checks to an idle resource.
124    ///
125    /// Returns the resource and its original creation time on success; returns
126    /// `None` (dropping the resource) when it is too old, has sat idle too long,
127    /// or fails validation.
128    fn prepare(&self, mut idle: Idle<M::Resource>) -> Option<(M::Resource, Instant)> {
129        let now = Instant::now();
130        if let Some(max_lifetime) = self.config.max_lifetime {
131            if now.saturating_duration_since(idle.created_at) >= max_lifetime {
132                return None;
133            }
134        }
135        if let Some(idle_timeout) = self.config.idle_timeout {
136            if now.saturating_duration_since(idle.last_used) >= idle_timeout {
137                return None;
138            }
139        }
140        if !self.manager.validate(&mut idle.resource) {
141            return None;
142        }
143        Some((idle.resource, idle.created_at))
144    }
145
146    /// Give back a reserved slot and wake one waiter so it can claim it.
147    fn release_slot(&self) {
148        let mut state = lock(&self.state);
149        state.total = state.total.saturating_sub(1);
150        drop(state);
151        self.available.notify_one();
152    }
153
154    /// Return a borrowed resource to the pool. Called from [`Pooled`]'s `Drop`.
155    pub(crate) fn checkin(&self, mut resource: M::Resource, created_at: Instant) {
156        let recycled = self.manager.recycle(&mut resource);
157        let mut state = lock(&self.state);
158        if state.closed || recycled.is_err() {
159            state.total = state.total.saturating_sub(1);
160            drop(state);
161            self.available.notify_one();
162            // `resource` is dropped here, outside the lock.
163        } else {
164            let last_used = Instant::now();
165            state.idle.push_back(Idle {
166                resource,
167                created_at,
168                last_used,
169            });
170            drop(state);
171            self.available.notify_one();
172        }
173    }
174}
175
176/// A thread-safe pool of reusable resources.
177///
178/// A `Pool<M>` lends out resources built by a [`Manager`], reclaiming and
179/// recycling each one when its [`Pooled`] guard is dropped. It is cheap to clone
180/// — every clone is a handle onto the same shared pool — so share it across
181/// threads by cloning rather than wrapping it in another `Arc`.
182///
183/// The pool is runtime-agnostic and carries no async dependency.
184/// [`get`](Pool::get) blocks the calling thread until a resource is available; in
185/// an async context, acquire on a blocking-friendly executor thread (for example
186/// `tokio::task::spawn_blocking`). The returned guard is `Send`, so it can be
187/// held across `.await` points.
188///
189/// # Examples
190///
191/// ```
192/// use pool_mod::{Manager, Pool};
193/// use std::convert::Infallible;
194///
195/// struct Connections;
196/// impl Manager for Connections {
197///     type Resource = String;
198///     type Error = Infallible;
199///     fn create(&self) -> Result<String, Infallible> { Ok(String::new()) }
200///     fn recycle(&self, c: &mut String) -> Result<(), Infallible> { c.clear(); Ok(()) }
201/// }
202///
203/// let pool = Pool::builder(Connections).max_size(4).build()
204///     .expect("configuration is valid");
205///
206/// let mut conn = pool.get().expect("a connection is available");
207/// conn.push_str("SELECT 1");
208/// assert_eq!(pool.status().in_use, 1);
209/// drop(conn);
210/// assert_eq!(pool.status().in_use, 0);
211/// ```
212pub struct Pool<M: Manager>(Arc<PoolInner<M>>);
213
214impl<M: Manager> Clone for Pool<M> {
215    fn clone(&self) -> Self {
216        Pool(Arc::clone(&self.0))
217    }
218}
219
220impl<M: Manager> Pool<M> {
221    /// Start building a pool for `manager` with the default configuration.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use pool_mod::{Manager, Pool};
227    /// use std::convert::Infallible;
228    /// # struct M;
229    /// # impl Manager for M {
230    /// #   type Resource = u32; type Error = Infallible;
231    /// #   fn create(&self) -> Result<u32, Infallible> { Ok(0) }
232    /// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
233    /// # }
234    /// let pool = Pool::builder(M).max_size(8).min_idle(2).build()
235    ///     .expect("configuration is valid");
236    /// assert_eq!(pool.status().max_size, 8);
237    /// ```
238    pub fn builder(manager: M) -> Builder<M> {
239        Builder::new(manager)
240    }
241
242    /// Build a pool for `manager` with the [default configuration](PoolConfig::default).
243    ///
244    /// A shortcut for `Pool::builder(manager).build()`.
245    ///
246    /// # Errors
247    ///
248    /// Returns [`Error::Backend`] if pre-creating the initial resources fails.
249    /// (With the default `min_idle` of 0, no resources are created up front, so
250    /// the default-configured pool only fails to build if you have customized the
251    /// configuration through [`Pool::builder`] instead.)
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use pool_mod::{Manager, Pool};
257    /// use std::convert::Infallible;
258    /// # struct M;
259    /// # impl Manager for M {
260    /// #   type Resource = u32; type Error = Infallible;
261    /// #   fn create(&self) -> Result<u32, Infallible> { Ok(0) }
262    /// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
263    /// # }
264    /// let pool = Pool::new(M).expect("configuration is valid");
265    /// assert_eq!(pool.status().max_size, 10); // the default
266    /// ```
267    pub fn new(manager: M) -> Result<Self, Error<M::Error>> {
268        Builder::new(manager).build()
269    }
270
271    /// Borrow a resource, waiting up to the configured
272    /// [`create_timeout`](PoolConfig::create_timeout) if the pool is saturated.
273    ///
274    /// Reuses an idle resource when one is available (after validation), grows the
275    /// pool toward `max_size` when it is not, and otherwise blocks until a
276    /// resource is returned or the timeout elapses.
277    ///
278    /// # Errors
279    ///
280    /// - [`Error::Backend`] if the manager fails to create a resource.
281    /// - [`Error::Timeout`] if the pool stays saturated past `create_timeout`.
282    /// - [`Error::Closed`] if the pool has been closed.
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use pool_mod::{Manager, Pool};
288    /// use std::convert::Infallible;
289    /// # struct M;
290    /// # impl Manager for M {
291    /// #   type Resource = u32; type Error = Infallible;
292    /// #   fn create(&self) -> Result<u32, Infallible> { Ok(7) }
293    /// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
294    /// # }
295    /// let pool = Pool::builder(M).max_size(2).build().expect("valid");
296    /// let resource = pool.get().expect("available");
297    /// assert_eq!(*resource, 7);
298    /// ```
299    pub fn get(&self) -> Result<Pooled<M>, Error<M::Error>> {
300        let deadline = self
301            .0
302            .config
303            .create_timeout
304            .map(|timeout| Instant::now() + timeout);
305        self.acquire(deadline)
306    }
307
308    /// Borrow a resource, waiting at most `timeout` regardless of the configured
309    /// [`create_timeout`](PoolConfig::create_timeout).
310    ///
311    /// A `timeout` of [`Duration::ZERO`] makes this a non-blocking try: it returns
312    /// [`Error::Timeout`] at once if no resource can be handed out immediately.
313    ///
314    /// # Errors
315    ///
316    /// - [`Error::Backend`] if the manager fails to create a resource.
317    /// - [`Error::Timeout`] if no resource becomes available within `timeout`.
318    /// - [`Error::Closed`] if the pool has been closed.
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use std::time::Duration;
324    /// use pool_mod::{Error, Manager, Pool};
325    /// use std::convert::Infallible;
326    /// # struct M;
327    /// # impl Manager for M {
328    /// #   type Resource = u32; type Error = Infallible;
329    /// #   fn create(&self) -> Result<u32, Infallible> { Ok(0) }
330    /// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
331    /// # }
332    /// let pool = Pool::builder(M).max_size(1).build().expect("valid");
333    /// let held = pool.get().expect("first checkout");
334    /// // The single slot is taken, so an immediate retry times out.
335    /// assert!(matches!(pool.get_timeout(Duration::ZERO), Err(Error::Timeout)));
336    /// ```
337    pub fn get_timeout(&self, timeout: Duration) -> Result<Pooled<M>, Error<M::Error>> {
338        self.acquire(Some(Instant::now() + timeout))
339    }
340
341    /// Borrow a resource without ever blocking.
342    ///
343    /// Returns a resource if one can be handed out immediately — an idle resource
344    /// is ready, or the pool has room to create one — and otherwise returns
345    /// [`Error::Timeout`] at once. Equivalent to
346    /// [`get_timeout(Duration::ZERO)`](Pool::get_timeout).
347    ///
348    /// # Errors
349    ///
350    /// - [`Error::Backend`] if the manager fails to create a resource.
351    /// - [`Error::Timeout`] if no resource is immediately available.
352    /// - [`Error::Closed`] if the pool has been closed.
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use pool_mod::{Error, Manager, Pool};
358    /// use std::convert::Infallible;
359    /// # struct M;
360    /// # impl Manager for M {
361    /// #   type Resource = u32; type Error = Infallible;
362    /// #   fn create(&self) -> Result<u32, Infallible> { Ok(0) }
363    /// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
364    /// # }
365    /// let pool = Pool::builder(M).max_size(1).build().expect("valid");
366    /// let first = pool.try_get().expect("room to create one");
367    /// // The only slot is taken, so the next try fails immediately.
368    /// assert!(matches!(pool.try_get(), Err(Error::Timeout)));
369    /// ```
370    pub fn try_get(&self) -> Result<Pooled<M>, Error<M::Error>> {
371        self.acquire(Some(Instant::now()))
372    }
373
374    fn acquire(&self, deadline: Option<Instant>) -> Result<Pooled<M>, Error<M::Error>> {
375        let (resource, created_at) = self.0.acquire(deadline)?;
376        Ok(Pooled::new(Arc::clone(&self.0), resource, created_at))
377    }
378
379    /// Take a snapshot of the pool's current occupancy.
380    ///
381    /// # Examples
382    ///
383    /// ```
384    /// use pool_mod::{Manager, Pool};
385    /// use std::convert::Infallible;
386    /// # struct M;
387    /// # impl Manager for M {
388    /// #   type Resource = (); type Error = Infallible;
389    /// #   fn create(&self) -> Result<(), Infallible> { Ok(()) }
390    /// #   fn recycle(&self, _r: &mut ()) -> Result<(), Infallible> { Ok(()) }
391    /// # }
392    /// let pool = Pool::builder(M).max_size(4).min_idle(1).build().expect("valid");
393    /// let status = pool.status();
394    /// assert_eq!(status.idle, 1);
395    /// assert_eq!(status.max_size, 4);
396    /// ```
397    pub fn status(&self) -> Status {
398        let state = lock(&self.0.state);
399        let idle = state.idle.len();
400        let size = state.total;
401        Status {
402            size,
403            idle,
404            in_use: size.saturating_sub(idle),
405            max_size: self.0.config.max_size,
406        }
407    }
408
409    /// Close the pool: discard every idle resource and reject all future
410    /// checkouts with [`Error::Closed`].
411    ///
412    /// Resources currently checked out are unaffected and are simply dropped
413    /// (not returned to the idle set) when their guards fall. Closing is
414    /// idempotent. Idle resources are dropped outside the pool's lock, so a slow
415    /// resource destructor does not block other threads.
416    ///
417    /// # Examples
418    ///
419    /// ```
420    /// use pool_mod::{Error, Manager, Pool};
421    /// use std::convert::Infallible;
422    /// # struct M;
423    /// # impl Manager for M {
424    /// #   type Resource = (); type Error = Infallible;
425    /// #   fn create(&self) -> Result<(), Infallible> { Ok(()) }
426    /// #   fn recycle(&self, _r: &mut ()) -> Result<(), Infallible> { Ok(()) }
427    /// # }
428    /// let pool = Pool::builder(M).max_size(2).min_idle(2).build().expect("valid");
429    /// pool.close();
430    /// assert!(pool.is_closed());
431    /// assert!(matches!(pool.get(), Err(Error::Closed)));
432    /// ```
433    pub fn close(&self) {
434        let mut state = lock(&self.0.state);
435        let drained = std::mem::take(&mut state.idle);
436        state.total = state.total.saturating_sub(drained.len());
437        state.closed = true;
438        drop(state);
439        self.0.available.notify_all();
440        drop(drained); // resource destructors run here, outside the lock
441    }
442
443    /// Report whether the pool has been [closed](Pool::close).
444    #[must_use]
445    pub fn is_closed(&self) -> bool {
446        lock(&self.0.state).closed
447    }
448}
449
450/// A fluent builder for a [`Pool`].
451///
452/// Created by [`Pool::builder`]. Each setter consumes and returns the builder, so
453/// calls chain; [`build`](Builder::build) validates the configuration and
454/// pre-creates the `min_idle` resources.
455///
456/// # Examples
457///
458/// ```
459/// use std::time::Duration;
460/// use pool_mod::{Manager, Pool};
461/// use std::convert::Infallible;
462/// # struct M;
463/// # impl Manager for M {
464/// #   type Resource = u32; type Error = Infallible;
465/// #   fn create(&self) -> Result<u32, Infallible> { Ok(0) }
466/// #   fn recycle(&self, _r: &mut u32) -> Result<(), Infallible> { Ok(()) }
467/// # }
468/// let pool = Pool::builder(M)
469///     .max_size(32)
470///     .min_idle(4)
471///     .idle_timeout(Some(Duration::from_secs(600)))
472///     .max_lifetime(Some(Duration::from_secs(3600)))
473///     .build()
474///     .expect("configuration is valid");
475/// assert_eq!(pool.status().idle, 4);
476/// ```
477#[must_use = "a Builder does nothing until `.build()` is called"]
478pub struct Builder<M: Manager> {
479    manager: M,
480    config: PoolConfig,
481}
482
483impl<M: Manager> Builder<M> {
484    /// Create a builder for `manager` seeded with the default configuration.
485    pub fn new(manager: M) -> Self {
486        Builder {
487            manager,
488            config: PoolConfig::default(),
489        }
490    }
491
492    /// Set the maximum number of resources the pool may own at once.
493    pub fn max_size(mut self, max_size: usize) -> Self {
494        self.config.max_size = max_size;
495        self
496    }
497
498    /// Set how many resources to create up front and keep ready.
499    pub fn min_idle(mut self, min_idle: usize) -> Self {
500        self.config.min_idle = min_idle;
501        self
502    }
503
504    /// Set how long [`Pool::get`] waits when the pool is saturated. `None` waits
505    /// indefinitely.
506    pub fn create_timeout(mut self, timeout: Option<Duration>) -> Self {
507        self.config.create_timeout = timeout;
508        self
509    }
510
511    /// Set the idle-expiry window. `None` disables idle expiry.
512    pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
513        self.config.idle_timeout = timeout;
514        self
515    }
516
517    /// Set the maximum resource lifetime. `None` disables lifetime expiry.
518    pub fn max_lifetime(mut self, lifetime: Option<Duration>) -> Self {
519        self.config.max_lifetime = lifetime;
520        self
521    }
522
523    /// Replace the entire configuration with `config`.
524    ///
525    /// Useful when the configuration is loaded from a file rather than assembled
526    /// setter by setter.
527    pub fn config(mut self, config: PoolConfig) -> Self {
528        self.config = config;
529        self
530    }
531
532    /// Validate the configuration, build the pool, and pre-create `min_idle`
533    /// resources.
534    ///
535    /// # Errors
536    ///
537    /// - [`Error::InvalidConfig`] if `max_size` is zero or `min_idle` exceeds
538    ///   `max_size`.
539    /// - [`Error::Backend`] if creating one of the `min_idle` resources fails;
540    ///   any already-created resources are dropped before returning.
541    ///
542    /// # Examples
543    ///
544    /// ```
545    /// use pool_mod::{Error, Manager, Pool};
546    /// use std::convert::Infallible;
547    /// # struct M;
548    /// # impl Manager for M {
549    /// #   type Resource = (); type Error = Infallible;
550    /// #   fn create(&self) -> Result<(), Infallible> { Ok(()) }
551    /// #   fn recycle(&self, _r: &mut ()) -> Result<(), Infallible> { Ok(()) }
552    /// # }
553    /// let invalid = Pool::builder(M).max_size(0).build();
554    /// assert!(matches!(invalid, Err(Error::InvalidConfig(_))));
555    /// ```
556    pub fn build(self) -> Result<Pool<M>, Error<M::Error>> {
557        if self.config.max_size == 0 {
558            return Err(Error::InvalidConfig("max_size must be at least 1"));
559        }
560        if self.config.min_idle > self.config.max_size {
561            return Err(Error::InvalidConfig("min_idle must not exceed max_size"));
562        }
563
564        let pool = Pool(Arc::new(PoolInner {
565            manager: self.manager,
566            config: self.config,
567            state: Mutex::new(State {
568                idle: VecDeque::with_capacity(self.config.max_size),
569                total: 0,
570                closed: false,
571            }),
572            available: Condvar::new(),
573        }));
574
575        for _ in 0..pool.0.config.min_idle {
576            match pool.0.manager.create() {
577                Ok(resource) => {
578                    let now = Instant::now();
579                    let mut state = lock(&pool.0.state);
580                    state.idle.push_back(Idle {
581                        resource,
582                        created_at: now,
583                        last_used: now,
584                    });
585                    state.total += 1;
586                }
587                Err(source) => return Err(Error::Backend(source)),
588            }
589        }
590
591        Ok(pool)
592    }
593}
594
595#[cfg(test)]
596#[allow(clippy::unwrap_used, clippy::expect_used)]
597mod tests {
598    use super::*;
599    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
600
601    #[derive(Debug, PartialEq, Eq)]
602    struct TestError(&'static str);
603
604    impl std::fmt::Display for TestError {
605        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606            f.write_str(self.0)
607        }
608    }
609
610    impl std::error::Error for TestError {}
611
612    /// A manager whose behaviour is steerable through atomics so individual
613    /// lifecycle paths can be exercised deterministically.
614    struct Steerable {
615        created: AtomicUsize,
616        recycled: AtomicUsize,
617        validated: AtomicUsize,
618        create_fails: AtomicBool,
619        recycle_fails: AtomicBool,
620        valid: AtomicBool,
621    }
622
623    impl Steerable {
624        fn new() -> Self {
625            Steerable {
626                created: AtomicUsize::new(0),
627                recycled: AtomicUsize::new(0),
628                validated: AtomicUsize::new(0),
629                create_fails: AtomicBool::new(false),
630                recycle_fails: AtomicBool::new(false),
631                valid: AtomicBool::new(true),
632            }
633        }
634    }
635
636    impl Manager for Steerable {
637        type Resource = usize;
638        type Error = TestError;
639
640        fn create(&self) -> Result<usize, TestError> {
641            if self.create_fails.load(Ordering::SeqCst) {
642                return Err(TestError("create failed"));
643            }
644            Ok(self.created.fetch_add(1, Ordering::SeqCst))
645        }
646
647        fn recycle(&self, _resource: &mut usize) -> Result<(), TestError> {
648            let _ = self.recycled.fetch_add(1, Ordering::SeqCst);
649            if self.recycle_fails.load(Ordering::SeqCst) {
650                return Err(TestError("recycle failed"));
651            }
652            Ok(())
653        }
654
655        fn validate(&self, _resource: &mut usize) -> bool {
656            let _ = self.validated.fetch_add(1, Ordering::SeqCst);
657            self.valid.load(Ordering::SeqCst)
658        }
659    }
660
661    fn pool(builder: impl FnOnce(Builder<Steerable>) -> Builder<Steerable>) -> Pool<Steerable> {
662        builder(Pool::builder(Steerable::new())).build().unwrap()
663    }
664
665    #[test]
666    fn test_build_min_idle_precreates_resources() {
667        let p = pool(|b| b.max_size(4).min_idle(2));
668        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 2);
669        let status = p.status();
670        assert_eq!(status.idle, 2);
671        assert_eq!(status.size, 2);
672        assert_eq!(status.in_use, 0);
673    }
674
675    #[test]
676    fn test_get_then_drop_reuses_same_resource() {
677        let p = pool(|b| b.max_size(4));
678        {
679            let first = p.get().unwrap();
680            assert_eq!(*first, 0);
681        }
682        let second = p.get().unwrap();
683        assert_eq!(*second, 0); // same resource id, reused
684        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 1);
685        assert_eq!(p.0.manager.recycled.load(Ordering::SeqCst), 1);
686    }
687
688    #[test]
689    fn test_in_use_tracks_outstanding_guards() {
690        let p = pool(|b| b.max_size(2));
691        let a = p.get().unwrap();
692        let b = p.get().unwrap();
693        assert_eq!(p.status().in_use, 2);
694        assert_eq!(p.status().idle, 0);
695        drop(a);
696        drop(b);
697        assert_eq!(p.status().in_use, 0);
698        assert_eq!(p.status().idle, 2);
699    }
700
701    #[test]
702    fn test_saturated_pool_times_out() {
703        let p = pool(|b| b.max_size(1));
704        let _held = p.get().unwrap();
705        let result = p.get_timeout(Duration::ZERO);
706        assert!(matches!(result, Err(Error::Timeout)));
707    }
708
709    #[test]
710    fn test_invalid_resource_is_discarded_and_replaced() {
711        let p = pool(|b| b.max_size(4).min_idle(1));
712        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 1);
713        p.0.manager.valid.store(false, Ordering::SeqCst);
714
715        // The single idle resource fails validation, so it is dropped and a fresh
716        // one is created. (The fresh resource is not itself re-validated.)
717        let resource = p.get().unwrap();
718        assert_eq!(*resource, 1);
719        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 2);
720        assert!(p.0.manager.validated.load(Ordering::SeqCst) >= 1);
721    }
722
723    #[test]
724    fn test_max_lifetime_forces_replacement() {
725        let p = pool(|b| b.max_size(4).min_idle(1).max_lifetime(Some(Duration::ZERO)));
726        // Zero lifetime means any idle resource is always too old on checkout.
727        let resource = p.get().unwrap();
728        assert_eq!(*resource, 1);
729        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 2);
730    }
731
732    #[test]
733    fn test_idle_timeout_forces_replacement() {
734        let p = pool(|b| b.max_size(4).min_idle(1).idle_timeout(Some(Duration::ZERO)));
735        let resource = p.get().unwrap();
736        assert_eq!(*resource, 1);
737        assert_eq!(p.0.manager.created.load(Ordering::SeqCst), 2);
738    }
739
740    #[test]
741    fn test_recycle_failure_drops_resource() {
742        let p = pool(|b| b.max_size(2));
743        p.0.manager.recycle_fails.store(true, Ordering::SeqCst);
744        {
745            let _resource = p.get().unwrap();
746            assert_eq!(p.status().size, 1);
747        }
748        // Recycle failed on return, so the resource was discarded, not pooled.
749        assert_eq!(p.status().size, 0);
750        assert_eq!(p.status().idle, 0);
751    }
752
753    #[test]
754    fn test_create_failure_surfaces_and_frees_slot() {
755        let p = pool(|b| b.max_size(2));
756        p.0.manager.create_fails.store(true, Ordering::SeqCst);
757        let result = p.get();
758        assert!(matches!(
759            result,
760            Err(Error::Backend(TestError("create failed")))
761        ));
762        // The reserved slot was released, so the pool did not shrink.
763        assert_eq!(p.status().size, 0);
764    }
765
766    #[test]
767    fn test_closed_pool_rejects_checkout() {
768        let p = pool(|b| b.max_size(2).min_idle(1));
769        p.close();
770        assert!(p.is_closed());
771        assert!(matches!(p.get(), Err(Error::Closed)));
772        assert_eq!(p.status().idle, 0); // idle resources were dropped on close
773    }
774
775    #[test]
776    fn test_close_is_idempotent() {
777        let p = pool(|b| b.max_size(2).min_idle(2));
778        p.close();
779        p.close();
780        assert!(p.is_closed());
781    }
782
783    #[test]
784    fn test_build_rejects_zero_max_size() {
785        let result = Pool::builder(Steerable::new()).max_size(0).build();
786        assert!(matches!(result, Err(Error::InvalidConfig(_))));
787    }
788
789    #[test]
790    fn test_build_rejects_min_idle_above_max_size() {
791        let result = Pool::builder(Steerable::new())
792            .max_size(2)
793            .min_idle(3)
794            .build();
795        assert!(matches!(result, Err(Error::InvalidConfig(_))));
796    }
797
798    #[test]
799    fn test_try_get_does_not_block_when_saturated() {
800        let p = pool(|b| b.max_size(1));
801        let _held = p.try_get().unwrap();
802        assert!(matches!(p.try_get(), Err(Error::Timeout)));
803    }
804
805    #[test]
806    fn test_clone_shares_one_pool() {
807        let p = pool(|b| b.max_size(1));
808        let clone = p.clone();
809        let _held = p.get().unwrap();
810        // The clone sees the same exhausted pool.
811        assert!(matches!(
812            clone.get_timeout(Duration::ZERO),
813            Err(Error::Timeout)
814        ));
815    }
816}