Предыстория
В процессе изучения работы 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.
Запуск 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/).
Однако все они показались мне слишком замудрёнными и требующими какого-то прям длительного и глубокого изучения их возможностей прежде чем что-то осмысленно с их помощью делать.
Да и как-то это, наверное, перебор — использовать таких тяжеловесов лишь для того, чтобы с нуля не создавать простого демона. :-)
Хотя… ;-)