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}