Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.
API Index — Namespaces, Packages, Reports, Indices

DbConnectionPool
in package

FinalYes

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 Channel of size connections; acquire blocks (yields) until one is free or $timeout elapses (→ 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
template

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 refuses acquire() 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 whose with() body threw), and pool_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 $timeout seconds for one to become available; throws DbException on 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 later acquire() throws instead of rebuilding a fresh channel that would never be drained.
mysqli()  : 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.
pdo()  : PDO>
Pool of \PDO connections from a DSN. ERRMODE_EXCEPTION + FETCH_ASSOC are 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 $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).
with()  : T
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.
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 $size connections.

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

$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
TConn

close()

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
int

stats()

Per-worker stats — acquires, timeouts, clients created/discarded, validation replacements.

public stats() : Stats
Return values
Stats

transaction()

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
template
Return values
T

with()

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
template
Return values
T

aliveEnough()

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
bool

build()

Build a fresh connection via the driver and count it.

private build() : TConn
Return values
TConn

buildChannelLocked()

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
Channel

discard()

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.

Return values
Channel
On this page