Skip to main content

compio_pool/
pool.rs

1//! The pool handle, the acquire path, and the per-shard reaper.
2
3use std::{
4    any::Any,
5    cell::RefCell,
6    collections::HashMap,
7    rc::{Rc, Weak},
8    sync::{
9        Arc,
10        atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
11    },
12    time::Instant,
13};
14
15use crate::{
16    config::Config,
17    error::Error,
18    exchange::{Exchange, NoExchange, Parked, Unparked},
19    guard::Pooled,
20    manage::Manage,
21    metrics::{Counters, Metrics},
22    shard::Shard,
23    slot::Slot,
24};
25
26static NEXT_POOL_ID: AtomicU64 = AtomicU64::new(0);
27
28/// Holds a shard's capacity claim across an `await`, giving it back if the
29/// `acquire` future is cancelled.
30///
31/// `acquire` claims budget with `Shard::try_reserve` *before* awaiting
32/// `Manage::connect`, `Exchange::unpark` or `Manage::recycle`, and `acquire`
33/// is itself cancellable - by the acquire timeout, or by the caller's own
34/// `select!`. Without this guard, a cancellation between the claim and the end
35/// of the await would leak that capacity permanently, and a shard would
36/// eventually sit at `max_size` holding no connections at all.
37struct Reserved<'a, M: Manage> {
38    shard: &'a Rc<Shard<M>>,
39    counters: &'a Counters,
40    /// True when a real connection is riding along, counted in `live`.
41    holds_conn: bool,
42    armed: bool,
43}
44
45impl<'a, M: Manage> Reserved<'a, M> {
46    /// Budget claimed, no connection behind it yet.
47    fn empty(shard: &'a Rc<Shard<M>>, counters: &'a Counters) -> Self {
48        Self {
49            shard,
50            counters,
51            holds_conn: false,
52            armed: true,
53        }
54    }
55
56    /// Budget claimed and a live connection is out of the free list.
57    fn holding(shard: &'a Rc<Shard<M>>, counters: &'a Counters) -> Self {
58        Self {
59            shard,
60            counters,
61            holds_conn: true,
62            armed: true,
63        }
64    }
65
66    /// The await completed; ownership has passed on.
67    fn disarm(mut self) {
68        self.armed = false;
69    }
70}
71
72impl<M: Manage> Drop for Reserved<'_, M> {
73    fn drop(&mut self) {
74        if !self.armed {
75            return;
76        }
77        if self.holds_conn {
78            // The connection is dropped by the cancelled future without a
79            // graceful `Manage::disconnect`; there is no way to await one here.
80            Counters::dec(&self.counters.live);
81            Counters::inc(&self.counters.closed);
82        }
83        self.shard.release();
84    }
85}
86
87thread_local! {
88    /// Every pool's shard for *this* thread, keyed by pool id.
89    ///
90    /// This is what makes `Pool` `Send + Sync` while `M::Connection` is not:
91    /// the `Arc<Inner>` holds no connections at all. They live here, and a
92    /// value in a thread-local is by construction only reachable from its own
93    /// thread. At thread exit the map drops, closing that thread's connections
94    /// on the thread whose compio driver owns them.
95    static SHARDS: RefCell<HashMap<u64, Box<dyn Any>>> = RefCell::new(HashMap::new());
96}
97
98struct Inner<M: Manage, X: Exchange<M>> {
99    id: u64,
100    manager: Arc<M>,
101    config: Config,
102    exchange: X,
103    counters: Arc<Counters>,
104    generation: AtomicU64,
105    closed: AtomicBool,
106}
107
108/// A sharded connection pool for `compio`.
109///
110/// Cheap to clone and `Send + Sync`: clone it onto every compio thread. Each
111/// thread transparently gets its own shard, so the acquire fast path touches no
112/// atomics and no locks.
113///
114/// The `X` parameter selects the cross-thread strategy. The default,
115/// [`NoExchange`], keeps threads fully independent. Swap in a
116/// [`Reservoir`](crate::Reservoir) — which requires [`Detach`](crate::Detach) —
117/// to let idle connections migrate to whichever thread needs them.
118pub struct Pool<M: Manage, X: Exchange<M> = NoExchange> {
119    inner: Arc<Inner<M, X>>,
120}
121
122impl<M: Manage, X: Exchange<M>> Clone for Pool<M, X> {
123    fn clone(&self) -> Self {
124        Self {
125            inner: self.inner.clone(),
126        }
127    }
128}
129
130impl<M: Manage> Pool<M> {
131    /// Builds a pool with no cross-thread sharing.
132    pub fn new(manager: M, config: Config) -> Self {
133        Self::with_exchange(manager, config, NoExchange)
134    }
135
136    /// Starts a [`Builder`].
137    pub fn builder(manager: M) -> Builder<M, NoExchange> {
138        Builder {
139            manager,
140            config: Config::default(),
141            exchange: NoExchange,
142        }
143    }
144}
145
146impl<M: Manage, X: Exchange<M>> Pool<M, X> {
147    /// Builds a pool with an explicit cross-thread [`Exchange`].
148    pub fn with_exchange(manager: M, config: Config, exchange: X) -> Self {
149        Self {
150            inner: Arc::new(Inner {
151                id: NEXT_POOL_ID.fetch_add(1, Relaxed),
152                manager: Arc::new(manager),
153                config,
154                exchange,
155                counters: Arc::new(Counters::default()),
156                generation: AtomicU64::new(0),
157                closed: AtomicBool::new(false),
158            }),
159        }
160    }
161
162    /// The manager, for callers that need to reach configuration through it.
163    pub fn manager(&self) -> &M {
164        self.inner.manager.as_ref()
165    }
166
167    /// The configuration this pool was built with.
168    pub fn config(&self) -> &Config {
169        &self.inner.config
170    }
171
172    /// Connections owned by *this thread's* shard, idle plus checked out.
173    pub fn local_size(&self) -> usize {
174        self.shard().size()
175    }
176
177    /// Idle connections sitting in *this thread's* shard.
178    pub fn local_idle(&self) -> usize {
179        self.shard().idle_len()
180    }
181
182    /// A snapshot of pool activity.
183    pub fn metrics(&self) -> Metrics {
184        self.inner.counters.snapshot(self.inner.exchange.parked())
185    }
186
187    /// Retires every connection created before this call.
188    ///
189    /// Live checkouts keep working and are closed when returned. Use after a
190    /// credential rotation or a failover, where existing sockets point at the
191    /// wrong place but the pool itself is still wanted.
192    pub fn invalidate(&self) {
193        self.inner.generation.fetch_add(1, Relaxed);
194        self.inner.exchange.clear();
195    }
196
197    /// Closes the pool.
198    ///
199    /// Subsequent [`acquire`](Pool::acquire) calls fail with
200    /// [`Error::Closed`]. This thread's idle connections are closed
201    /// immediately; other threads' are closed when they next touch the pool or
202    /// when their thread exits.
203    pub fn close(&self) {
204        self.inner.closed.store(true, Relaxed);
205        self.inner.exchange.clear();
206        let shard = self.shard();
207        for slot in shard.drain_all() {
208            self.destroy(&shard, slot);
209        }
210    }
211
212    /// Whether [`close`](Pool::close) has been called.
213    pub fn is_closed(&self) -> bool {
214        self.inner.closed.load(Relaxed)
215    }
216
217    /// Opens connections on the current thread up to `min_idle`.
218    ///
219    /// Call this once per compio thread at startup so the first real request
220    /// does not pay for a handshake.
221    pub async fn warm(&self) -> Result<(), Error<M::Error>> {
222        let shard = self.shard();
223        let target = self.inner.config.effective_min_idle();
224        while shard.idle_len() < target {
225            if !shard.try_reserve(self.inner.config.max_size) {
226                break;
227            }
228            match self.inner.manager.connect().await {
229                Ok(conn) => {
230                    Counters::inc(&self.inner.counters.created);
231                    Counters::inc(&self.inner.counters.live);
232                    let generation = self.inner.generation.load(Relaxed);
233                    shard.push_idle(Slot::new(conn, generation));
234                }
235                Err(e) => {
236                    shard.release();
237                    return Err(Error::Backend(e));
238                }
239            }
240        }
241        Ok(())
242    }
243
244    /// Checks out a connection, waiting if the shard is at `max_size`.
245    ///
246    /// Must be called on a compio runtime thread. Honours
247    /// [`Config::acquire_timeout`].
248    pub async fn acquire(&self) -> Result<Pooled<M, X>, Error<M::Error>> {
249        match self.inner.config.acquire_timeout {
250            Some(limit) => match compio::time::timeout(limit, self.acquire_inner()).await {
251                Ok(result) => result,
252                Err(_elapsed) => {
253                    Counters::inc(&self.inner.counters.timeouts);
254                    Err(Error::Timeout)
255                }
256            },
257            None => self.acquire_inner().await,
258        }
259    }
260
261    async fn acquire_inner(&self) -> Result<Pooled<M, X>, Error<M::Error>> {
262        let shard = self.shard();
263        loop {
264            if self.inner.closed.load(Relaxed) {
265                return Err(Error::Closed);
266            }
267            let generation = self.inner.generation.load(Relaxed);
268
269            // 1. Warmest idle connection on this thread. No atomics, no lock.
270            while let Some(slot) = shard.pop_idle() {
271                if slot.is_expired(&self.inner.config, generation) {
272                    self.destroy(&shard, slot);
273                    continue;
274                }
275                let Slot { mut conn, meta } = slot;
276                // The connection is now out of the free list but not yet in a
277                // guard: if this await is cancelled, `reserved` accounts for it.
278                let reserved = Reserved::holding(&shard, &self.inner.counters);
279                let recycled = self.inner.manager.recycle(&mut conn, &meta).await;
280                reserved.disarm();
281                match recycled {
282                    Ok(()) => {
283                        Counters::inc(&self.inner.counters.acquires);
284                        return Ok(Pooled::new(Slot { conn, meta }, shard, self.clone()));
285                    }
286                    Err(_) => {
287                        Counters::inc(&self.inner.counters.recycle_failures);
288                        self.destroy_conn(&shard, conn);
289                    }
290                }
291            }
292
293            // 2. Room for one more on this thread? Claim the budget first, so
294            //    that unpark and connect are both covered by one reservation.
295            if shard.try_reserve(self.inner.config.max_size) {
296                // Both awaits below are cancellable; `reserved` hands the
297                // budget back if either is dropped part-way through.
298                let reserved = Reserved::empty(&shard, &self.inner.counters);
299                let unparked = self.inner.exchange.unpark().await;
300                reserved.disarm();
301
302                match unparked {
303                    // Someone else's idle socket, re-wrapped in this thread's
304                    // runtime. Cheaper than a handshake, so it is tried first.
305                    Unparked::Claimed(conn, meta) => {
306                        Counters::inc(&self.inner.counters.unparked);
307                        Counters::inc(&self.inner.counters.live);
308                        Counters::inc(&self.inner.counters.acquires);
309                        return Ok(Pooled::new(Slot { conn, meta }, shard, self.clone()));
310                    }
311                    // It was popped but could not be reattached, so it is gone.
312                    // Parking already decremented `live`; balance `created`
313                    // here and fall through to dialling a replacement.
314                    Unparked::Lost => Counters::inc(&self.inner.counters.closed),
315                    Unparked::Empty => {}
316                }
317                let reserved = Reserved::empty(&shard, &self.inner.counters);
318                let connected = self.inner.manager.connect().await;
319                reserved.disarm();
320                return match connected {
321                    Ok(conn) => {
322                        Counters::inc(&self.inner.counters.created);
323                        Counters::inc(&self.inner.counters.live);
324                        Counters::inc(&self.inner.counters.acquires);
325                        Ok(Pooled::new(
326                            Slot::new(conn, generation),
327                            shard,
328                            self.clone(),
329                        ))
330                    }
331                    Err(e) => {
332                        shard.release();
333                        Err(Error::Backend(e))
334                    }
335                };
336            }
337
338            // 3. At capacity. Wait for one of *this thread's* checkouts to come
339            //    back; they always will, so this cannot deadlock.
340            Counters::inc(&self.inner.counters.waits);
341            shard.wait().await;
342        }
343    }
344
345    /// Return path for [`Pooled::drop`].
346    pub(crate) fn release(
347        &self,
348        shard: &Rc<Shard<M>>,
349        mut slot: Slot<M::Connection>,
350        poisoned: bool,
351    ) {
352        slot.meta.uses += 1;
353        slot.meta.last_used = Instant::now();
354
355        if poisoned {
356            Counters::inc(&self.inner.counters.poisoned);
357            return self.destroy(shard, slot);
358        }
359        if self.inner.closed.load(Relaxed) {
360            return self.destroy(shard, slot);
361        }
362        let generation = self.inner.generation.load(Relaxed);
363        if slot.is_expired(&self.inner.config, generation) {
364            return self.destroy(shard, slot);
365        }
366
367        // Surplus idle connections are offered to other threads rather than
368        // held here until they age out. Only surplus: keep `min_idle` local,
369        // and never give one away while someone on this thread is waiting.
370        if !shard.has_waiters() && shard.idle_len() >= self.inner.config.effective_min_idle() {
371            let Slot { conn, meta } = slot;
372            match self.inner.exchange.park(conn, meta) {
373                Parked::Accepted => {
374                    // The reservoir took it; it is no longer this shard's.
375                    Counters::dec(&self.inner.counters.live);
376                    shard.release();
377                    return;
378                }
379                // `detach` found an operation still in flight and dropped it
380                // rather than hand a busy handle to another driver.
381                Parked::Destroyed => {
382                    Counters::dec(&self.inner.counters.live);
383                    Counters::inc(&self.inner.counters.closed);
384                    shard.release();
385                    return;
386                }
387                Parked::Refused(conn, meta) => slot = Slot { conn, meta },
388            }
389        }
390
391        shard.push_idle(slot);
392    }
393
394    /// Drops a connection out of the pool without closing it, for
395    /// [`Pooled::take`].
396    pub(crate) fn forget(&self, shard: &Rc<Shard<M>>) {
397        Counters::dec(&self.inner.counters.live);
398        shard.release();
399    }
400
401    fn destroy(&self, shard: &Rc<Shard<M>>, slot: Slot<M::Connection>) {
402        self.destroy_conn(shard, slot.conn);
403    }
404
405    fn destroy_conn(&self, shard: &Rc<Shard<M>>, conn: M::Connection) {
406        self.inner.manager.disconnect(conn);
407        Counters::dec(&self.inner.counters.live);
408        Counters::inc(&self.inner.counters.closed);
409        shard.release();
410    }
411
412    /// This thread's shard, creating it (and its reaper) on first touch.
413    fn shard(&self) -> Rc<Shard<M>> {
414        let id = self.inner.id;
415
416        let (shard, created) = SHARDS.with(|shards| {
417            let mut shards = shards.borrow_mut();
418            match shards.get(&id) {
419                Some(any) => (
420                    any.downcast_ref::<Rc<Shard<M>>>()
421                        .expect("pool id uniquely determines shard type")
422                        .clone(),
423                    false,
424                ),
425                None => {
426                    // Cloned here, not above: these are `Arc`s shared by every
427                    // thread, and bumping their refcounts on the hit path would
428                    // put two atomic RMWs on a contended cache line into every
429                    // single checkout.
430                    let shard = Rc::new(Shard::<M>::new(
431                        self.inner.counters.clone(),
432                        self.inner.manager.clone(),
433                    ));
434                    shards.insert(id, Box::new(shard.clone()));
435                    (shard, true)
436                }
437            }
438        });
439
440        // Spawn outside the `with` closure: the thread-local is no longer
441        // borrowed, so the reaper is free to reach for its own shard.
442        if created && self.inner.config.needs_reaper() {
443            self.spawn_reaper(&shard);
444        }
445        shard
446    }
447
448    fn spawn_reaper(&self, shard: &Rc<Shard<M>>) {
449        // Outside a compio runtime there is nothing to spawn onto. The pool
450        // still works; expired connections are then caught on acquire instead.
451        if compio::runtime::Runtime::try_with_current(|_| ()).is_err() {
452            return;
453        }
454        let pool = self.clone();
455        let shard = Rc::downgrade(shard);
456        compio::runtime::spawn(async move { pool.reap_loop(shard).await }).detach();
457    }
458
459    /// Enforces idle/lifetime limits and refills `min_idle` on one thread.
460    ///
461    /// Holds only a `Weak` to the shard, so it stops on its own once the
462    /// thread's shard is gone.
463    async fn reap_loop(self, shard: Weak<Shard<M>>) {
464        let interval = self.inner.config.reap_interval;
465        loop {
466            compio::time::sleep(interval).await;
467
468            let Some(shard) = shard.upgrade() else { return };
469            if self.inner.closed.load(Relaxed) {
470                for slot in shard.drain_all() {
471                    self.destroy(&shard, slot);
472                }
473                return;
474            }
475
476            let generation = self.inner.generation.load(Relaxed);
477            for slot in shard.drain_expired(&self.inner.config, generation) {
478                self.destroy(&shard, slot);
479            }
480
481            let target = self.inner.config.effective_min_idle();
482            while shard.idle_len() < target && !shard.has_waiters() {
483                if !shard.try_reserve(self.inner.config.max_size) {
484                    break;
485                }
486                match self.inner.manager.connect().await {
487                    Ok(conn) => {
488                        Counters::inc(&self.inner.counters.created);
489                        Counters::inc(&self.inner.counters.live);
490                        shard.push_idle(Slot::new(conn, generation));
491                    }
492                    Err(_) => {
493                        shard.release();
494                        break;
495                    }
496                }
497            }
498        }
499    }
500}
501
502impl<M: Manage, X: Exchange<M>> std::fmt::Debug for Pool<M, X> {
503    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504        f.debug_struct("Pool")
505            .field("id", &self.inner.id)
506            .field("closed", &self.inner.closed.load(Relaxed))
507            .field("metrics", &self.metrics())
508            .finish_non_exhaustive()
509    }
510}
511
512/// Fluent constructor for [`Pool`].
513pub struct Builder<M: Manage, X: Exchange<M>> {
514    manager: M,
515    config: Config,
516    exchange: X,
517}
518
519impl<M: Manage, X: Exchange<M>> Builder<M, X> {
520    /// Replaces the whole configuration.
521    pub fn config(mut self, config: Config) -> Self {
522        self.config = config;
523        self
524    }
525
526    /// Maximum live connections per runtime thread.
527    pub fn max_size(mut self, n: usize) -> Self {
528        self.config = self.config.clone().max_size(n);
529        self
530    }
531
532    /// Idle connections each shard keeps warm.
533    pub fn min_idle(mut self, n: usize) -> Self {
534        self.config = self.config.clone().min_idle(n);
535        self
536    }
537
538    /// How long `acquire` waits before giving up.
539    pub fn acquire_timeout(mut self, t: impl Into<Option<std::time::Duration>>) -> Self {
540        self.config = self.config.clone().acquire_timeout(t);
541        self
542    }
543
544    /// Hard age cap on a connection.
545    pub fn max_lifetime(mut self, t: impl Into<Option<std::time::Duration>>) -> Self {
546        self.config = self.config.clone().max_lifetime(t);
547        self
548    }
549
550    /// How long a connection may sit idle before being reaped.
551    pub fn idle_timeout(mut self, t: impl Into<Option<std::time::Duration>>) -> Self {
552        self.config = self.config.clone().idle_timeout(t);
553        self
554    }
555
556    /// Retire a connection after this many checkouts.
557    pub fn max_uses(mut self, n: impl Into<Option<u64>>) -> Self {
558        self.config = self.config.clone().max_uses(n);
559        self
560    }
561
562    /// Installs a cross-thread [`Exchange`], changing the pool's type.
563    pub fn exchange<Y: Exchange<M>>(self, exchange: Y) -> Builder<M, Y> {
564        Builder {
565            manager: self.manager,
566            config: self.config,
567            exchange,
568        }
569    }
570
571    /// Builds the pool.
572    pub fn build(self) -> Pool<M, X> {
573        Pool::with_exchange(self.manager, self.config, self.exchange)
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use crate::test_support::TestManager;
581
582    fn untimed() -> Config {
583        Config::new().max_lifetime(None).idle_timeout(None)
584    }
585
586    fn pool() -> Pool<TestManager> {
587        Pool::new(TestManager::new(), untimed())
588    }
589
590    // ---- Reserved: the cancellation-safety guard ----------------------------
591    //
592    // `acquire` claims shard budget before awaiting, and `acquire` is itself
593    // cancellable. These four cases are the whole contract.
594
595    /// A guard for a claim that has already been converted into a real checkout
596    /// must not touch anything when it goes out of scope.
597    #[test]
598    fn a_disarmed_empty_reservation_returns_nothing() {
599        let p = pool();
600        let shard = p.shard();
601        assert!(shard.try_reserve(4));
602
603        Reserved::empty(&shard, &p.inner.counters).disarm();
604
605        assert_eq!(shard.size(), 1, "the budget stayed claimed");
606        assert_eq!(p.metrics().closed, 0);
607    }
608
609    /// The point of the guard: a cancelled dial hands its budget back, or the
610    /// shard silently loses capacity until it sits at `max_size` holding nothing.
611    #[test]
612    fn a_dropped_empty_reservation_returns_the_budget() {
613        let p = pool();
614        let shard = p.shard();
615        assert!(shard.try_reserve(4));
616
617        drop(Reserved::empty(&shard, &p.inner.counters));
618
619        assert_eq!(shard.size(), 0);
620        let m = p.metrics();
621        assert_eq!(m.closed, 0, "there was no connection to close");
622        assert_eq!(m.live, 0);
623    }
624
625    #[test]
626    fn a_disarmed_holding_reservation_leaves_the_connection_counted() {
627        let p = pool();
628        let shard = p.shard();
629        assert!(shard.try_reserve(4));
630        Counters::inc(&p.inner.counters.live);
631
632        Reserved::holding(&shard, &p.inner.counters).disarm();
633
634        let m = p.metrics();
635        assert_eq!(m.live, 1, "the connection is still ours");
636        assert_eq!(m.closed, 0);
637        assert_eq!(shard.size(), 1);
638    }
639
640    /// A connection out of the free list but not yet in a guard is dropped by
641    /// the cancelled future, with no chance to await `disconnect`. It still has
642    /// to be accounted for.
643    #[test]
644    fn a_dropped_holding_reservation_counts_the_connection_closed() {
645        let p = pool();
646        let shard = p.shard();
647        assert!(shard.try_reserve(4));
648        Counters::inc(&p.inner.counters.live);
649
650        drop(Reserved::holding(&shard, &p.inner.counters));
651
652        let m = p.metrics();
653        assert_eq!(m.live, 0);
654        assert_eq!(m.closed, 1);
655        assert_eq!(shard.size(), 0, "and the budget comes back too");
656    }
657
658    // ---- The thread-local shard map -----------------------------------------
659
660    #[test]
661    fn a_shard_is_created_once_per_thread_and_then_reused() {
662        let p = pool();
663        let first = p.shard();
664        let second = p.shard();
665        assert!(
666            Rc::ptr_eq(&first, &second),
667            "the hit path must not rebuild the shard"
668        );
669        // A clone of the handle is the same pool, so the same shard.
670        assert!(Rc::ptr_eq(&first, &p.clone().shard()));
671    }
672
673    #[test]
674    fn two_pools_of_the_same_type_do_not_share_a_shard() {
675        let a = pool();
676        let b = pool();
677        assert!(
678            !Rc::ptr_eq(&a.shard(), &b.shard()),
679            "shards are keyed by pool id, not by connection type"
680        );
681    }
682
683    /// Outside a compio runtime there is nothing to spawn the reaper onto. The
684    /// pool must still be usable; expiry is then enforced on acquire instead.
685    #[test]
686    fn a_pool_with_a_reaper_config_works_without_a_runtime() {
687        let p = Pool::new(TestManager::new(), Config::new().min_idle(2));
688        assert!(p.config().needs_reaper());
689        assert_eq!(p.local_size(), 0);
690        assert_eq!(p.local_idle(), 0);
691    }
692
693    #[test]
694    fn a_pool_without_a_reaper_config_skips_the_spawn_entirely() {
695        let p = pool();
696        assert!(!p.config().needs_reaper());
697        assert_eq!(p.local_size(), 0);
698    }
699
700    // ---- Handle surface ------------------------------------------------------
701
702    #[test]
703    fn debug_reports_identity_state_and_metrics() {
704        let p = pool();
705        let s = format!("{p:?}");
706        assert!(s.contains("closed: false"), "got {s}");
707        assert!(s.contains("metrics"), "got {s}");
708
709        p.close();
710        assert!(format!("{p:?}").contains("closed: true"));
711    }
712
713    #[test]
714    fn pool_ids_are_unique() {
715        let a = pool();
716        let b = pool();
717        assert_ne!(a.inner.id, b.inner.id);
718    }
719
720    #[test]
721    fn destroy_closes_through_the_manager_and_frees_budget() {
722        let manager = TestManager::new();
723        let counts = manager.counts();
724        let p = Pool::new(manager, untimed());
725        let shard = p.shard();
726        assert!(shard.try_reserve(4));
727        Counters::inc(&p.inner.counters.live);
728
729        p.destroy(&shard, Slot::new(crate::test_support::TestConn::new(0), 0));
730
731        assert_eq!(counts.disconnected(), 1);
732        assert_eq!(shard.size(), 0);
733        let m = p.metrics();
734        assert_eq!((m.live, m.closed), (0, 1));
735    }
736
737    #[test]
738    fn forget_frees_budget_without_closing_anything() {
739        let manager = TestManager::new();
740        let counts = manager.counts();
741        let p = Pool::new(manager, untimed());
742        let shard = p.shard();
743        assert!(shard.try_reserve(4));
744        Counters::inc(&p.inner.counters.live);
745
746        p.forget(&shard);
747
748        assert_eq!(counts.disconnected(), 0, "the caller owns it now");
749        assert_eq!(shard.size(), 0, "but the shard may open a replacement");
750        let m = p.metrics();
751        assert_eq!((m.live, m.closed), (0, 0));
752    }
753}