Тут нам надо просто подойти логически к вопросу. Понимаем, что RoadRunner держит приложение Symfony в памяти. Значит, в памяти будет находиться и сервис-контейнер со всеми инициализированными сервисами. Поэтому дефолтное подключение для Doctrine не подойдёт, потому что соединение упадёт, и оно будет выдавать ошибку.
Т.е. нам надо написать свою обёртку вокруг доктриновского класса, предоставляющего соединение с базой данных. И в этой обёртке мы и сделаем логику псевдо-пула соединений. А именно, перед каждым запросом будем пинговать соединение, и если оно мертво, то пробовать реконнектиться и продолжать запросы далее в случае успешного реконнекта. Это самое простое, но не самое эффективное решение.
1. Надеюсь, у вас уже установлен бандл для работы с Roadrunner
composer require spiral/roadrunner-bundle
2. Создаём вот эту вот обёртку над соединением
<?php
// src/DBAL/ConnectionWrapper.php
namespace App\DBAL;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Configuration;
use Doctrine\Common\EventManager;
class ConnectionWrapper extends Connection
{
/**
* @var int
*/
private $reconnectAttempts = 3;
public function __construct(array $params, Driver $driver, ?Configuration $config = null, ?EventManager $eventManager = null)
{
parent::__construct($params, $driver, $config, $eventManager);
}
public function prepare($sql)
{
$this->ping();
return parent::prepare($sql);
}
public function executeQuery(string $sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null)
{
$this->ping();
return parent::executeQuery($sql, $params, $types, $qcp);
}
public function executeUpdate(string $sql, array $params = [], array $types = [])
{
$this->ping();
return parent::executeUpdate($sql, $params, $types);
}
public function ping(): bool
{
if ($this->isConnected() && $this->getWrappedConnection()->ping()) {
return true;
}
$this->close();
for ($i = 0; $i < $this->reconnectAttempts; $i++) {
try {
$this->connect();
if ($this->getWrappedConnection()->ping()) {
return true;
}
} catch (\Exception $e) {
// ещё разок
}
}
throw new \RuntimeException("Unable to reconnect to the database.");
}
}
3. Конфигурируем Доктрину, чтобы использовала нашу обёртку.
# config/packages/doctrine.yaml
doctrine:
dbal:
# обычные настройки
url: '%env(resolve:DATABASE_URL)%'
driver: 'pdo_mysql'
server_version: '8.0'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
# наша обёртка
wrapper_class: App\DBAL\ConnectionWrapper
4. Что ещё
- Обязательно очищайте EntityManager вручную после каждого запроса по API, или каждого большого действия, или что там у вас ещё. Иначе доктриновская Identity Map распухнет, и память будет течь как мороженное в пустыне.
- Не забывайте аккуратно обращаться с транзакциями и всегда их коммитить или откатывать.
- Продумайте, как лучше всего рестартовать сервис, если реконнект всё-таки не сработает.
5. Лучше, конечно, сделать всё через воркеры RoadRunner.
Это сложнее, но более правильно, если так можно выразиться. Тут уже сами разберитесь в документации к этим вот пакетам, там нужно повозиться.
composer require spiral/roadrunner-symfony
composer require spiral/roadrunner-doctrine
Но идея та же, нужно написать обёртку для Доктрины, плюс использовать воркер RoadRunner. Там будет уже более адекватный connection pool и т.д.