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 idle_secs = i64::try_from(self.idle.as_secs()).unwrap_or_else(|_| {
179            eprintln!("idle duration exceeds i64::MAX seconds, using max value");
180            i64::MAX
181        });
182        let cutoff = now.saturating_sub(idle_secs);
183        let mut pools = self.pools.lock().expect("pool lock");
184        let stale: Vec<String> = pools
185            .iter()
186            .filter(|(_, cached)| cached.last_used < cutoff)
187            .map(|(tenant, _)| tenant.clone())
188            .collect();
189        // Returned rather than closed under the lock: `close` awaits
190        // in-flight queries, and holding a std Mutex across an await is
191        // the deadlock this avoids by construction.
192        stale
193            .into_iter()
194            .filter_map(|tenant| pools.remove(&tenant).map(|cached| cached.db))
195            .collect()
196    }
197
198    fn now_secs(&self) -> i64 {
199        self.clock.now().unix_timestamp()
200    }
201}
202
203/// Why a keyed pool could not be produced. Internal: every variant is a
204/// [`TenantDbError::Unreachable`] to a caller, but which one it was is
205/// worth having in a log line.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub(crate) enum PoolError {
208    /// The source has no connection string under that key.
209    NoDsn,
210    /// The source itself failed.
211    Lookup,
212    /// The server refused or did not answer.
213    Connect,
214}
215
216impl PoolRegistry {
217    /// The pool for `key`, opened on first use and reused after.
218    ///
219    /// **Keyed, not tenanted.** Lazy open, idle eviction and both caps are
220    /// pool bookkeeping and have nothing to do with tenancy, so they live
221    /// here and are tested by string. What makes a key a *tenant* is the
222    /// [`TenantDatabases`] impl below, which is short enough to be correct
223    /// by inspection — the split is deliberate, so that the part with the
224    /// interesting behaviour does not need a `Tenant` to test and core's
225    /// private constructor can stay private.
226    pub(crate) async fn pool_for(&self, key: &str) -> Result<Arc<CappedDb>, PoolError> {
227        let now = self.now_secs();
228
229        for stale in self.evict_idle(now) {
230            if let Err(e) = stale.inner.close().await {
231                tracing::warn!("failed to close idle pool: {}", e);
232            }
233        }
234
235        if let Some(cached) = self.pools.lock().expect("pool lock").get_mut(key) {
236            cached.last_used = now;
237            return Ok(Arc::clone(&cached.db));
238        }
239
240        // Cold. One opener per key, so the check above and the insert
241        // below are not a check-then-act across two awaits: without
242        // this, two callers that both missed open two pools, and the
243        // second `insert` replaced the first — closing it out from
244        // under whoever still held it, and putting one tenant on two
245        // pools for as long as the orphaned one stayed alive.
246        let flight = self.flight(key);
247        let _opening = flight.lock().await;
248
249        // Re-check under the flight: whoever held it may have opened
250        // this key while we waited, and opening a second pool for a key
251        // that now has one is the race this lock exists to stop.
252        if let Some(cached) = self.pools.lock().expect("pool lock").get_mut(key) {
253            cached.last_used = now;
254            return Ok(Arc::clone(&cached.db));
255        }
256
257        // The DSN exists only inside this block, and the error it could be
258        // folded into is discarded rather than formatted.
259        let dsn = self
260            .dsns
261            .dsn(key)
262            .await
263            .map_err(|_| PoolError::Lookup)?
264            .ok_or(PoolError::NoDsn)?;
265        let pool = (self.open)(dsn.clone(), self.limits)
266            .await
267            .map_err(|_| PoolError::Connect)?;
268        drop(dsn);
269
270        let db = Arc::new(CappedDb {
271            inner: pool,
272            total: Arc::clone(&self.total),
273            acquire_timeout: self.limits.acquire_timeout,
274            clock: Arc::clone(&self.clock),
275            key: key.to_owned(),
276        });
277        self.pools.lock().expect("pool lock").insert(
278            key.to_owned(),
279            Cached {
280                db: Arc::clone(&db),
281                last_used: now,
282            },
283        );
284        Ok(db)
285    }
286
287    /// The lock that serialises opens of `key`.
288    ///
289    /// Entries are swept on every open, keeping only a key somebody is
290    /// still opening: a registry outlives its tenants and must not
291    /// accumulate a lock for every tenant it ever served. Sweeping on
292    /// `Arc::strong_count` is what makes that safe — the map holds one
293    /// reference, so a count of one means nobody is opening it or
294    /// waiting to, while a caller still holding a clone keeps its lock
295    /// alive and keeps waiting on it.
296    fn flight(&self, key: &str) -> Arc<tokio::sync::Mutex<()>> {
297        let mut inflight = self.inflight.lock().expect("pool lock");
298        inflight.retain(|_, flight| Arc::strong_count(flight) > 1);
299        Arc::clone(
300            inflight
301                .entry(key.to_owned())
302                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
303        )
304    }
305}
306
307#[async_trait]
308impl TenantDatabases for PoolRegistry {
309    /// The tenant half, and all of it: a tenant's key is its id.
310    ///
311    /// Every way the lookup can miss is one answer to the caller — the
312    /// tenant's database is unreachable — because the distinction between
313    /// "no DSN
314    /// recorded" and "the server refused" is an operator's question, not
315    /// the caller's, and answering it in a response body would describe
316    /// the deployment to a stranger.
317    async fn database(&self, tenant: &Tenant) -> Result<Arc<dyn Database>, TenantDbError> {
318        match self.pool_for(tenant.id().as_str()).await {
319            Ok(db) => Ok(db as Arc<dyn Database>),
320            Err(why) => {
321                tracing::warn!(tenant = %tenant.id(), reason = ?why, "no pool for tenant");
322                Err(TenantDbError::Unreachable {
323                    tenant: tenant.id().to_string(),
324                })
325            }
326        }
327    }
328}
329
330/// A tenant's pool with the process-wide total applied around every
331/// operation.
332pub(crate) struct CappedDb {
333    inner: Postgres,
334    total: Arc<tokio::sync::Semaphore>,
335    acquire_timeout: Duration,
336    clock: Arc<dyn Clock>,
337    key: String,
338}
339
340impl CappedDb {
341    /// A permit from the process-wide total, or a refusal on the deadline.
342    ///
343    /// Refuses rather than queues: an unbounded queue turns a connection
344    /// shortage into an unbounded latency, which is the failure #136's
345    /// outbound budget refuses for the same reason.
346    async fn permit(&self) -> Result<tokio::sync::OwnedSemaphorePermit, DbError> {
347        let total = Arc::clone(&self.total);
348        let acquire = async move { total.acquire_owned().await };
349        match cratefield_core::timeout(&*self.clock, acquire, self.acquire_timeout).await {
350            Some(Ok(permit)) => Ok(permit),
351            // The semaphore is never closed, so `Err` is unreachable; it
352            // is mapped rather than unwrapped because an unreachable
353            // panic in a database path is not worth the line it saves.
354            Some(Err(_)) | None => Err(DbError::Execute(format!(
355                "`{}` waited out the connection budget",
356                self.key
357            ))),
358        }
359    }
360}
361
362#[async_trait]
363impl Database for CappedDb {
364    async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
365        let _permit = self.permit().await?;
366        self.inner.execute(stmt).await
367    }
368
369    async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
370        let _permit = self.permit().await?;
371        self.inner.query(stmt).await
372    }
373
374    async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError> {
375        // One permit for the whole batch: `batch` holds its connection
376        // across `pool.begin()`, so a per-statement permit would let the
377        // total be exceeded by exactly the number of open transactions.
378        let _permit = self.permit().await?;
379        self.inner.batch_atomic(stmts).await
380    }
381}
382
383#[cfg(test)]
384#[allow(clippy::disallowed_types)] // recording fakes, not request state
385mod tests {
386    use super::*;
387    use crate::testing::{TempDb, base_url, skip_reason};
388    use cratefield_core::Statement;
389
390    /// A clock the test moves, so idle eviction is deterministic rather
391    /// than a sleep.
392    struct StepClock {
393        offset: std::sync::atomic::AtomicI64,
394    }
395
396    impl StepClock {
397        fn new() -> Arc<Self> {
398            Arc::new(Self {
399                offset: std::sync::atomic::AtomicI64::new(0),
400            })
401        }
402
403        fn advance(&self, secs: i64) {
404            self.offset
405                .fetch_add(secs, std::sync::atomic::Ordering::SeqCst);
406        }
407    }
408
409    #[async_trait]
410    impl Clock for StepClock {
411        fn now(&self) -> time::OffsetDateTime {
412            time::OffsetDateTime::now_utc()
413                + time::Duration::seconds(self.offset.load(std::sync::atomic::Ordering::SeqCst))
414        }
415
416        async fn timeout_any(
417            &self,
418            fut: cratefield_core::BoxFuture<'static, Box<dyn std::any::Any + Send>>,
419            after: Duration,
420        ) -> Option<Box<dyn std::any::Any + Send>> {
421            tokio::time::timeout(after, fut).await.ok()
422        }
423    }
424
425    /// Hands out one DSN for the keys it knows, and records every lookup
426    /// so a test can prove a pool opened once rather than per request.
427    struct Dsns {
428        url: String,
429        known: Vec<String>,
430        lookups: std::sync::Mutex<Vec<String>>,
431    }
432
433    impl Dsns {
434        fn new(url: &str, known: &[&str]) -> Arc<Self> {
435            Arc::new(Self {
436                url: url.to_owned(),
437                known: known.iter().map(|k| (*k).to_owned()).collect(),
438                lookups: std::sync::Mutex::new(Vec::new()),
439            })
440        }
441
442        fn lookups(&self) -> Vec<String> {
443            self.lookups.lock().expect("lookup lock").clone()
444        }
445    }
446
447    #[async_trait]
448    impl TenantDsns for Dsns {
449        async fn dsn(&self, key: &str) -> Result<Option<String>, String> {
450            self.lookups
451                .lock()
452                .expect("lookup lock")
453                .push(key.to_owned());
454            Ok(self
455                .known
456                .iter()
457                .any(|known| known == key)
458                .then(|| self.url.clone()))
459        }
460    }
461
462    #[tokio::test]
463    async fn a_pool_opens_on_first_use_and_is_reused() {
464        let Some(url) = base_url() else {
465            eprintln!("SKIPPED: {}", skip_reason());
466            return;
467        };
468        let Some(temp) = TempDb::create(&url, "reglazy").await else {
469            eprintln!("SKIPPED: {}", skip_reason());
470            return;
471        };
472        let dsns = Dsns::new(&temp.url, &["a"]);
473        let registry =
474            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
475
476        assert_eq!(registry.open_pools(), 0, "nothing opens at construction");
477        for _ in 0..3 {
478            let db = registry.pool_for("a").await.expect("opens");
479            db.query(&Statement::new("SELECT 1"))
480                .await
481                .expect("queries");
482        }
483
484        assert_eq!(registry.open_pools(), 1);
485        assert_eq!(
486            dsns.lookups(),
487            vec!["a".to_owned()],
488            "the DSN is read once, not per request"
489        );
490        temp.finish().await;
491    }
492
493    #[tokio::test]
494    async fn an_idle_pool_is_closed_and_reopens_on_the_next_request() {
495        let Some(url) = base_url() else {
496            eprintln!("SKIPPED: {}", skip_reason());
497            return;
498        };
499        let Some(temp) = TempDb::create(&url, "regidle").await else {
500            eprintln!("SKIPPED: {}", skip_reason());
501            return;
502        };
503        let dsns = Dsns::new(&temp.url, &["a"]);
504        let clock = StepClock::new();
505        let registry = PoolRegistry::with_limits(
506            Arc::clone(&dsns) as Arc<dyn TenantDsns>,
507            Arc::clone(&clock) as Arc<dyn Clock>,
508            PoolLimits::default(),
509            8,
510            Duration::from_secs(60),
511        );
512
513        registry.pool_for("a").await.expect("opens");
514        assert_eq!(registry.open_pools(), 1);
515
516        clock.advance(120);
517        registry.pool_for("a").await.expect("reopens");
518        assert_eq!(registry.open_pools(), 1, "reopened, not accumulated");
519        assert_eq!(
520            dsns.lookups().len(),
521            2,
522            "a reopen reads the DSN again, which is what makes eviction real"
523        );
524        temp.finish().await;
525    }
526
527    #[tokio::test]
528    async fn one_key_cannot_starve_the_others_of_the_total() {
529        // #32's starvation criterion, in the terms it actually lives in:
530        // the total is one, held by A, so B must refuse on its deadline
531        // rather than queue behind A.
532        let Some(url) = base_url() else {
533            eprintln!("SKIPPED: {}", skip_reason());
534            return;
535        };
536        let Some(temp) = TempDb::create(&url, "regstarve").await else {
537            eprintln!("SKIPPED: {}", skip_reason());
538            return;
539        };
540        let dsns = Dsns::new(&temp.url, &["a", "b"]);
541        let registry = Arc::new(PoolRegistry::with_limits(
542            Arc::clone(&dsns) as Arc<dyn TenantDsns>,
543            StepClock::new(),
544            PoolLimits {
545                max_connections: 4,
546                idle_timeout: None,
547                acquire_timeout: Duration::from_millis(400),
548            },
549            1,
550            Duration::from_secs(300),
551        ));
552
553        let a = registry.pool_for("a").await.expect("a opens");
554        let b = registry.pool_for("b").await.expect("b opens");
555
556        let hold = tokio::spawn(async move {
557            let _ = a.query(&Statement::new("SELECT pg_sleep(5)")).await;
558        });
559
560        // Wait until the permit is *observably* taken rather than sleeping
561        // a guessed interval and hoping.
562        //
563        // The first version slept 200ms and assumed A had won the single
564        // permit by then. Under load it had not, B took the permit first,
565        // and the refusal this test exists to see never happened — it
566        // failed on a machine running two builds at once, which is exactly
567        // when a timing assumption is worth least.
568        let contended = async {
569            loop {
570                if b.query(&Statement::new("SELECT 1")).await.is_err() {
571                    return;
572                }
573                tokio::time::sleep(Duration::from_millis(20)).await;
574            }
575        };
576        tokio::time::timeout(Duration::from_secs(4), contended)
577            .await
578            .expect("A holds the only permit within four seconds");
579
580        // Now that contention is established, the deadline is the claim:
581        // B refuses on its own 400ms rather than waiting out A's five
582        // seconds.
583        let started = std::time::Instant::now();
584        let refused = b.query(&Statement::new("SELECT 1")).await;
585        let waited = started.elapsed();
586
587        assert!(
588            refused.is_err(),
589            "B must be refused rather than queued behind A: {refused:?}"
590        );
591        assert!(
592            waited < Duration::from_secs(2),
593            "and refused on its own deadline, not after A finished: {waited:?}"
594        );
595        hold.abort();
596        let _ = hold.await;
597
598        // A refusal is backpressure, not a broken pool.
599        b.query(&Statement::new("SELECT 1"))
600            .await
601            .expect("B recovers once the budget frees");
602        temp.finish().await;
603    }
604
605    #[tokio::test]
606    async fn an_unknown_key_opens_nothing_and_says_which_kind_of_miss() {
607        let Some(url) = base_url() else {
608            eprintln!("SKIPPED: {}", skip_reason());
609            return;
610        };
611        let Some(temp) = TempDb::create(&url, "regmissing").await else {
612            eprintln!("SKIPPED: {}", skip_reason());
613            return;
614        };
615        let dsns = Dsns::new(&temp.url, &["a"]);
616        let registry =
617            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new());
618
619        assert_eq!(
620            registry.pool_for("nobody").await.err(),
621            Some(PoolError::NoDsn)
622        );
623        assert_eq!(registry.open_pools(), 0, "and opened nothing");
624        temp.finish().await;
625    }
626
627    /// A `Postgres` over a pool that has not connected.
628    ///
629    /// `connect_lazy` is what makes the race testable without a
630    /// server: it parses the URL and opens nothing, so a test can
631    /// exercise which pool was *handed out* without one being open.
632    fn lazy_postgres(limits: PoolLimits) -> Postgres {
633        Postgres {
634            pool: sqlx::postgres::PgPoolOptions::new()
635                .max_connections(limits.max_connections)
636                .connect_lazy("postgres://unused@127.0.0.1:1/unused")
637                .expect("a lazy pool needs no server"),
638        }
639    }
640
641    /// Two callers racing one cold key open one pool between them.
642    ///
643    /// "A key has *a* pool" is the registry's whole promise, and two
644    /// opens break it: one tenant on two pools, the first orphaned by
645    /// the second's `insert` while whoever held it is still querying.
646    ///
647    /// Counted at the connect rather than observed on a server, so the
648    /// assertion is on the thing the race is about rather than on a
649    /// symptom downstream of it.
650    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
651    async fn two_callers_racing_one_cold_key_open_one_pool() {
652        let opens = Arc::new(std::sync::atomic::AtomicUsize::new(0));
653        // Holds every open until the test releases it, so the second
654        // caller reaches the map's insert before the first one has.
655        let gate = Arc::new(tokio::sync::Semaphore::new(0));
656        let counted = Arc::clone(&opens);
657        let held = Arc::clone(&gate);
658
659        let dsns = Dsns::new("postgres://unused/unused", &["a"]);
660        let registry = Arc::new(
661            PoolRegistry::new(Arc::clone(&dsns) as Arc<dyn TenantDsns>, StepClock::new())
662                .with_open(Arc::new(move |_dsn: String, limits: PoolLimits| {
663                    let counted = Arc::clone(&counted);
664                    let held = Arc::clone(&held);
665                    Box::pin(async move {
666                        counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
667                        let _permit = held.acquire().await.expect("gate is open");
668                        Ok(lazy_postgres(limits))
669                    })
670                })),
671        );
672
673        let mut racers: Vec<_> = (0..2)
674            .map(|_| {
675                let registry = Arc::clone(&registry);
676                tokio::spawn(async move { registry.pool_for("a").await })
677            })
678            .collect();
679
680        // Let both callers run as far as they can before releasing.
681        tokio::time::sleep(Duration::from_millis(200)).await;
682        gate.add_permits(2);
683
684        let first = racers.remove(0).await.expect("racer").expect("a pool");
685        let second = racers.remove(0).await.expect("racer").expect("a pool");
686
687        assert_eq!(
688            opens.load(std::sync::atomic::Ordering::SeqCst),
689            1,
690            "one key, one pool — a second open is a second pool for one tenant"
691        );
692        assert!(
693            Arc::ptr_eq(&first, &second),
694            "and both callers get that one pool, not one of two"
695        );
696        assert_eq!(registry.open_pools(), 1);
697        assert_eq!(
698            dsns.lookups(),
699            vec!["a".to_owned()],
700            "and its DSN is read once, not once per racing caller"
701        );
702    }
703}