Package · cpool

Quick start

Three steps: write an adapter, create the pool, borrow and release connections. The example below uses PDO, but exactly the same works with any driver.

Step 1 — the adapter

The only place that knows about the driver. Three methods:

src/PdoFactory.php
<?php

use Flytachi\Winter\CPool\ConnectionFactory;

final readonly class PdoFactory implements ConnectionFactory
{
  public function __construct(private string $dsn) {}

  public function create(): object
  {
      return new PDO($this->dsn, options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
  }

  public function validate(object $connection): bool
  {
      try {
          $connection->query('SELECT 1');
          return true;
      } catch (Throwable) {
          return false;          // dead → the pool evicts it and opens a new one
      }
  }

  public function close(object $connection): void
  {
      // PDO closes when dereferenced; other drivers need an explicit call
  }
}

Step 2 — the pool

php
use Flytachi\Winter\CPool\{ConnectionPool, PoolPolicy};

$pool = new ConnectionPool(
  new PdoFactory('sqlite::memory:'),
  new PoolPolicy(maximumPoolSize: 10),
);

Creating the pool opens nothing — connections appear on demand.

Step 3 — borrow and release

php
$entry = $pool->borrow();          // waits for a free one, up to connectionTimeout

try {
  /** @var PDO $pdo */
  $pdo = $entry->resource;
  $pdo->query('SELECT 1');
} finally {
  $pool->release($entry);        // returning it is mandatory
}

borrow() returns a PoolEntry; the connection itself is in $entry->resource.

Releasing is the caller's duty

The pool cannot track a borrower who never gives the connection back: every such leak permanently shrinks the pool by one, until it serves nobody at all. Hence the finally — or better, a facade that releases automatically at the end of the request (in the framework a coroutine defer does that).

What happens under load

The pool never opens more connections than the cap. When all are busy the borrower waits; if nothing frees up within connectionTimeout — an exception:

php
use Flytachi\Winter\CPool\PoolException;

$pool = new ConnectionPool($factory, new PoolPolicy(
  maximumPoolSize:   1,
  connectionTimeout: 0.2,
));

$held = $pool->borrow();

try {
  $pool->borrow();               // none free
} catch (PoolException $e) {
  echo $e->getMessage();
  // ConnectionPool: no free connection within 0.2s — raise maximumPoolSize or connectionTimeout.
}

That is a guard rail, not a failure: a queue inside the application beats too many connections on the database server, which takes the neighbouring applications down with it.

You can inspect the load at any moment:

php
$pool->stats();   // ['total' => 2, 'idle' => 2, 'active' => 0, 'maximum' => 10]

Without Swoole

A pool has nothing to distribute when the process serves one unit of work at a time. For such runtimes there is SingleConnection — same adapter, same meaning:

php
use Flytachi\Winter\CPool\SingleConnection;

$single = new SingleConnection(new PdoFactory('sqlite::memory:'));

$pdo = $single->get();     // opens on the first call
$pdo = $single->get();     // the same object; validated if it sat idle for a while

The choice is made once, at initialisation, and the application code knows nothing of it:

php
$connection = extension_loaded('swoole') && \Swoole\Coroutine::getCid() > 0
  ? $pool->borrow()->resource      // do not forget release()
  : $single->get();

Next