A fixed-ceiling connection pool over any [Driver]. Opening a database
connection is expensive — a TCP handshake and auth round trip for a networked
backend — so a server hands out a small set of connections and reuses them
instead of opening one per request. This is the moondb counterpart of Go's
sql.DB connection pool and SQLAlchemy's QueuePool.
The pool is generic in the concrete driver D rather than holding
&Driver trait objects, so acquire gives back the real driver type and a
caller keeps access to backend-specific methods (SQLite's exec_script, a
driver's prepared-statement handle). new takes a make factory that opens
one fresh connection; the pool calls it only when no idle connection is
available and the open count is still below max_size.
Beyond plain reuse, the pool keeps a connection healthy over its lifetime,
matching the knobs both database/sql and SQLAlchemy's QueuePool expose:
- pre-ping (pre_ping) — before an idle connection is handed back it is
probed with [Driver::ping]; a connection that fails the probe is closed and
skipped, so a caller never receives a connection the backend has already
dropped. This is SQLAlchemy's pool_pre_ping.
- max lifetime (max_lifetime_ms) — a connection older than this is retired
on acquire and replaced with a fresh one, so long-lived pools recycle
connections a load balancer or the server may have aged out. This is Go's
SetConnMaxLifetime. Age is measured with an injected clock, exactly as
database/sql swaps nowFunc in tests; the default clock disables the check.
- acquire timeout (acquire_timeout_ms) — the wait budget an exhausted
acquire is allowed. See the note on synchrony below.
It is synchronous and single-threaded by design — moondb's base contract is
sync, and MoonBit's pure backends have no shared-memory threads — so acquire
never truly blocks: nothing else can return a connection while one call waits.
A request that finds the pool exhausted therefore fails immediately rather than
sleeping; acquire_timeout_ms is carried into that failure's message and is the
budget a driver running on an async runtime honours when it layers real waiting
on top. Use [try_acquire] for the non-raising "give me one only if free" path.
The reuse, ceiling, health, and lifecycle bookkeeping all live here.
The counting is moonpool's Pool: live, idle and lent, each lent connection's
birth recognised on its way back so max_lifetime counts from creation. So is
the optional ward in front of the database: backoff after failed connects, a
token bucket spacing reconnections, a breaker, a shedder. This type adds only
what needs a driver — opening, closing, probing.