Результаты работы примера совместного использования Workerman и Symfony Messenger.
Результаты работы примера совместного использования Workerman и Symfony Messenger.

Предыстория

В процессе изучения работы PHP компонента Symfony Messenger (https://symfony.com/doc/current/components/messenger.html) мной был создан самодостаточный пример совместной работы Symfony Messenger и Symfony Console, подробно описанный в статье https://habr.com/ru/articles/817425/.

Для демонстрации работы этого примера нужно было вручную запустить несколько консолей (терминалов), а потом в каждой вручную запустить Worker.
Мой внутренний перфекционист :-) сильно против этого возражал и говорил «а вот бы все эти консоли-терминалы запускались одной командой, в нужном количестве, сразу с Worker’ами, а если какой Worker упадёт, то заново запускались в нужном количестве».

Возражать своему внутреннему перфекционисту я не стал и создал ещё один пример работы Symfony Messenger, который запускается Worker’ами из PHP фреймворка Workerman (https://github.com/walkor/workerman). При этом Symfony Console вообще не используется.

Подробнее о Workerman можно узнать здесь: https://manual.workerman.net/doc/ru/ (описание на русском языке).

Сам пример использования Symfony Messenger и Workerman

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

Сам пример можно взять отсюда:
https://github.com/balpom/symfony-messenger-and-workerman

Либо можно установить через Composer:
composer create balpom/symfony-messenger-and-workerman

Как запустить пример

После установки откройте консоль и перейдите в созданную Composer'ом директорию symfony-messenger-and-workerman.
Выполните команду:
php bin/start
Эта команда запустит три простых Worker’а, имитирующих отправку SMS. Сейчас они ждут, когда в очереди появятся сообщения.
Количество Worker’ов настраивается в файле bin/runner.

Выполните команду:
php tests/sendmany.php
Эта команда запустит простой скрипт, который добавит в очередь несколько десятков сообщений.
После этого в ранее открытых консолях можно увидеть, как Worker'ы совместно "отправляют" SMS, берущиеся ими из очереди.

Выполните команду:
php bin/reload
Все Worker’ы "доотправят" взятые ими в работу SMS, завершат работу, запустятся заново и продолжат "отправлять" SMS (если они есть в очереди).

Выполните команду:
php bin/stop
Все Worker’ы "доотправят" взятые ими в работу SMS и завершат работу.

Тонкости реализации и выявленные недостатки

Призрак Symfony Console

Да, от компонента Symfony Console мы избавились. Однако его бледная тень в нашем примере незримо присутствует.
Дело в том, что класс SymfonyWorker, являющийся некой обёрткой для класса Symfony\Component\Messenger\Worker, примерно наполовину сделан на основе метода execute класса ConsumeMessagesCommand (Symfony\Component\Messenger\Command\ConsumeMessagesCommand).
Ну да, ничего умнее придумать не смог… ;-)

"Запускатор" для Workerman\Worker

Скрипт, запускающий аж целый asynchronous event-driven PHP framework with high performance, прост до безобразия и я позволю себе привести его здесь почти целиком (чуть позже мне это будет нужно ещё и для более простого описания выявленных недостатков).

 // bin/runner
namespace Balpom\SymfonyMessengerWorkerman;
use Workerman\Worker;
use Symfony\Component\Process\Process;
Worker::$daemonize = true; // Always run as daemon.
$worker = new Worker();
$worker->count = 3;        // Numbef of Workers.
// SymfonyWorkerFactory::getWorker(DIR . '/../config/dependencies.php')->run();
$process = new Process(['gnome-terminal', '--', 'php', 'bin/start_worker']);
$process->run();
};
Worker::runAll();

Прям "из коробки" эта конструкция понимает говорящие сами за себя команды
php bin/runner start
php bin/runner reload
php bin/runner stop

