Пишу, по большей части, про историю, свою жизнь и немного про программирование.

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

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

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

2 комментария
Роман Парпалак 2020

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

$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. БД перестает обрабатывать код, выбрасывается исключение. Его можно обработать на свое усмотрение.

Евгений Степанищев 2020

А вы пулер (pooler) не используете или это выше всё в транзакцию обёрнуто?

Роман Парпалак 2020

Нет, это фоновый процесс (консьюмер RabbitMQ), в нем пулер не особо нужен.

Евгений Степанищев 2020

Понятно, у нас ситуация другая, без пулера никуда, поэтому способ с установкой таймаута через команду Постгресу не заработает. Вроде кто-то из сообщества пилил родной кулер для Постгреса, интересно сможет ли он учитывать такие нюансы?