Policy
PoolPolicy is an immutable value object. Every setting has a default; pass named
arguments only for what you are changing.
use Flytachi\Winter\CPool\PoolPolicy;
new PoolPolicy(maximumPoolSize: 20, maxLifetime: 600.0);
PoolPolicy::default(); // all defaultsCapacity
| Parameter | Default | What it does |
|---|---|---|
maximumPoolSize |
10 |
Hard cap on the number of open connections |
connectionTimeout |
15.0 |
Deadline for a whole borrow |
That is one decision, not two. The cap turns “too many connections” from a refusal on the database side into a queue on the application side; the timeout decides how long that queue is allowed to grow before a request gives up.
connectionTimeout bounds the borrow, not each wait inside it. Waiting for a free
connection, discarding dead ones and opening their replacements all come out of the same
budget, and when it runs out the borrow throws: unusable() if the time went on dead
connections, exhausted() if everything was merely busy.
How to size it: the cap is a property of the database, divided by the number of
workers talking to it. Four Swoole workers against PostgreSQL with
max_connections = 100, leaving room for migrations and psql, is closer to
maximumPoolSize: 15 than to 100.
A pool at its cap is not always too small
Before raising the limit, look for a borrow() without a matching release(): every
such leak permanently shrinks the working size of the pool by one, and the symptom looks
identical.
Lifetime
| Parameter | Default | What it does |
|---|---|---|
maxLifetime |
1800.0 |
A connection older than this is evicted on borrow |
maxLifetimeJitter |
0.1 |
The fraction of maxLifetime the deadline is spread over |
Why evict a healthy connection at all: proxies, load balancers and failover addresses move underneath long-lived sockets. A connection opened an hour ago may answer perfectly while pointing at a server being rotated out. Reopening is how you follow infrastructure the pool cannot see.
The jitter is not decoration. Ten connections created at startup with the same deadline expire within the same second — and the application stalls while all ten reconnect. Ten percent of half an hour stretches that over three minutes.
Liveness probing
| Parameter | Default | What it does |
|---|---|---|
aliveBypassWindow |
0.5 |
Skip the probe for a connection used recently |
Set 0.0 and the probe runs on every borrow: correct, and it doubles the round trips of
a busy service. Raise it and a broken socket survives a little longer before it is
noticed. The default assumes that a connection which answered half a second ago is
alive; if the database died inside that window the request fails, and the caller evicts
the connection itself.
Background housekeeping
| Parameter | Default | What it does |
|---|---|---|
housekeepingInterval |
30.0 |
How often the sweep runs |
keepaliveTime |
120.0 |
Ping connections idle for longer than this (0 — off) |
idleTimeout |
600.0 |
Close connections idle for longer than this (0 — off) |
minimumIdle |
0 |
Never drop below this many |
Housekeeping is on by default, and the two reasons for it are the same one seen from
both ends. A pool nobody sweeps holds its sockets forever: maxLifetime is checked when a
connection is borrowed, and an idle application borrows nothing, so a pool that grew
during a burst at midnight still holds every socket at dawn. And when the server or a
firewall drops those sockets meanwhile, the first request back is the one that finds out —
it has to bury every corpse and reopen before doing its own work. keepaliveTime stops
them dying, idleTimeout gives them back, and neither costs a request anything.
The timer is armed by the first borrow, not by constructing the pool, and only under Swoole. An application that never opens a connection still pays exactly nothing. One that does pays a tick every thirty seconds per pool per worker, plus a ping per idle connection every two minutes.
A pool holds a timer after its first borrow
A live repeating timer keeps the Swoole reactor from draining, so the pool has to be
closed: under the framework that is already handled (workerExit closes the pools), but
in your own script or test call close() yourself — otherwise Swoole\Coroutine\run()
will not return.
The sweep probes one connection at a time and returns each to the channel before taking the next, so borrowers always find the rest waiting — only the connection in flight is out of reach. A pass costs one round trip per connection that is due a ping: five connections against a 50 ms server take ~250 ms of background time and nothing of any request’s time.
You turn keepaliveTime off (0.0) when connections are cheap and the server is local —
a ping every two minutes is not free if the pool is large and the link is not. You turn
idleTimeout off when the pool should stay warm between bursts, and that is also when you
set minimumIdle, so the next burst does not start from zero.
The ordering rule
keepaliveTime < idleTimeout < maxLifetime < whatever kills idle connections upstream
All three are deadlines on the same connection, and each only means something while the connection is still there to receive it:
keepaliveTime ≥ maxLifetime— the connection is rotated before the first ping ever reaches it. The pings never happen.keepaliveTime ≥ idleTimeout(withminimumIdle: 0) — the connection is closed before the first ping reaches it. Same outcome. With a warm floor this is fine: the floor connections outliveidleTimeout, so keepalive still has work.maxLifetime ≥ the server's own idle timeout— the thing this is all defending against wins: Postgresidle_session_timeout, a Redistimeout, a NAT that forgets the flow. The pool has to recycle sooner than they cut, and that bound is outside its knowledge.
The pool applies the first two itself: an unreachable keepaliveTime is dropped to 0.0
when the policy is constructed, so PoolPolicy::$keepaliveTime always reads as what the
housekeeper will actually do. A setting that silently does nothing is worse than one that
is plainly off — the operator believes idle connections are being pinged while they quietly
die. HikariCP takes the same line, disabling a keepaliveTime that reaches maxLifetime.
For reference, HikariCP’s own defaults sit inside this ordering: keepaliveTime 2 minutes,
idleTimeout 10 minutes, maxLifetime 30 minutes.
Opening after a failure
Not a knob — the pool decides this one on its own. When opening a connection fails, or the connection opens and then cannot answer, opening is held shut for 10 ms; each consecutive failure doubles the wait up to 5 s, and the first connection that opens and answers clears it.
It is deliberately not configurable: there is no useful value of “hammer a failing server harder”. The numbers are HikariCP’s, whose background connection creator throttles the same way. What it buys shows on a server that accepts sockets but does not serve on them — a database still starting, a cache still loading its dataset: without the penalty a single borrow opened 10,038 sockets in five milliseconds, and every concurrent request did the same.
Next
- API reference — where each setting is applied
- Writing an adapter —
validate()under the bypass window