Пакет · cpool

Свой адаптер

Адаптер — единственное место, знающее, что лежит в пуле. Три метода, и почти вся сложность сосредоточена в одном из них.

Контракт

php
interface ConnectionFactory
{
  public function create(): object;
  public function validate(object $connection): bool;
  public function close(object $connection): void;
}

create()

Открывает одно соединение и возвращает его. Может бросать — пул завернёт любое исключение в PoolException::connectFailed(), поэтому вызывающий видит один тип независимо от драйвера.

Здесь делается полное рукопожатие: подключение, аутентификация, выбор базы, сессионные параметры. Недонастроенное соединение, отданное пулу, станет недонастроенным соединением у случайного заёмщика позже.

validate() — здесь ошибаются чаще всего

Отвечает на один вопрос: сработал бы сейчас запрос на этом соединении?

php
public function validate(object $connection): bool
{
  try {
      $connection->query('SELECT 1');   // Redis: $connection->ping()
      return true;
  } catch (Throwable) {
      return false;
  }
}

Три правила:

Возвращайте false, а не бросайте. Брошенное исключение пул тоже считает признаком смерти, так что ничего не сломается — но намерение размывается, а в трассе появляется шум.

Один round-trip, не больше. Проверка выполняется на большинстве выдач, и всё дорогое здесь оплачивает каждый запрос.

Проверяйте соединение, а не схему. SELECT 1 и PING отвечают «сокет жив». Проверка, которая трогает таблицу, провалится и при её блокировке — и пул начнёт увлечённо выселять совершенно исправные соединения.

Свежее соединение не проверяется

У только что созданного соединения метка последнего использования — момент создания, поэтому оно попадает в окно aliveBypassWindow и выдаётся без проверки. Проверять сокет, который сами же открыли секунду назад, значит платить за рукопожатие дважды.

close()

Освобождает ресурс. Ошибки здесь пул игнорирует: соединение и так уходит, а исключение только скрыло бы причину, по которой его закрывают.

Некоторым драйверам делать нечего — PDO закрывается при разыменовании, и пустое тело честнее выдуманного вызова. Другим нужен явный close().

Полный пример: Redis

src/RedisFactory.php
<?php

use Flytachi\Winter\CPool\ConnectionFactory;

final readonly class RedisFactory implements ConnectionFactory
{
  public function __construct(
      private string $host,
      private int    $port = 6379,
      private string $password = '',
      private int    $database = 0,
  ) {}

  public function create(): object
  {
      $redis = new \Redis();
      $redis->connect($this->host, $this->port, 2.0);

      if ($this->password !== '') {
          $redis->auth($this->password);
      }
      $redis->select($this->database);

      return $redis;
  }

  public function validate(object $connection): bool
  {
      try {
          return $connection->ping() !== false;
      } catch (\Throwable) {
          return false;
      }
  }

  public function close(object $connection): void
  {
      try {
          $connection->close();
      } catch (\Throwable) {
          // уже закрыто — делать нечего
      }
  }
}

Чего адаптер делать не должен

Кэшировать соединение. Адаптер создаёт, пул владеет. Адаптер, запоминающий свой результат, превращает пул из десяти соединений в одно, разделённое на десятерых — ровно тот баг, ради которого пул и существует.

Считать, что его вызовут один раз. create() работает при каждом росте пула и после каждого выселенного соединения. Всё одноразовое — чтение конфигурации, резолв хоста — место в конструкторе.

Хранить состояние запроса. Адаптер живёт дольше любого заёмщика. Идентификатор арендатора или контекст пользователя, захваченный в него, утечёт в соединения чужих запросов.

Дальше