PHP выполнить shell команду синхронно с timeout

Задача которую решаем — возможность из-под PHP-кода выполнить произвольную shell-команду (как через shell_exec) с возможностью задать timeout на команду.

Для чего задавать timeout для shell команды?

Timeout нужен в случае, если shell_exec на выполнении команды просто подвисает. Мы не можем никаким образом обернуть функцию shell_exec чтобы прервать через заданное время и выбросить исключение.

Если shell_exec() запущен, то PHP интерпретатор будет ждать завершения. Timeout нужен в случае, когда команда, которую мы запускаем, может или повиснуть вовсе, или отрабатывать дольше, чем мы хотим ждать результат. Если для CLI долгое время ожидания не так критично, то для веба отвечать 504 Gateway Timeout будет не самым лучшим вариантом.

Вариант 1: запуск команды через exec либо shell_exec

shell_exec не позволяет ограничить время своего выполнения, то есть не дает ограничить время ожидания ответа от команды.

Представленный ниже PHP код будет работать отлично, если запускаемая внутри функции exec команда работает отлично. В зависимости от тела команды в exec, данный код может создавать проблемы из-за отсутствия timeout. sleep 40 приведен для примера. Внутри может быть запуск произвольной команды.

<?php

declare(strict_types=1);

$command = 'sleep 40';

//this call can cost a fortune depending on $command and what happens inside the command' execution
//Long execution can cause problems in case of php-fpm (for example 504 Bad Gateway response)
exec($command, $output, $code);

echo 'Exit Code: ' . $code . PHP_EOL;
echo 'Output: ' . implode(PHP_EOL, $output);

Вариант 2: запуск команды используя symfony/process с timeout

Можно использовать возможности пакета symfony/process для явного указания timeout на команду. Пример:

<?php

declare(strict_types=1);

use Symfony\Component\Process\Process;

require_once 'vendor/autoload.php';

$command = 'sleep 40';

$process = Process::fromShellCommandline($command, timeout: 5);

// mustRun is the same as run but will throw exception if exit status != 0
$process->mustRun();

echo 'Output: ' . $process->getOutput();

В вызове fromShellCommandline явно передаю в аргументе желаемый timeout ожидания команды (в данном примере 5 сек).

Когда может быть нужно ограничивать время выполнения shell команды:

  • Если команда может отрабатывать непозволительно долго. Сталкивался с бинарями (вероятно, багованными), при подаче на вход которых не ожидаемых данных наблюдался бесконечный его процессинг, то есть бесконечное ожидание. Не всегда есть возможность исправить и пересобрать бинарь. Приходится фиксить уровнем выше, то есть ставить timeout на shell_exec в PHP и перехватывать ошибки.
  • При выполнении бинаря из-под php-fpm всё становится гораздо критичней. По причине того, что при "подвисании" сторонней команды запрос будет возвращен веб-сервером со статусом 504 Bad Gateway, а PHP процесс надолго будет занят. В случае, если запуск такого бинаря происходит внутри транзакции, то "при подвисании" дело не дойдет ни до commit, ни до rollback. При использовании MySQL, ряды останутся залоченными на время wait_timeout секунд, после которых rollback произойдет автоматически и лок снимется. При этом (пока wait_timeout не закончился), другие запросы к уже залоченным рядам будут ждать innodb_lock_wait_timeout секунд, прежде чем словить что-то вроде этого: "ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction". Старайтесь избегать какие-либо exec внутри запущенной SQL-транзакции.
     
 
 
 

icon Комментарии 0

Ваш комментарий к статье.. (для авторизованных)

ctrl+enter

icon Вход в систему

зарегистрироваться
НОВЫЕ ПОЛЬЗОВАТЕЛИ