Свой адаптер
Адаптер — единственное место, знающее, что лежит в пуле. Три метода, и почти вся сложность сосредоточена в одном из них.
Контракт
interface ConnectionFactory
{
public function create(): object;
public function validate(object $connection): bool;
public function close(object $connection): void;
}create()
Открывает одно соединение и возвращает его. Может бросать — пул завернёт любое
исключение в PoolException::connectFailed(), поэтому вызывающий видит один тип
независимо от драйвера.
Здесь делается полное рукопожатие: подключение, аутентификация, выбор базы, сессионные параметры. Недонастроенное соединение, отданное пулу, станет недонастроенным соединением у случайного заёмщика позже.
validate() — здесь ошибаются чаще всего
Отвечает на один вопрос: сработал бы сейчас запрос на этом соединении?
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
<?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() работает при каждом росте пула и
после каждого выселенного соединения. Всё одноразовое — чтение конфигурации,
резолв хоста — место в конструкторе.
Хранить состояние запроса. Адаптер живёт дольше любого заёмщика. Идентификатор арендатора или контекст пользователя, захваченный в него, утечёт в соединения чужих запросов.
Дальше
- Политика —
aliveBypassWindowрешает, как часто зовутvalidate() - Справочник API — сигнатуры и исключения