Выполнение запроса с таймаутом
В продукте, который мы делаем в нашей компании, не используется PDO для соединения с базой данных — так исторически сложилось и вряд ли имеет смысл менять. Причина тому — оптимизации; мы широко используем эскуэль и задействуем диалектные особенности используемой СУБД (в нашем случае это «Постгрес»). То есть универсальность PDO нам ничего бы не дала.
Недавно возникла задача выполнения запроса с таймаутом — если выполнение запроса занимает больше времени, чем по нашему мнению пользователь готов ждать, то лучше прервать запрос и показать заглушку. В PDO кажется такая возможность есть (параметр можно задать, но мы не тестировали), а в постгресовском модуле для ПХП готового способа не оказалось.
Мы долгое время перебирали идеи (вплоть до внешнего процесса с таймером), но ничего не выглядело достаточно хорошо, чтобы помещать это в код. В конечном счёте мой братишка предложил очень простое решение. Ответ лежал в работе с асинхронным АПИ. Обычно это выглядит как-то так:
$con = pg_pconnect("…");
if ($con === false) {
throw new \RuntimeException('Could not connect');
}
// асинхронно выполняем запрос, в синхронном варианте тут было бы pg_query
if (pg_send_query($conn, "select * from users")) {
// тут можно делать что угодно, пока мы не будем готовы принять результат
// …
// синхронно получаем результат
var_dump(pg_get_result($conn));
}
Поскольку pg_send_query неблокирующий, то можно было бы попытаться как-то проконтролировать не истёк ли таймаут. Но как это сделать? Вот что он придумал:
function waitForResult($connection, callable $callback, int $timeout): void
{
$stream = pg_socket($connection);
while (pg_connection_busy($connection)) {
$read = [$stream];
$write = $except = null;
$numChangedStreams = stream_select($read, $write, $except, 0, $timeout);
if ($numChangedStreams > 0 || $numChangedStreams === false) {
break;
}
$callback();
}
}
После отсылки запроса мы получаем сетевой сокет соединения с базой данных и ожидаем в нём появления данных для чтения. Если за таймаут этого не происходит (или возникает ошибка), то цикл ожидания прерывается, иначе вызывается функция обратного вызова для получения порции данных.
У нас недавно была такая же задача. Коллега решил другим способом:
$this->connection->executeQuery(sprintf(’SET statement_timeout TO %d;’, self::TIMEOUT_MILLISECONDS));
try {
$data = $this->connection->executeQuery($sql, $params)->fetchAll();
} catch (\Exception $e) {
// ...
} finally {
$this->connection->executeQuery(’SET statement_timeout TO 0;’);
}
Это синтаксис DBAL, обертка над PDO. БД перестает обрабатывать код, выбрасывается исключение. Его можно обработать на свое усмотрение.
А вы пулер (pooler) не используете или это выше всё в транзакцию обёрнуто?
Нет, это фоновый процесс (консьюмер RabbitMQ), в нем пулер не особо нужен.
Понятно, у нас ситуация другая, без пулера никуда, поэтому способ с установкой таймаута через команду Постгресу не заработает. Вроде кто-то из сообщества пилил родной кулер для Постгреса, интересно сможет ли он учитывать такие нюансы?