API Index — Namespaces, Packages, Reports, Indices
DbConnectionPool
in package
Per-worker pool of database connections — the DB counterpart to {@see \ZealPHP\Store\RedisConnectionPool}.
Connection-library-agnostic: the pool manages a bounded channel of opaque
connection objects and delegates everything library-specific (connect,
transaction lifecycle, liveness, teardown) to a PoolDriver. Ships
with PdoDriver (any PDO driver) and MysqliDriver (mysqli),
with ::pdo() / ::mysqli() convenience constructors.
Why this exists
Under coroutine mode with Runtime::HOOK_ALL, ZealPHP's documented DB
pattern is "one connection per coroutine" — because two coroutines sharing
one handle interleave wire frames and corrupt the socket. That is SAFE but
does NOT scale: peak live DB connections = peak concurrent requests, so a
few hundred concurrent queries blow past MySQL/Postgres max_connections.
This pool bounds connections to size × workers × nodes regardless of
request concurrency: each query borrows a private connection, uses it, and
returns it.
Sizing: keep size × workers × nodes ≤ db_max_connections − headroom,
and cap the OpenSwoole server's max_coroutine so request concurrency can't
outrun the pool and pile up on acquire() waits.
Semantics
- Coroutine context: a bounded
Channelofsizeconnections;acquireblocks (yields) until one is free or$timeoutelapses (→DbException). - Sync context (no scheduler, e.g.
superglobals(true)): degrades to a single lazily-built connection used sequentially. - Transaction-safe return:
release()asks the driver to roll back any transaction the borrower left open (PDO; mysqli can't introspect — see MysqliDriver). - Poison-pill discard: a
with()body that throws discards its connection (half-finished transaction or dead socket) and refills the pool. - Optional liveness check:
$validationQuery(e.g.'SELECT 1') pings a connection on acquire and replaces it if the server closed it while idle.
Usage
$pool = DbConnectionPool::pdo('mysql:host=127.0.0.1;dbname=app', 'user', 'pass',
size: 16, validationQuery: 'SELECT 1');
// or mysqli (WordPress $wpdb-style code):
$pool = DbConnectionPool::mysqli('127.0.0.1', 'user', 'pass', 'app', size: 16);
$rows = $pool->with(fn (\PDO $db) =>
$db->query('SELECT * FROM users LIMIT 10')->fetchAll());
$pool->transaction(function (\PDO $db): void {
$db->exec('UPDATE accounts SET balance = balance - 10 WHERE id = 1');
$db->exec('UPDATE accounts SET balance = balance + 10 WHERE id = 2');
});
Tags
Table of Contents
Properties
- $buildLock : Channel|null
- Size-1 channel used as a coroutine mutex around channel construction.
- $ch : Channel|null
- The coroutine channel holding available connections. Created lazily on
the first
acquire()inside a coroutine (Channel::push()throws outside the scheduler). - $closed : bool
- Set by
close()— a closed pool refusesacquire()rather than silently rebuilding a fresh (never-drained) channel. - $driver : PoolDriver<string|int, TConn>
- $size : int
- Configured pool capacity (minimum
1). - $stats : Stats
- Per-worker counters:
pool_acquires_total,pool_acquire_timeouts_total,pool_clients_created_total,pool_clients_discarded_total(poison-pill discards of a connection whosewith()body threw), andpool_validation_replacements_total(connections replaced because they failed the on-acquire liveness probe). - $syncClient : TConn|null
- Single connection used in sync (non-coroutine) mode.
- $validationQuery : string|null
- Optional liveness probe run on acquire (null = no check).
Methods
- __construct() : mixed
- acquire() : TConn
- Pop a connection from the pool. In coroutine context, yields up to
$timeoutseconds for one to become available; throwsDbExceptionon timeout. In sync context, returns a shared single connection. - close() : void
- Tear down the pool. Drains every channel connection (driver
disconnect()+ reference drop) and marks the pool closed so a lateracquire()throws instead of rebuilding a fresh channel that would never be drained. - mysqli() : mysqli>
- Pool of
\mysqliconnections.$charsetdefaults toutf8mb4. For SSL / custom init, build a {@see MysqliDriver} from your own factory and pass it to the constructor instead. - pdo() : PDO>
- Pool of
\PDOconnections from a DSN.ERRMODE_EXCEPTION+FETCH_ASSOCare applied unless overridden in$options. - release() : void
- Return a connection to the pool. Rolls back any transaction the borrower left open first (driver-dependent). A connection that can't even be inspected is discarded + replaced. No-op for the sync-mode singleton.
- size() : int
- Return the configured pool capacity.
- stats() : Stats
- Per-worker stats — acquires, timeouts, clients created/discarded, validation replacements.
- transaction() : T
- Run
$fninside a transaction on a pooled connection: BEGIN, then COMMIT on success or ROLLBACK on throw. The connection returns to the pool afterward (cleaned either way). - with() : T
- Acquire + use + release in one call. On success the connection returns to
the pool (transaction-cleaned); if
$fnthrows, the connection is discarded and the pool refilled, then the exception re-thrown. - aliveEnough() : bool
- True if no validation is configured, or the connection answers the validation probe. A connection that fails is counted as a validation replacement so the caller rebuilds.
- build() : TConn
- Build a fresh connection via the driver and count it.
- buildChannelLocked() : Channel
- The serialized half of {@see ensureChannel()} — runs under
$buildLock. - discard() : void
- Discard a connection (broken / poisoned) and preserve pool capacity by refilling with a fresh one. For the sync singleton, just null it so the next acquire rebuilds.
- ensureChannel() : Channel
- Lazily initialise the channel and pre-fill it with
$sizeconnections.
Properties
$buildLock
Size-1 channel used as a coroutine mutex around channel construction.
private
Channel|null
$buildLock
= null
Every build() in the fill loop yields on network I/O, so without this
gate every cold concurrent acquirer passed the $ch === null check and
built its OWN full channel — leaking all but the last (#322).
$ch
The coroutine channel holding available connections. Created lazily on
the first acquire() inside a coroutine (Channel::push() throws
outside the scheduler).
private
Channel|null
$ch
= null
$closed
Set by close() — a closed pool refuses acquire() rather than silently rebuilding a fresh (never-drained) channel.
private
bool
$closed
= false
$driver
private
PoolDriver<string|int, TConn>
$driver
$size
Configured pool capacity (minimum 1).
private
int
$size
$stats
Per-worker counters: pool_acquires_total, pool_acquire_timeouts_total,
pool_clients_created_total, pool_clients_discarded_total (poison-pill
discards of a connection whose with() body threw), and
pool_validation_replacements_total (connections replaced because they
failed the on-acquire liveness probe).
private
Stats
$stats
$syncClient
Single connection used in sync (non-coroutine) mode.
private
TConn|null
$syncClient
= null
$validationQuery
Optional liveness probe run on acquire (null = no check).
private
string|null
$validationQuery
Methods
__construct()
public
__construct(PoolDriver<string|int, TConn> $driver[, int $size = 8 ][, string|null $validationQuery = null ]) : mixed
Parameters
- $driver : PoolDriver<string|int, TConn>
-
Connection-library adapter (PDO, mysqli, …).
- $size : int = 8
-
Max pool size per worker (default
8). - $validationQuery : string|null = null
-
Liveness probe run on acquire (e.g.
'SELECT 1'); null = skip.
acquire()
Pop a connection from the pool. In coroutine context, yields up to
$timeout seconds for one to become available; throws DbException on
timeout. In sync context, returns a shared single connection.
public
acquire([float $timeout = 5.0 ]) : TConn
When $validationQuery is configured, a connection that fails the probe
(server closed it while idle) is transparently replaced with a fresh one.
Parameters
- $timeout : float = 5.0
Return values
TConnclose()
Tear down the pool. Drains every channel connection (driver
disconnect() + reference drop) and marks the pool closed so a later
acquire() throws instead of rebuilding a fresh channel that would never
be drained.
public
close() : void
Call only at worker shutdown / when no borrows are in flight — a
connection a coroutine is still holding can't be drained here; it closes
when that borrower's reference drops (its release() becomes a no-op on
the closed pool).
mysqli()
Pool of \mysqli connections. $charset defaults to utf8mb4. For
SSL / custom init, build a {@see MysqliDriver} from your own factory and
pass it to the constructor instead.
public
static mysqli(string $host[, string|null $username = null ][, string|null $password = null ][, string|null $database = null ][, int $port = 3306 ][, string|null $socket = null ][, string $charset = 'utf8mb4' ][, int $size = 8 ][, string|null $validationQuery = null ]) : mysqli>
Parameters
- $host : string
- $username : string|null = null
- $password : string|null = null
- $database : string|null = null
- $port : int = 3306
- $socket : string|null = null
- $charset : string = 'utf8mb4'
- $size : int = 8
- $validationQuery : string|null = null
Return values
mysqli>pdo()
Pool of \PDO connections from a DSN. ERRMODE_EXCEPTION +
FETCH_ASSOC are applied unless overridden in $options.
public
static pdo(string $dsn[, string|null $username = null ][, string|null $password = null ][, array<int, mixed> $options = [] ][, int $size = 8 ][, string|null $validationQuery = null ]) : PDO>
Parameters
- $dsn : string
- $username : string|null = null
- $password : string|null = null
- $options : array<int, mixed> = []
-
PDO driver options (merged over the defaults).
- $size : int = 8
- $validationQuery : string|null = null
Return values
PDO>release()
Return a connection to the pool. Rolls back any transaction the borrower left open first (driver-dependent). A connection that can't even be inspected is discarded + replaced. No-op for the sync-mode singleton.
public
release(TConn $client) : void
Parameters
- $client : TConn
size()
Return the configured pool capacity.
public
size() : int
Return values
intstats()
Per-worker stats — acquires, timeouts, clients created/discarded, validation replacements.
public
stats() : Stats
Return values
Statstransaction()
Run $fn inside a transaction on a pooled connection: BEGIN, then COMMIT
on success or ROLLBACK on throw. The connection returns to the pool
afterward (cleaned either way).
public
transaction(callable(TConn): T $fn) : T
Parameters
- $fn : callable(TConn): T
Tags
Return values
Twith()
Acquire + use + release in one call. On success the connection returns to
the pool (transaction-cleaned); if $fn throws, the connection is
discarded and the pool refilled, then the exception re-thrown.
public
with(callable(TConn): T $fn) : T
Parameters
- $fn : callable(TConn): T
Tags
Return values
TaliveEnough()
True if no validation is configured, or the connection answers the validation probe. A connection that fails is counted as a validation replacement so the caller rebuilds.
private
aliveEnough(TConn $client) : bool
Parameters
- $client : TConn
Return values
boolbuild()
Build a fresh connection via the driver and count it.
private
build() : TConn
Return values
TConnbuildChannelLocked()
The serialized half of {@see ensureChannel()} — runs under $buildLock.
private
buildChannelLocked() : Channel
Re-checks $ch first: a parked peer wakes here AFTER the winner built,
so it must adopt the winner's channel instead of building another.
Return values
Channeldiscard()
Discard a connection (broken / poisoned) and preserve pool capacity by refilling with a fresh one. For the sync singleton, just null it so the next acquire rebuilds.
private
discard(TConn $client) : void
Parameters
- $client : TConn
ensureChannel()
Lazily initialise the channel and pre-fill it with $size connections.
private
ensureChannel() : Channel
MUST be called inside a coroutine — Channel::push() throws otherwise.
Coroutine-safe (#322): exactly ONE cold acquirer builds the channel;
concurrent acquirers queue on $buildLock and re-check after waking.
A fill that throws mid-way drains + disconnects its partial build so a
transient connect failure can't leak connections or brick the pool —
the next acquirer simply retries the build.