и некоторые другие (см. https://manual.workerman.net/doc/ru/install/start-and-stop.html).

Чтобы при запуске Workerman в режиме демона у команды "start" не указывать опцию "-d", в скрипте прописано Worker::$daemonize = true.

Работа команд php bin/runner start, php bin/runner reload и php bin/runner status. В "Системном мониторе" видны соответствующие PHP-процессы.
Работа команд php bin/runner start, php bin/runner reload и php bin/runner status.
В "Системном мониторе" видны соответствующие PHP-процессы.
Работа команды php bin/runner stop. В "Системном мониторе" не видно ни одного PHP-процесса. Команда php bin/runner status после остановки Workerman'а ничего не выводит.
Работа команды php bin/runner stop.
В "Системном мониторе" не видно ни одного PHP-процесса.
Команда php bin/runner status после остановки Workerman'а ничего не выводит.

Запуск Worker’ов "под микроскопом"

Если вы чуть более внимательно посмотрите на вышеприведённый код "запускатора", то увидите, что в своём примере Worker’ы, которые Worker’ы Symfony Messenger, а не Workerman’а, ;-) запускаются из Gnome Terminal.
Соответственно, если он у вас не установлен, то данный пример вам придётся адаптировать под ваши реалии.
Да, запускать всё это под Windows я не пробовал. Чёрт его знает, может, и будет работать, если как-то на запуск через команду "start" переделать...

Файл "bin/start_worker" запускает Symfony Worker и выглядит так:
namespace Balpom\SymfonyMessengerWorkerman;
SymfonyWorkerFactory::getWorker('/../config/dependencies.php')->run();

Файл "bin/stop_worker" останавливает Symfony Worker и выглядит так:
namespace Balpom\SymfonyMessengerWorkerman;
SymfonyWorkerFactory::getWorker('/../config/dependencies.php')->stopWorkers();

Файлы "bin/start" и "bin/stop" — это некий синтаксический сахар (чтобы поменьше символов в консоли набирать ;-) ) и выглядят они так:
// bin/start
use Symfony\Component\Process\Process;
$process = new Process(['php', 'bin/runner', 'start']);
$process→run();

// bin/stop
$process = new Process(['php', 'bin/stop_workers']);
$process->run();
$process = new Process(['php', 'bin/runner', 'stop']);
$process->run();

Ну хорошо, хорошо… "bin/stop" - не совсем синтаксический сахар…
Как видно, он отдельно даёт команду на остановку Worker’ов Symfony Messenger, а потом уже Worker’ов Workerman’а.
Ну да, ничего умнее не придумал… ;-)

Файл "bin/reload" даёт команду на остановку Worker’ов Symfony и Worker’ов Workerman’а, а потом заново запускает workerman:
// bin/reload
$process = new Process(['php', 'bin/stop']);
$process->run();
$process = new Process(['php', 'bin/runner', 'start']);
$process->run();

Worker’ы Symfony, запущенные внутри терминалов, работают не так, как ожидалось

Да, конечно, возможность наблюдать работу Worker’ов в окнах терминалов — это наглядно и позволяет контролировать весь процесс.
Однако при тестировании этого всего столкнулся с тем, что если по каким-то причинам (не важно по каким — может, по таймауту / по числу обработанных message’s да или просто по kill) Worker Symfony Messenger прекратит свою работу, то новые консоли с Worker’ами Symfony не открываются.
При этом в «Системном мониторе» видно, что соответствующие PHP-процессы Worker’ов Workerman’а вполне себе живы-здоровы и умирать не собираются (если б умерли — то были бы автоматически перезагружены Workerman’ом и консоли бы открылись).
Как-то "правильно" запускать Workerman’ом из терминала Worker’ы Symfony у меня так и не получилось… :-(

Так как в реальных задачах вряд ли есть необходимость запускать Worker’ы именно в терминалах, то я не сильно-то и расстроился из-за вышеописанного непонятного поведения Worker’ов.
Тем более, что при запуске Worker’ов Symfony напрямую (закомментированная строка в коде "запускатора") они вполне себе работают как положено (ну да, работу по "отправке" не видно, ну да, можно было вывод не в консоль, а в файл выводить, но мне лень стало дальше этот пример усложнять пилить ;-) ).

Послесловие

