Что делать, если нам нужно добавить возможность выполнять какие-то задачи в фоновом режиме, вроде отправки уведомлений в APNs для подвернувшегося под руку сайта на PHP?

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

Неленивый программист пойдет изобретать очередного монстра на двух колесах из pcntl_fork() и их друзей (против которых я не имею ничего против), который на его любимом языке делает всё то, что уже десять раз написано, испытано и проверено на других, более подходящих для этой задачи, языках, наступив при этом на всевозможные грабли, которые разложены на пути к надежным демонам.

Ленивый программист попробует решить ту же задачу минимальными средствами, то есть максимально используя существующие надежные и проверенные средства и инструменты.

Что у нас есть сегодня?

  • Пользовательский режим в systemd, который имеет не слишком понятную настройку, и, прямо скажем, совершенное неочевидно что нам подходит. И, к тому же, systemd есть пока ещё не везде, а значит выбрать его мы не можем.

  • Supervisor - менеджер пользовательских процессов, который делает ровно то, что нужно, существует с незапамятных времен и активно поддерживается сообществом.

Выбор очевиден.

Установим и настроим Supervisor

Установка Supervisor проще быть не может:

apt install supervisor

Настройка Supervisor состоит из двух частей: конфигурационного файла supervisord и самой программы, предназначенной для постоянного выполнения в фоне.

Конфиг сервиса

Конфигурационные файлы сервисов Supervisor находятся в /etc/supervisor/conf.d. Логично будет назвать сервис как поддомен для сайта, к которому этот сервис относится, то есть для example.com это будет service.example.com.conf. Если же сервис один, то можно не усложнять и назвать его как самого пользователя.

Если задачи выполняются от пользователя example.com из корня сайта который живет в домашнем каталоге этого пользователя, то конфигурационный файл получается следующий:

[program:service.example.com]
user = example.com
command = php bin/run-service.php
directory = /home/example.com/www
numprocs = 1
autorestart = true
autostart = true
stdout_logfile = /home/example.com/www/logs/supervisor_service.log
stderr_logfile = /home/example.com/www/logs/supervisor_service_errors.log
stopwaitsecs = 60

С этим конфигом для запуска сервиса Supervisor сначала перейдет в /home/example.com/www, затем запустит команду php bin/run-service.php и будет следить за её работой, перезапуская при падении. Описание всех директив можно найти в официальной документации.

Чтобы Supervisor увидел новый сервис нужно перезапустить его самого.

sudo service supervisor restart

Код сервиса

Если забыть о подключении зависимостей включением vendor/autoload.php и собственно выполнении работы, то самая минимально-работоспособная программа должна уметь корректно заканчивать работу при получении сигнала остановки SIGTERM от Supervisor.

Такая самая простая программа будет выглядеть следующим образом.

<?php
// флаг остановки
$shallStopWorking = false;

// сигнал об остановке от supervisord
pcntl_signal(SIGTERM, function () use (&$shallStopWorking) {
    echo "Received SIGTERM\n";
    $shallStopWorking = true;
});

// обработчик для ctrl+c
pcntl_signal(SIGINT,  function () use (&$shallStopWorking) {
    echo "Received SIGINT\n";
    $shallStopWorking = true;
});

echo "Started\n";

while (!$shallStopWorking) {

    // обрабатываем задания из очереди, считаем статистику чего-либо,
    // или делаем ещё что-то очень важное

    // или совершенно бесполезное
    for ($i = 0; $i < 10; $i += 1) sleep(1);
    echo "Slept for ten seconds\n";

    // обработаем сигналы в конце итерации
    pcntl_signal_dispatch();
}

echo "Finished\n";

Обработчик для Ctrl+С нужен для удобства отладки при ручном запуске программы из консоли.

Сигналы мы обрабатываем в конце цикла с тем, чтобы выполнение текущей задачи не прерывалось. К тому же, Supervisor будет ждать нас до 60 секунд согласно значения директивы stopwaitsecs прежде чем предпримет более серьёзные меры.

Проверяем!

Для проверки перезапустим сервис и остановим его, подождав немного:

supervisorctl restart service.example.com 
supervisorctl stop service.example.com

В логе supervisor_service.log после этого мы увидим следующее:

Started
Slept for ten seconds
Slept for ten seconds
Slept for ten seconds
Received SIGTERM
Finished
Started

Что ещё нужно иметь ввиду?

После обновления исходных кодов нужно обязательно перезапускать все подобные сервисы чтобы они использовали новую кодовую базу. Это можно делать прямо из хука post-receive или из Makefile, если такой используется для сборки ресурсов после выгрузки новый версии.

sudo /usr/bin/supervisorctl restart service.example.com

К сожалению, работа с supervisord требует повышенных привелегий, потому стоит дать возможность всем заслуживающим того пользователям возможность перезапускать то, что им потребно, через sudo без запроса пароля.

%users ALL = NOPASSWD: /usr/bin/supervisorctl restart *