Symfony 5 запуск command фоновым процессом
После сохранения настроек пользователем, необходимо из контроллера запускать фоновым процессом комманд, который кроме этого висит на кроне. Необходимо обработать несколько ситуаций:
- Во-первых, если пользователь нажал кнопку сохранить, а через пару минут передумал, что то поменял, и снова нажал "сохранить", нужно убить процесс запущенный первым сохранением и запустить новый.
- Во-вторых, при запуске команда по крону нужно проверять, работает ли он в данный момент времени от контроллера и\или уже отрабатывал сегодня, то есть запускать только в том случае, если он сегодня еще не работал.
Пробую реализовать логику с помощью process component.
Код контроллера:
$query = $request->query->all();
$filename = $query['filename'];
$basePath = $this->getParameter('kernel.project_dir');
$process = Process::fromShellCommandline(
"php $basePath/bin/console test:test $filename",
);
$process->start();
$pid = $process->getPid()
Код комманда:
$filename = $input->getArgument('filename');
file_put_contents(__DIR__."/$filename.pid.log", posix_getpid() . PHP_EOL, FILE_APPEND);
for ($i = 1; $i <= 180; $i++) {
sleep(1);
file_put_contents(__DIR__."/$filename.log", $i . PHP_EOL, FILE_APPEND);
}
Процессы фоном запускаются и отрабатывают как надо. Вот только я никак не могу решить вопрос с проверкой запущен ли он сейчас. Думал как то писать в базу пиды и отлеживал по ним, но пиды из контроллера и комманда всегда разные, в комманде всегда больше на 1. К тому же проверку надо делать в комманде, так как контроллер не всегда будет присутствовать в обработке, а как убить процесс из комманда не могу понять. $process->stop() принимает ресурс который мне в комманде недоступен.
Ответы (2 шт):
2. Проверить, запущен ли на данный момент такой же процесс, который хотите породить:
Как возможное решение, вы можете запускать отдельный процесс, который вызовет unix команду/последовательность и перенаправит вывод в какой-то отдельный файл, далее, вы можете спокойно посмотреть в данный файл из вашей команды и определить дальнейшее поведение.
Следующая команда посмотрит список ваших запущенных процессов, постарается найти совпадение с "php $basePath/bin/console test:test $filename", исключит процесс grep-a из поиска и запишет в целевой файл столько строк, сколько будет найдено процессов или ни одной, если подобных нет (!файл будет перетираться, если хотите дописывать (append) то должно быть два якоря ">> путь к файлу"):
$process = Process::fromShellCommandline(
"ps aux | grep \"php $basePath/bin/console test:test $filename\" | grep -v grep > путь до файла сохранения вывода"
);
1. Убить процесс из контроллера, запущенный ранее из кода: Сделайте все то же самое, что описано во втором пункте, только вывод пишите в другой файл (чтобы не появилось состояния гонки), далее, вам нужно дописать немного кода по парсингу созданного файла, если он не пустой, чтобы разобрать из него pid и убить процесс.
Вы можете взять строку из файла, использовать preg_replace('\s+', ' ', $targetStr), далее $data = explode(' ', $targetStr) и взять значение pid-а по ключу, если имеется, $data[1].
Можно убивать:
if (isset($data[1])) {
$process = Process::fromShellCommandline(
sprintf('kill %s', $data[1])
);
}
Как в итоге я реализовал задачу.
Наш комманд export, который является основным процессом, принимает на вход 2 аргумента (int)clientId и (bool)runAnyway, и имеет логику такую:
$this->connectionId = $input->getArgument('connectionId');
$runAnyway = $this->runAnyway($input->getOption('runAnyway'));
$this->exportInfo = $this->wixExportInfoRepository->findOneBy(['connectionId' => $this->connectionId]);
// Если запрос пришел с контроллера и по этому клиенту он уже запущен в данный момент - убиваем запущенный процесс экспорта
if ($runAnyway && $pid = $this->exportInfo->getPid()) {
posix_kill($pid, 9); //SIGKILL
}
$this->wixExportInfoRepository->addPid($this->connectionId, posix_getpid());
$this->export();
$this->wixExportInfoRepository->deletePid($this->connectionId);
В одну из своих сущностей, которая хранит информацию об экспорте, я добавил колонку pid, которая говорит нам - если тут не null, то процесс по данному клиенту активен, если null, живых процессов нет. Собственно этот комманд и прокидывает туда свой pid в начале выполнения, и удаляет его по завершению.
Данный комманд может запускаться из контроллера с флагом runAnyway, который говорит нам убей все и начни сначала:
private function startExport($connectionId)
{
$exportProcess = Process::fromShellCommandline(
sprintf("php %s/bin/console vk:wix:export %s --runAnyway=%s",
$this->getParameter('kernel.project_dir'),
$connectionId,
true
)
);
$exportProcess->disableOutput()->start();
}
Так же на кроне у меня висит другой комманд, который делает из таблицы выборку всех клиентов у которых pid = null, и асинхронно запускает по каждому процесс:
$exportInfo = $this->em->getRepository(WixExportInfo::class)->getEntitiesForExport();
foreach ($exportInfo as $info) {
$exportProcess = Process::fromShellCommandline(
sprintf("php %s/bin/console vk:wix:export %s",
$this->rootDir,
$info->getConnectionId(),
)
);
$exportProcess->disableOutput()->start();
}
Чего я добился в итоге. Так как, с учетом количественных (отборный мат) ограничений api одного из сервисов, полный экспорт товаров может происходить в три этапа в течение 3-х дней. Мы в некоем подобии асинхронности запускаем последовательно этапы сразу по всем клиентам одновременно. При этом имеем возможность при изменении клиентом настроек экспорта, в любой момент прекратить текущий процесс экспорта и сразу же запустить его сначала с новыми настройками.