Используя Workerman (https://manual.workerman.net/doc/ru/), я смог как нефиг делать создать простого демона простым и понятным образом.

Я осведомлён о существовании AMPHP (https://amphp.org/), ReactPHP (https://reactphp.org/) и Swoole (https://openswoole.com/).
Однако все они показались мне слишком замудрёнными и требующими какого-то прям длительного и глубокого изучения их возможностей прежде чем что-то осмысленно с их помощью делать.
Да и как-то это, наверное, перебор — использовать таких тяжеловесов лишь для того, чтобы с нуля не создавать простого демона. :-)

Хотя… ;-)

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


  1. vladdnepr
    04.06.2024 08:03
    +2

    Однако при тестировании этого всего столкнулся с тем, что если по каким-то причинам (не важно по каким — может, по таймауту / по числу обработанных message’s да или просто по kill) Worker Symfony Messenger прекратит свою работу, то новые консоли с Worker’ами Symfony не открываются.
    При этом в «Системном мониторе» видно, что соответствующие PHP-процессы Worker’ов Workerman’а вполне себе живы-здоровы и умирать не собираются (если б умерли — то были бы автоматически перезагружены Workerman’ом и консоли бы открылись).

    Особенность запуска с прослойкой в виде bash

    https://gist.github.com/portante/e81bc6b8e7560a6b3d9dd1acfdd4d427

    Нужно прокинуть через bash до php прохождение posix signal


  1. balpom Автор
    04.06.2024 08:03

    Нужно прокинуть через bash до php прохождение posix signal

    Огромнейшее спасибо за Ваш комментарий!

    Я прочитал всё, что изложено на странице по Вашей ссылке.

    Однако мне не хватает ума понять вот что:
    вот внутри себя Workerman запускает процесс gnome-terminal -- php bin/start_worker посредством PHP-функции proc_open, shell_exec или ещё как-то - неважно.
    Никак не пойму, каким боком тут оказывается bash... Исходный же скрипт, которым внутри себя Workerman запускает gnome-terminal - он же не bash, а php...

    И, соответственно, читая про все эти перехваты сигналов bash'евской функцией trap, никак не могу понять, каким боком оно применимо к моей ситуации.

    Я пробовал запускать и как bash -c "gnome-terminal -- php bin/start_worker", и как bash -c "gnome-terminal -- php bin/start_worker & wait" и ещё как-то.
    И из скрипта а-ля bash bin/gnome.sh
    #!/bin/bash
    gnome-terminal -- php bin/start_worker
    exit 0

    пробовал запускать...

    И оно даже запускается. Но всё также при прекращении работы Symfony Worker'ов запустившие их Workerman'овские Worker'ы не падают (и, соответственно, не перезагружаются Workerman'ом).

    Наверное, я в принципе не понимаю смысл фразы "прокинуть через bash до php прохождение posix signal"...
    Если можно - приведите, пожалуйста, пример такой волшебной команды (или иного шаманского действия).....


    1. vladdnepr
      04.06.2024 08:03

      Нужно запускать без прослойки в виде bash или gnome-terminal

      https://github.com/balpom/symfony-messenger-and-workerman/blob/main/bin/runner#L21

      Нужно просто почитать документацию как рекомендуют работать

      https://symfony.com/doc/current/components/process.html#using-features-from-the-os-shell
      https://symfony.com/doc/current/components/process.html#process-signals


  1. balpom Автор
    04.06.2024 08:03

    Нужно запускать без прослойки в виде bash или gnome-terminal

    Да это-то понятно... Без "прослойки" оно работает. С "прослойкой" хотелось для наглядности.

    В-общем, задачу запуска с "прослойкой" в виде Gnome Terminal я решил довольно элегантным абсолютно варварским способом: :-)

    $pid = posix_getpid(); // Current Workerman's Worker PID.
    $line = 'bash -c "gnome-terminal --wait -- php bin/start_worker; kill -SIGQUIT ' . $pid . '"';
    $process = Process::fromShellCommandline($line);
    $process->run();

    Что тут делается:

    • определяем PID текущего Workerman Worker'а

    • запускаем gnome-terminal, в котором запускаем Symfony Worker

    • следующей командой ставим kill, которому динамически подсовываем PID текущего Workerman Worker'а

    gnome-terminal обязательно запускаем с опцией --wait.
    Это важно, т.к. иначе сразу же исполняется следующая команда. А так терминал ждёт, пока завершится его дочерний процесс (Symfony Worker).

    Почему нельзя просто запустить терминал, без bash -c ?
    Я пробовал, но при этом вываливается ошибка
    sh: 1: kill: Illegal option -S
    in /var/www/ ... /vendor/symfony/process/Process.php:270

    Детально разбираться что к чему я не стал, но, глядя на текст ошибки, интуитивно понятно, что если запускать терминал не из bash ("напрямую"), то так каким-то образом используется оболочка sh, в которой, видимо, другой синтаксис. Ну и, соответственно, надо запускать через bash...

    В-общем, пример я обновил: https://github.com/balpom/symfony-messenger-and-workerman
    Теперь команды php bin/start и php bin/stop работают с Gnome Terminal как задумано. :-)

    Да: в процессе тестирования выяснилось, что старая версия bin/stop работает неправильно.
    Поменял местами команды остановки Worker'ов - теперь сначала останавливаются Worker'ы Workerman'а, а потом - Worker'ы Symfony.

    Как-то так...