Skip to main content

cratefield_adapter_postgres/
registry.rs

1//! [`PoolRegistry`]: one connection pool per tenant, opened on first use
2//! and closed when idle (TENANT-ROUTING.md §4, issue #32).
3//!
4//! **Lazy, not eager.** Boot reconciliation already touches every tenant
5//! database; holding those connections afterwards would make a replica's
6//! idle cost scale with the tenant count rather than with its traffic.
7//! The registry is a cache, not an inventory.
8//!
9//! **Two caps, because one is not enough.** `sqlx` caps *per pool*, which
10//! is exactly the per-tenant boundary — a pool belongs to one tenant, and
11//! that cap is what stops one busy tenant taking every connection on a
12//! shared cluster. It cannot express a total across pools, which is the
13//! failure the per-tenant cap does not prevent: the process taking every
14//! connection on the *server*. So the total is a semaphore this registry
15//! holds, acquired around every `execute`, `query` and `batch` — and for
16//! a `batch` the permit spans the whole batch, because `batch` holds its
17//! connection across `pool.begin()`.
18//!
19//! Without that semaphore `TENANT_POOL_TOTAL` would be a config key that
20//! does nothing, which is worse than not having it: it reads as
21//! protection.
22
23use std::collections::HashMap;
24use std::sync::Arc;
25use std::time::Duration;
26
27use async_trait::async_trait;
28use cratefield_core::{
29    Clock, Database, DbError, Rows, Statement, Tenant, TenantDatabases, TenantDbError,
30};
31
32use crate::{PoolLimits, Postgres};
33
34/// Where a tenant's connection string comes from.
35///
36/// A trait rather than a hard-wired lookup, because the answer is not
37/// settled: the control database's registry carries a `dsn` column, and
38/// the secrets design (#39) puts it at `tenants/<id>/db_ref` behind a
39/// `HarnessOnly` proof. Both are defensible and the choice is a
40/// deployment's, so the registry takes whichever and never decides.
41///
42/// Whatever supplies it, the DSN stops here: [`PoolRegistry`] is the only
43/// thing that holds one, and it maps a connect failure to
44/// [`TenantDbError::Unreachable`] before the URL can reach a log line or
45/// a response body.
46#[async_trait]
47pub trait TenantDsns: Send + Sync {
48    /// The connection string for `tenant`, or `None` if it has none.
49    ///
50    /// # Errors
51    ///
52    /// Any lookup failure, as a message that must not contain the DSN.
53    async fn dsn(&self, tenant: &str) -> Result<Option<String>, String>;
54}
55
56/// The default total across every tenant pool in this process.
57///
58/// A guess pending a measurement against a real cluster's
59/// `max_connections` and the replica count — recorded as one in
60/// TENANT-ROUTING.md §12 rather than presented as a finding. Note that
61/// boot reconciliation runs 8 tenants concurrently and the control
62/// database has its own pool; both draw on the same server limit and
63/// neither is inside this cap.
64pub const TENANT_POOL_TOTAL: usize = 64;
65
66/// How long a pool with no checkout survives before it is closed.
67pub const TENANT_POOL_IDLE: Duration = Duration::from_secs(300);
68
69struct Cached {
70    db: Arc<CappedDb>,
71    /// Unix seconds of the last handout. Coarse on purpose: this decides
72    /// eviction, not correctness.
73    last_used: i64,
74}
75
76/// How a pool is opened. Always [`Postgres::connect_with`] in production;
77/// a field rather than a bare call so a test can count opens and race
78/// them without a server behind them — the race this exists for is
79/// between two opens, so it is only observable if a test can perform
80/// one.
81type Open = Arc<
82    dyn Fn(String, PoolLimits) -> cratefield_core::BoxFuture<'static, Result<Postgres, DbError>>
83        + Send
84        + Sync,
85>;
86
87/// Pools per tenant, with both caps applied.
88pub struct PoolRegistry {
89    dsns: Arc<dyn TenantDsns>,
90    limits: PoolLimits,
91    idle: Duration,
92    clock: Arc<dyn Clock>,
93    total: Arc<tokio::sync::Semaphore>,
94    /// Always [`Postgres::connect_with`] outside tests.
95    open: Open,
96    // Not request state (ADR 0007): deployment-scoped, outlives every
97    // request, and shared by all of them.
98    #[allow(clippy::disallowed_types)]
99    pools: std::sync::Mutex<HashMap<String, Cached>>,
100    /// One async lock per key, held only while that key's pool is being
101    /// opened — the single-flight that stops two cold callers opening
102    /// one pool each and the second `insert` orphaning the first. A
103    /// lock is per key, not one for the registry: opening tenant A must
104    /// not hold up tenant B.
105    #[allow(clippy::disallowed_types)]
106    inflight: std::sync::Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
107}
108
109impl PoolRegistry {
110    /// A registry with the default caps.
111    #[must_use]
112    pub fn new(dsns: Arc<dyn TenantDsns>, clock: Arc<dyn Clock>) -> Self {
113        Self::with_limits(
114            dsns,
115            clock,
116            PoolLimits::default(),
117            TENANT_POOL_TOTAL,
118            TENANT_POOL_IDLE,
119        )
120    }
121
122    /// A registry with explicit caps.
123    // The pool map is deployment-scoped shared state that outlives every
124    // request, not the ambient request state ADR 0007 bans — the same
125    // category as the sidecar probe cache in core's harness.
126    #[allow(clippy::disallowed_types)]
127    #[must_use]
128    pub fn with_limits(
129        dsns: Arc<dyn TenantDsns>,
130        clock: Arc<dyn Clock>,
131        limits: PoolLimits,
132        total: usize,
133        idle: Duration,
134    ) -> Self {
135        Self {
136            dsns,
137            limits,
138            idle,
139            clock,
140            total: Arc::new(tokio::sync::Semaphore::new(total.max(1))),
141            open: Arc::new(|dsn: String, limits: PoolLimits| {
142                Box::pin(async move { Postgres::connect_with(&dsn, limits).await })
143            }),
144            pools: std::sync::Mutex::new(HashMap::new()),
145            inflight: std::sync::Mutex::new(HashMap::new()),
146        }
147    }
148
149    /// A registry whose pools come from `open` instead of a real server.
150    /// Test-only: the single-flight below is only observable if a test
151    /// can count opens and race them, which needs a connect the test
152    /// controls.
153    #[cfg(test)]
154    fn with_open(mut self, open: Open) -> Self {
155        self.open = open;
156        self
157    }
158
159    /// How many pools are open right now. For tests and `/__health`;
160    /// never a correctness signal — TENANT-ROUTING.md §12 makes the
161    /// *registry row* authoritative, precisely so that pool presence
162    /// cannot be mistaken for tenant health.
163    ///
164    /// # Panics
165    ///
166    /// If the pool map's lock was poisoned by a panic in another thread.
167    /// Poisoning here means a previous handout panicked mid-map, and
168    /// continuing on a map in an unknown state is worse than stopping.
169    #[must_use]
170    pub fn open_pools(&self) -> usize {
171        self.pools.lock().expect("pool lock").len()
172    }
173
174    /// Closes every pool whose last handout is older than the idle
175    /// window. Called on each handout, so a busy deployment sweeps itself
176    /// and an idle one costs nothing.
177    fn evict_idle(&self, now: i64) -> Vec<Arc<CappedDb>> {
178        let cutoff = now.saturating_sub(i64::try_from(self.idle.as_secs()).unwrap_or(i64::MAX));
179        let mut pools = self.pools.lock().expect("pool lock");
180        let stale: Vec<String> = pools
181            .iter()
182            .filter(|(_, cached)| cached.last_used < cutoff)
183            .map(|(tenant, _)| tenant.clone())
184            .collect();
185        // Returned rather than closed under the lock: `close` awaits
186        // in-flight queries, and holding a std Mutex across an await is
187        // the deadlock this avoids by construction.
188        stale
189            .into_iter()
190            .filter_map(|tenant| pools.remove(&tenant).map(|cached| cached.db))
191            .collect()
192    }
193
194    fn now_secs(&self) -> i64 {
195        self.clock.now().unix_timestamp()
196    }
197}
198
199/// Why a keyed pool could not be produced. Internal: every variant is a
200/// [`TenantDbError::Unreachable`] to a caller, but which one it was is
201/// worth having in a log line.
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub(crate) enum PoolError {
204    /// The source has no connection string under that key.
205    NoDsn,
206    /// The source itself failed.
207    Lookup,
208    /// The server refused or did not answer.
209    Connect,
210}
211
212impl PoolRegistry {
213    /// The pool for `key`, opened on first use and reused after.
214    ///
215    /// **Keyed, not tenanted.** Lazy open, idle eviction and both caps are
216    /// pool bookkeeping and have nothing to do with tenancy, so they live
217    /// here and are tested by string. What makes a key a *tenant* is the
218    /// [`TenantDatabases`] impl below, which is short enough to be correct
219    /// by inspection — the split is deliberate, so that the part with the
220    /// interesting behaviour does not need a `Tenant` to test and core's
221    /// private constructor can stay private.
222    pub(crate) async fn pool_for(&self, key: &str) -> Result<Arc<CappedDb>, PoolError> {
223        let now = self.now_secs();
224
225        for stale in self.evict_idle(now) {
226            let _ = stale.inner.close().await;
227        }
228
229        if let Some(cached) = self.pools.lock().expect("pool lock").get_mut(key) {
230            cached.last_used = now;
231            return Ok(Arc::clone(&cached.db));
232        }
233
234        // Cold. One opener per key, so the check above and the insert
235        // below are not a check-then-act across two awaits: without
236        // this, two callers that both missed open two pools, and the
237        // second `insert` replaced the first — closing it out from
238        // under whoever still held it, and putting one tenant on two
239        // pools for as long as the orphaned one stayed alive.
240        let flight = self.flight(key);
241        let _opening = flight.lock().await;
242
243        // Re-check under the flight: whoever held it may have opened
244        // this key while we waited, and opening a second pool for a key
245        // that now has one is the race this lock exists to stop.
246        if let Some(cached) = self.pools.lock().expect("pool lock").get_mut(key) {
247            cached.last_used = now;
248            return Ok(Arc::clone(&cached.db));
249        }
250
251        // The DSN exists only inside this block, and the error it could be
252        // folded into is discarded rather than formatted.
253        let dsn = self
254            .dsns
255            .dsn(key)
256            .await
257            .map_err(|_| PoolError::Lookup)?
258            .ok_or(PoolError::NoDsn)?;
259        let pool = (self.open)(dsn.clone(), self.limits)
260            .await
261            .map_err(|_| PoolError::Connect)?;
262        drop(dsn);
263
264        let db = Arc::new(CappedDb {
265            inner: pool,
266            total: Arc::clone(&self.total),
267            acquire_timeout: self.limits.acquire_timeout,
268            clock: Arc::clone(&self.clock),
269            key: key.to_owned(),
270        });
271        self.pools.lock().expect("pool lock").insert(
272            key.to_owned(),
273            Cached {
274                db: Arc::clone(&db),
275                last_used: now,
276            },
277        );
278        Ok(db)
279    }
280
281    /// The lock that serialises opens of `key`.
282    ///
283    /// Entries are swept on every open, keeping only a key somebody is
284    /// still opening: a registry outlives its tenants and must not
285    /// accumulate a lock for every tenant it ever served. Sweeping on
286    /// `Arc::strong_count` is what makes that safe — the map holds one
287    /// reference, so a count of one means nobody is opening it or
288    /// waiting to, while a caller still holding a clone keeps its lock
289    /// alive and keeps waiting on it.
290    fn flight(&self, key: &str) -> Arc<tokio::sync::Mutex<()>> {
291        let mut inflight = self.inflight.lock().expect("pool lock");
292        inflight.retain(|_, flight| Arc::strong_count(flight) > 1);
293        Arc::clone(
294            inflight
295                .entry(key.to_owned())
296                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
297        )
298    }
299}
300
301#[async_trait]
302impl TenantDatabases for PoolRegistry {
303    /// The tenant half, and all of it: a tenant's key is its id.
304    ///
305    /// Every way the lookup can miss is one answer to the caller — the
306    /// tenant's database is unreachable — because the distinction between
307    /// "no DSN
308    /// recorded" and "the server refused" is an operator's question, not
309    /// the caller's, and answering it in a response body would describe
310    /// the deployment to a stranger.
311    async fn database(&self, tenant: &Tenant) -> Result<Arc<dyn Database>, TenantDbError> {
312        match self.pool_for(tenant.id().as_str()).await {
313            Ok(db) => Ok(db as Arc<dyn Database>),
314            Err(why) => {
315                tracing::warn!(tenant = %tenant.id(), reason = ?why, "no pool for tenant");
316                Err(TenantDbError::Unreachable {
317                    tenant: tenant.id().to_string(),
318                })
319            }
320        }
321    }
322}
323
324/// A tenant's pool with the process-wide total applied around every
325/// operation.
326pub(crate) struct CappedDb {
327    inner: Postgres,
328    total: Arc<tokio::sync::Semaphore>,
329    acquire_timeout: Duration,
330    clock: Arc<dyn Clock>,
331    key: String,
332}
333
334impl CappedDb {
335    /// A permit from the process-wide total, or a refusal on the deadline.
336    ///
337    /// Refuses rather than queues: an unbounded queue turns a connection
338    /// shortage into an unbounded latency, which is the failure #136's
339    /// outbound budget refuses for the same reason.
340    async fn permit(&self) -> Result<tokio::sync::OwnedSemaphorePermit, DbError> {
341        let total = Arc::clone(&self.total);
342        let acquire = async move { total.acquire_owned().await };
343        match cratefield_core::timeout(&*self.clock, acquire, self.acquire_timeout).await {
344            Some(Ok(permit)) => Ok(permit),
345            // The semaphore is never closed, so `Err` is unreachable; it
346            // is mapped rather than unwrapped because an unreachable
347            // panic in a database path is not worth the line it saves.
348            Some(Err(_)) | None => Err(DbError::Execute(format!(
349                "`{}` waited out the connection budget",
350                self.key
351            ))),
352        }
353    }
354}
355
356#[async_trait]
357impl Database for CappedDb {
358    async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
359        let _permit = self.permit().await?;
360        self.inner.execute(stmt).await
361    }
362
363    async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
364        let _permit = self.permit().await?;
365        self.inner.query(stmt).await
366    }
367
368    async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError> {
369        // One permit for the whole batch: `batch` holds its connection
370        // across `pool.begin()`, so a per-statement permit would let the
371        // total be exceeded by exactly the number of open transactions.
372        let _permit = self.permit().await?;
373        self.inner.batch_atomic(stmts).await
374    }
375}
376
377#[cfg(test)]
378#[allow(clippy::disallowed_types)] // recording fakes, not request state
379mod tests {
380    use super::*;
381    use crate::testing::{TempDb, base_url, skip_reason};
382    use cratefield_core::Statement;
383
384    /// A clock the test moves, so idle eviction is deterministic rather
385    /// than a sleep.
386    struct StepClock {
387        offset: std::sync::atomic::AtomicI64,
388    }
389
390    impl StepClock {
391        fn new() -> Arc<Self> {
392            Arc::new(Self {
393                offset: std::sync::atomic::AtomicI64::new(0),
394            })
395        }
396
397        fn advance(&self, secs: i64) {
398            self.offset
399                .fetch_add(secs, std::sync::atomic::Ordering::SeqCst);
400        }
401    }
402
403    #[async_trait]
404    impl Clock for StepClock {
405        fn now(&self) -> time::OffsetDateTime {
406            time::OffsetDateTime::now_utc()
407                + time::Duration::seconds(self.offset.load(std::sync::atomic::Ordering::SeqCst))
408        }
409
410        async fn timeout_any(
411            &self,
412            fut: cratefield_core::BoxFuture<'static, Box<dyn std::any::Any + Send>>,
413            after: Duration,
414        ) -> Option<Box<dyn std::any::Any + Send>> {
415            tokio::time::timeout(after, fut).await.ok()
416        }
417    }
418
419    /// Hands out one DSN for the keys it knows, and records every lookup
420    /// so a test can prove a pool opened once rather than per request.
421    struct Dsns {
422        url: String,
423        known: Vec<String>,
424        lookups: std::sync::Mutex<Vec<String>>,
425    }
426
427    impl Dsns {
428        fn new(url: &str, known: &[&str]) -> Arc<Self> {
429            Arc::new(Self {
430                url: url.to_owned(),
431                known: known.iter().map(|k| (*k).to_owned()).collect(),
432                lookups: std::sync::Mutex::new(Vec::new()),
433            })
434        }
435
436        fn lookups(&self) -> Vec<String> {
437            self.lookups.lock().expect("lookup lock").clone()
438        }
439    }
440
441    #[async_trait]
442    impl TenantDsns for Dsns {
443        async fn dsn(&self, key: &str) -> Result<Option<String>, String> {
444            self.lookups
445                .lock()
446                .expect("lookup lock")
447                .push(key.to_owned());
448            Ok(self
449                .known
450                .iter()
451                .any(|known| known == key)
452                .then(|| self.url.clone()))
453        }
454    }
455
456    #[tokio::test]
457    async fn a_pool_opens_on_first_use_and_is_reused() {
458        let Some(url) = base_url() else {
459            eprintln!("SKIPPED: {}", skip_reason());
460            return;
461        };
462        let Some(temp) = TempDb::create(&url, "reglazy").await else {
463            eprintln!("SKIPPED: {}", skip_reason());
464            return;
465        };
466        let dsns = Dsns::new(&temp.url, &["a"]);
467        let registry =
468            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
469
470        assert_eq!(registry.open_pools(), 0, "nothing opens at construction");
471        for _ in 0..3 {
472            let db = registry.pool_for("a").await.expect("opens");
473            db.query(&Statement::new("SELECT 1"))
474                .await
475                .expect("queries");
476        }
477
478        assert_eq!(registry.open_pools(), 1);
479        assert_eq!(
480            dsns.lookups(),
481            vec!["a".to_owned()],
482            "the DSN is read once, not per request"
483        );
484        temp.finish().await;
485    }
486
487    #[tokio::test]
488    async fn an_idle_pool_is_closed_and_reopens_on_the_next_request() {
489        let Some(url) = base_url() else {
490            eprintln!("SKIPPED: {}", skip_reason());
491            return;
492        };
493        let Some(temp) = TempDb::create(&url, "regidle").await else {
494            eprintln!("SKIPPED: {}", skip_reason());
495            return;
496        };
497        let dsns = Dsns::new(&temp.url, &["a"]);
498        let clock = StepClock::new();
499        let registry = PoolRegistry::with_limits(
500            Arc::clone(&dsns) as Arc<dyn TenantDsns>,
501            Arc::clone(&clock) as Arc<dyn Clock>,
502            PoolLimits::default(),
503            8,
504            Duration::from_secs(60),
505        );
506
507        registry.pool_for("a").await.expect("opens");
508        assert_eq!(registry.open_pools(), 1);
509
510        clock.advance(120);
511        registry.pool_for("a").await.expect("reopens");
512        assert_eq!(registry.open_pools(), 1, "reopened, not accumulated");
513        assert_eq!(
514            dsns.lookups().len(),
515            2,
516            "a reopen reads the DSN again, which is what makes eviction real"
517        );
518        temp.finish().await;
519    }
520
521    #[tokio::test]
522    async fn one_key_cannot_starve_the_others_of_the_total() {
523        // #32's starvation criterion, in the terms it actually lives in:
524        // the total is one, held by A, so B must refuse on its deadline
525        // rather than queue behind A.
526        let Some(url) = base_url() else {
527            eprintln!("SKIPPED: {}", skip_reason());
528            return;
529        };
530        let Some(temp) = TempDb::create(&url, "regstarve").await else {
531            eprintln!("SKIPPED: {}", skip_reason());
532            return;
533        };
534        let dsns = Dsns::new(&temp.url, &["a", "b"]);
535        let registry = Arc::new(PoolRegistry::with_limits(
536            Arc::clone(&dsns) as Arc<dyn TenantDsns>,
537            StepClock::new(),
538            PoolLimits {
539                max_connections: 4,
540                idle_timeout: None,
541                acquire_timeout: Duration::from_millis(400),
542            },
543            1,
544            Duration::from_secs(300),
545        ));
546
547        let a = registry.pool_for("a").await.expect("a opens");
548        let b = registry.pool_for("b").await.expect("b opens");
549
550        let hold = tokio::spawn(async move {
551            let _ = a.query(&Statement::new("SELECT pg_sleep(5)")).await;
552        });
553
554        // Wait until the permit is *observably* taken rather than sleeping
555        // a guessed interval and hoping.
556        //
557        // The first version slept 200ms and assumed A had won the single
558        // permit by then. Under load it had not, B took the permit first,
559        // and the refusal this test exists to see never happened — it
560        // failed on a machine running two builds at once, which is exactly
561        // when a timing assumption is worth least.
562        let contended = async {
563            loop {
564                if b.query(&Statement::new("SELECT 1")).await.is_err() {
565                    return;
566                }
567                tokio::time::sleep(Duration::from_millis(20)).await;
568            }
569        };
570        tokio::time::timeout(Duration::from_secs(4), contended)
571            .await
572            .expect("A holds the only permit within four seconds");
573
574        // Now that contention is established, the deadline is the claim:
575        // B refuses on its own 400ms rather than waiting out A's five
576        // seconds.
577        let started = std::time::Instant::now();
578        let refused = b.query(&Statement::new("SELECT 1")).await;
579        let waited = started.elapsed();
580
581        assert!(
582            refused.is_err(),
583            "B must be refused rather than queued behind A: {refused:?}"
584        );
585        assert!(
586            waited < Duration::from_secs(2),
587            "and refused on its own deadline, not after A finished: {waited:?}"
588        );
589        hold.abort();
590        let _ = hold.await;
591
592        // A refusal is backpressure, not a broken pool.
593        b.query(&Statement::new("SELECT 1"))
594            .await
595            .expect("B recovers once the budget frees");
596        temp.finish().await;
597    }
598
599    #[tokio::test]
600    async fn an_unknown_key_opens_nothing_and_says_which_kind_of_miss() {
601        let Some(url) = base_url() else {
602            eprintln!("SKIPPED: {}", skip_reason());
603            return;
604        };
605        let Some(temp) = TempDb::create(&url, "regmissing").await else {
606            eprintln!("SKIPPED: {}", skip_reason());
607            return;
608        };
609        let dsns = Dsns::new(&temp.url, &["a"]);
610        let registry =
611            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
612
613        assert_eq!(
614            registry.pool_for("nobody").await.err(),
615            Some(PoolError::NoDsn)
616        );
617        assert_eq!(registry.open_pools(), 0, "and opened nothing");
618        temp.finish().await;
619    }
620
621    /// A `Postgres` over a pool that has not connected.
622    ///
623    /// `connect_lazy` is what makes the race testable without a
624    /// server: it parses the URL and opens nothing, so a test can
625    /// exercise which pool was *handed out* without one being open.
626    fn lazy_postgres(limits: PoolLimits) -> Postgres {
627        Postgres {
628            pool: sqlx::postgres::PgPoolOptions::new()
629                .max_connections(limits.max_connections)
630                .connect_lazy("postgres://unused@127.0.0.1:1/unused")
631                .expect("a lazy pool needs no server"),
632        }
633    }
634
635    /// Two callers racing one cold key open one pool between them.
636    ///
637    /// "A key has *a* pool" is the registry's whole promise, and two
638    /// opens break it: one tenant on two pools, the first orphaned by
639    /// the second's `insert` while whoever held it is still querying.
640    ///
641    /// Counted at the connect rather than observed on a server, so the
642    /// assertion is on the thing the race is about rather than on a
643    /// symptom downstream of it.
644    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
645    async fn two_callers_racing_one_cold_key_open_one_pool() {
646        let opens = Arc::new(std::sync::atomic::AtomicUsize::new(0));
647        // Holds every open until the test releases it, so the second
648        // caller reaches the map's insert before the first one has.
649        let gate = Arc::new(tokio::sync::Semaphore::new(0));
650        let counted = Arc::clone(&opens);
651        let held = Arc::clone(&gate);
652
653        let dsns = Dsns::new("postgres://unused/unused", &["a"]);
654        let registry = Arc::new(
655            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new())
656                .with_open(Arc::new(move |_dsn: String, limits: PoolLimits| {
657                    let counted = Arc::clone(&counted);
658                    let held = Arc::clone(&held);
659                    Box::pin(async move {
660                        counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
661                        let _permit = held.acquire().await.expect("gate is open");
662                        Ok(lazy_postgres(limits))
663                    })
664                })),
665        );
666
667        let mut racers: Vec<_> = (0..2)
668            .map(|_| {
669                let registry = Arc::clone(&registry);
670                tokio::spawn(async move { registry.pool_for("a").await })
671            })
672            .collect();
673
674        // Let both callers run as far as they can before releasing.
675        tokio::time::sleep(Duration::from_millis(200)).await;
676        gate.add_permits(2);
677
678        let first = racers.remove(0).await.expect("racer").expect("a pool");
679        let second = racers.remove(0).await.expect("racer").expect("a pool");
680
681        assert_eq!(
682            opens.load(std::sync::atomic::Ordering::SeqCst),
683            1,
684            "one key, one pool — a second open is a second pool for one tenant"
685        );
686        assert!(
687            Arc::ptr_eq(&first, &second),
688            "and both callers get that one pool, not one of two"
689        );
690        assert_eq!(registry.open_pools(), 1);
691        assert_eq!(
692            dsns.lookups(),
693            vec!["a".to_owned()],
694            "and its DSN is read once, not once per racing caller"
695        );
696    }
697}