Перехват управляющих сигналов в PHP. Graceful shutdown
версия для печати
Речь про сигналы, передваемые в Linux любому процессу: SIGINT, SIGKILL и пр. Эти сигналы доступны только в nix-подобных системах, базовая фича таких ОС. На Windows не будет работать почти наверняка. Я неправильно понимал, как работает, т.к. зашел не с той стороны. Сейчас разобрался, рассказываю. Картинка кликабельна. Автор: Daniel Stori — CC BY-NC-SA 4.0 |
Сигналы являются программными прерываниями, которые посылаются процессу, когда случается некоторое событие. Сигналы могут возникать синхронно с ошибкой в приложении, например SIGFPE (ошибка вычислений с плавающей запятой) и SIGSEGV (ошибка адресации), но большинство сигналов является. (с) OpenNET - Понятие о сигналах
Прим: под асинхронными тут понимаются сигналы, возникающие в произвольный момент времени, независимо от потока выполнения программы.
Полный список сигналов можно найти в Википедии. Есть еще сигналы POSIX, что как-то пересекается с сигналами UNIX, но так глубоко я уже не закапывался.
Перехват программных прерываний в PHP
В PHP для работы с управляющими сигналами есть расширение PCNTL. Но даже если мы ничего не будем писать на получение таких прерываний, запущенный скрипт все равно будет реагировать на них! Это меня сразу в тупик поставило. Зачем же тогда их слушать, когда и так все работает? А затем, чтобы написать какую-то реакцию в приложении на получение сигнала. Например, логировать факт остановки скрипта. Но это банально конечно :)
Тут уместна аналогия с исключениями: мы можем их ловить и как-то реагировать. А можем ничего не делать. И так же, как и с иключениями, можно продолжить выполнение программы, т.е. игнорировать полученный сигнал :o)
Сейчас мне интересны только SIGINT и SIGTERM. Это ассинхронные сигналы. Т.е. такой сигнал может прилететь в приложение в любой момент времени выполнения, что приведет к завершению приложения. И не важно, что там выполнялось в момент останова.
А теперь начинается самое интересное. Поскольку PHP не работает в несколько потоков, то проверять появление сигнала можно только между какими-нибудь действиями в основном коде. В древних версиях PHP (<=5.2) для этого использовалась директива declare(ticks = NN). Каждые NN низкоуровневых операций проверяем, есть ли управляющий сигнал.
Решение через тики было дорогим по времени выполнения. Поэтому в PHP 5.3 появилась возможность вручную проверять поступление сигналов. И это качественно изменило поведение программы, т.к. теперь реагировать на сигналы можно в тех местах кода, где мы этого хотим. Пример:
// Регистрируем функцию-обработчик на получение сигнала
pcntl_signal(SIGINT, static function() {
exit(PHP_EOL . 'Interrupted' . PHP_EOL);
});
// Демонстрационный код
for ($i = 0; $i < 10; $i++) {
echo date('h:i:s');
sleep(2);
echo ' pinned.' . PHP_EOL;
// Разбор входящих сигналов
pcntl_signal_dispatch();
}
Для проверки запустите этот код и нажмите Ctrl + C в любой момент его работы. Вероятный результат может быть таким:
vijit@dev: ~$ php some.php 10:51:39 pinned. 10:51:41 pinned. 10:51:43^C pinned. Interrupted
Видите, после отправки сигнала (^C) программа все еще работала. Т.е. когда бы вы не потребовали остановиться, она выполнит требование только там, где разбирает поступившие сигналы.
Дальше жизнь стала еще легче, т.к. с PHP 7.1 ввели pcntl_async_signals(). Я с трудом понял смысл этой функции, даже прочитав RFC.
На Хабре нашел такое объяснение:
эта функция позволяет интерпретатору не отвлекаться на проверку сигнала, по сути она ставит наш обработчик сигнала следующим в стек вызова за выполняемой функцией.
Понятнее не стало. В справочнике PHP тоже написано так, что я не сразу понял смысл.
Суть в том, что данная функция заменила declare(ticks = NN), т.е. автоматическую обработку ассинхронных сигналов, что положительно сказалось на производительности и косвенно на удобстве использования. Но эта функция конфликтует по логике использования с pcntl_signal_dispatch(), потому что обработка сигнала опять стала выполняться в момент его получения, но не когда нам это удобно. Это я так красиво подошел к следующей теме..
Graceful shutdown
Есть такая нужда: выполняем сложное действие с кучей данных, а тут прилетает сигнал прекратить работу. Поскольку мы его получили, можем корректно закончить задачу и завершиться. А если бы не слушали управляющие сигналы, то скрипт мог бы прерваться посредине выполняемой задачи. Корректное завершение и называется graceful shutdown.
Как я сказал чуть выше, чтобы корректно завершить работу, нельзя использовать pcntl_async_signals(true) (внимание на TRUE в параметре), т.к. реакция на сигнал будет происходить в момент его получения приложением.
Ну и что с того, не используй и живи спокойно. И в нормальных движках это сработало бы, но в рассово-правильном стало проблемой. Там хардкодом зашита автоматическая реакция в конструкторе Symfony\Component\Console\SignalRegistry\SignalRegistry. И я не нашел документированного решения, как это поведение изменить.
Собственно, этот опус родился в результате раскопок, почему у меня не работает graceful shutdown в новой Symfony. Измение появилось где-то в версии 5.2 или выше.
Костылька очевидна: в нужном месте своего кода выключать автоматическую реакцию и проверять управляющие сигналы там, где мы к этому готовы по логике приложения.
Менее очевидный костыль: в обработчике сигнала только устанавливать некий флаг, что нужно завершить работу. А проверять этот флаг уже где-то в коде. Возможная реализация:
class SomeService
{
private bool $mustShutdown = false;
public function __construct()
{
pcntl_signal(SIGINT, function () {
$this->mustShutdown = true;
});
}
public function exec(): void
{
for ($i = 0; $i < 10; $i++) {
echo date('h:i:s');
sleep(2);
echo ' pinned.' . PHP_EOL;
if ($this->mustShutdown) {
echo PHP_EOL . 'Interrupted' . PHP_EOL;
return;
}
}
}
}
pcntl_async_signals(true);
(new SomeService)->exec();
Мне такое решение не нравится по многим причинам: класс хранит состояние, что делает его объекты statefull; жесткая привязка реализуемой фичи на объект, где хранится флаг; подмена вызова pcntl_signal_dispatch() на проверку флага, что принципиально хуже; вынос реального обработчика за его пределы, что позволит не просто непредсказуемо изменить реализацию обработчика, но и в целом навешать на $mustShutdown массу логики в любом месте кода. Короче, это рабочее, но очень херовое решение, имхо.
Резюме
Изложение получилось субмурным. Попробую коротко:
- Если нужно просто прерывать выполнение программы, дополнительно писать ничего не требуется. Само все работает.
- Если нужна реакция на управляющий сигнал сразу при его получении, то через pcntl_signal() регистрируем обработчик сигнала и пишем в нем что-нибудь полезное. Далее разбор сигналов зависит от версии PHP:
- до v7.1 прописываем в скрипте declare(ticks = 1) или вручную разбираем сигналы через вызов pcntl_signal_dispatch().
- для версий >=7.1 включаем автоматическую обработку прерываний через pcntl_async_signals(true).
- Если нужна реакция на управляющий сигнал, но она должна быть отложенная (graceful shutdown), то на всякий случай отключаем автоматическую обработку - pcntl_async_signals(false), потом регистрируем обработчик через pcntl_signal() и в нужном месте разбираем сигналы через pcntl_signal_dispatch(). Скорее всего в обработчике ничего не будет, т.к. тут сама суть в выборе момента останова, а не в реакции на сигнал.
Кстати, по непроверенным данным нельзя повлиять на SIGKILL. Приложение будет остановлено немедленно. Я думаю, так сделано на случай корявого кода, когда другие прерывания игнорируются.
[1oo%, EoF]Понравилась статья? Расскажите о ней друзьям: