Выполнение запроса с таймаутом

В продукте, который мы делаем в нашей компании, не используется 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();
    }
}

После отсылки запроса мы получаем сетевой сокет соединения с базой данных и ожидаем в нём появления данных для чтения. Если за таймаут этого не происходит (или возникает ошибка), то цикл ожидания прерывается, иначе вызывается функция обратного вызова для получения порции данных.

Читайте также

Неблокирующий режим файлов в PHP
Позже я внимательно прочитал документацию на теме. По всей видимости, это ничего не даст в отношении проверки системы хранения на зависание.
2022

Комментарии 4

Роман Парпалак № 1
У нас недавно была такая же задача. Коллега решил другим способом:

$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. БД перестает обрабатывать код, выбрасывается исключение. Его можно обработать на свое усмотрение.
Роман Парпалак № 3
Нет, это фоновый процесс (консьюмер RabbitMQ), в нем пулер не особо нужен.
Евгений Степанищев автор № 4
Понятно, у нас ситуация другая, без пулера никуда, поэтому способ с установкой таймаута через команду Постгресу не заработает. Вроде кто-то из сообщества пилил родной кулер для Постгреса, интересно сможет ли он учитывать такие нюансы?