Пакет · cpool

Быстрый старт

Три шага: написать адаптер, создать пул, брать и возвращать соединения. Ниже — на примере PDO, но ровно то же работает с любым драйвером.

Шаг 1 — адаптер

Единственное место, знающее про драйвер. Три метода:

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;          // мёртвое → пул выселит и откроет новое
      }
  }

  public function close(object $connection): void
  {
      // PDO закрывается при разыменовании; другим драйверам нужен явный вызов
  }
}

Шаг 2 — пул

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

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

Создание пула ничего не открывает — соединения появляются по требованию.

Шаг 3 — взять и вернуть

php
$entry = $pool->borrow();          // ждёт свободное, максимум connectionTimeout

try {
  /** @var PDO $pdo */
  $pdo = $entry->resource;
  $pdo->query('SELECT 1');
} finally {
  $pool->release($entry);        // возвращать обязательно
}

borrow() отдаёт PoolEntry, само соединение лежит в $entry->resource.

Возврат — обязанность вызывающего

Пул не умеет отследить заёмщика, который не вернул соединение: каждая такая утечка навсегда уменьшает пул на единицу, пока он не перестанет обслуживать кого-либо. Поэтому finally, а лучше — фасад, возвращающий соединение автоматически в конце запроса (во фреймворке это делает defer корутины).

Что происходит под нагрузкой

Пул не открывает соединений сверх потолка. Когда все заняты, заёмщик ждёт; если свободное не появилось за connectionTimeout — исключение:

php
use Flytachi\Winter\CPool\PoolException;

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

$held = $pool->borrow();

try {
  $pool->borrow();               // свободных нет
} catch (PoolException $e) {
  echo $e->getMessage();
  // ConnectionPool: no free connection within 0.2s — raise maximumPoolSize or connectionTimeout.
}

Это защита, а не сбой: очередь в приложении лучше, чем too many connections на сервере базы, который положит и соседние приложения.

Проверить занятость можно в любой момент:

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

Без Swoole

Пулу нечего распределять, когда процесс обслуживает одну единицу работы за раз. Для таких рантаймов — SingleConnection, тот же адаптер, тот же смысл:

php
use Flytachi\Winter\CPool\SingleConnection;

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

$pdo = $single->get();     // открывает при первом вызове
$pdo = $single->get();     // тот же объект; проверяется, если долго простоял

Выбор делается один раз, при инициализации, и прикладной код о нём не знает:

php
$connection = extension_loaded('swoole') && \Swoole\Coroutine::getCid() > 0
  ? $pool->borrow()->resource      // не забыть release()
  : $single->get();

Дальше