Magento 2: Планировщик задач
В Magento 2 предусмотрен планировщик задач. По сути планировщик, это php скрипт который запускается через cron (имеется ввиду утилиту Linux) каждую минуту. Просмотреть список команд, можно выполнив в консоли команду от имени пользователя от которого работает magento..
1 2 3 |
crontab -l |
выведет что-то типа
1 2 3 4 5 6 7 |
#~ MAGENTO START * * * * * /usr/bin/php /var/www/html/magento2/bin/magento cron:run | grep -v Ran jobs by schedule >> /var/www/html/magento2/var/log/magento.cron.log * * * * * /usr/bin/php /var/www/html/magento2/update/cron.php >> /var/www/html/magento2/var/log/update.cron.log * * * * * /usr/bin/php /var/www/html/magento2/bin/magento setup:cron:run >> /var/www/html/magento2/var/log/setup.cron.log #~ MAGENTO END |
установить эти команды в cron можно выполнив
1 2 3 |
php bin/magento cron:install |
удалить, выполнив
1 2 3 |
bin/magento cron:remove |
Итак, крон запускается каждую минуту. После запуска выполняется метод execute из класса CronCommand, который наследуется от класса Symfony\Component\Console\Command\Command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// vendor/magento/module-cron/Console/Command/CronCommand.php namespace Magento\Cron\Console\Command; use Symfony\Component\Console\Command\Command; ... class CronCommand extends Command { protected function execute(InputInterface $input, OutputInterface $output) { ... /** @var \Magento\Framework\App\Cron $cronObserver */ $cronObserver = $objectManager->create(\Magento\Framework\App\Cron::class, ['parameters' => $params]); $cronObserver->launch(); $output->writeln('<info>' . 'Ran jobs by schedule.' . '</info>'); } } |
В нем парсятся и проверяются входные данные и если все Ок, создается экземпляр класса \Magento\Framework\App\Cron у которого вызывается метод launch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// vendor/magento/framework/App/Cron.php public function launch() { $this->_state->setAreaCode(Area::AREA_CRONTAB); $configLoader = $this->objectManager->get(\Magento\Framework\ObjectManager\ConfigLoaderInterface::class); $this->objectManager->configure($configLoader->load(Area::AREA_CRONTAB)); $this->areaList->getArea(Area::AREA_CRONTAB)->load(Area::PART_TRANSLATE); /** @var \Magento\Framework\Event\ManagerInterface $eventManager */ $eventManager = $this->objectManager->get(\Magento\Framework\Event\ManagerInterface::class); $eventManager->dispatch('default'); $this->_response->setCode(0); return $this->_response; } |
Далее происходит создание события "default" и его выполнение. Любой модуль может подписаться на это событие. Как вы можете заметить, событие вызывается в зоне crontab (areaCode = crontab), поэтому подписку надо делать в файле <ModuleName>/etc/crontab/events.xml, например так:
1 2 3 4 5 6 7 |
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="catalog_product_get_final_price"> <observer name="catalogrule" instance="Magento\CatalogRule\Observer\ProcessAdminFinalPriceObserver" /> </event> </config> |
это событие запускается каждый раз когда вызывается php bin/magento cron:run, т.е. при настройках по-умолчанию - каждую минуту.
Одним из подписчиков на это событие является класс: Magento\Cron\Observer\ProcessCronQueueObserver, который и отвечает за запуск задач по расписанию.
Задачи конфигурируются с помощью фалов <ModuleName>/etc/crontab.xml расположенных в модулях. Пример:
1 2 3 4 5 6 7 8 9 |
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> <group id="default"> <job name="backend_clean_cache" instance="Magento\Backend\Cron\CleanCache" method="execute"> <schedule>30 2 * * *</schedule> </job> </group> </config> |
Обработка задач происходит в методе Magento\Cron\Observer\ProcessCronQueueObserver::execute()
В котором, в цикле происходит обход групп и для каждой группы выполняются два метода которые отвечают за очистку и наполнение таблицы cron_schedule.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php public function execute(\Magento\Framework\Event\Observer $observer) { $pendingJobs = $this->_getPendingSchedules(); $currentTime = $this->dateTime->gmtTimestamp(); $jobGroupsRoot = $this->_config->getJobs(); $phpPath = $this->phpExecutableFinder->find() ?: 'php'; foreach ($jobGroupsRoot as $groupId => $jobsRoot) { $this->_cleanup($groupId); // <<<<<<<<<<<<<<<< $this->_generate($groupId); // <<<<<<<<<<<<<<<< ... foreach ($pendingJobs as $schedule) { ... } } } |
В magento 2.3.0 этот метод немного поменялся, добавили сортировку групп, чтобы группы имеющие флаг Use Separate Process выполнялись первыми. Выглядит это так
1234567891011121314151617181920212223242526272829303132333435363738394041424344 public function execute(\Magento\Framework\Event\Observer $observer){$currentTime = $this->dateTime->gmtTimestamp();$jobGroupsRoot = $this->_config->getJobs();// sort jobs groups to start from used in separated processuksort($jobGroupsRoot,function ($a, $b) {return $this->getCronGroupConfigurationValue($b, 'use_separate_process')- $this->getCronGroupConfigurationValue($a, 'use_separate_process');});$phpPath = $this->phpExecutableFinder->find() ?: 'php';foreach ($jobGroupsRoot as $groupId => $jobsRoot) {if (!$this->isGroupInFilter($groupId)) {continue;}if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1'&& $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1) {$this->_shell->execute($phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '='. self::STANDALONE_PROCESS_STARTED . '=1',[BP . '/bin/magento']);continue;}$this->lockGroup($groupId,function ($groupId) use ($currentTime, $jobsRoot) {$this->cleanupJobs($groupId, $currentTime);$this->generateSchedules($groupId);$this->processPendingJobs($groupId, $jobsRoot, $currentTime);});}}
Формат у таблицы cron_schedule такой:
- schedule_id - номер записи
- job_code - id задачи (например: indexer_update_all_views)
- status - статус выполнения. Может быть:
- pending - задача запланирована на выполнение, дата запуска указана в колонке scheduled_at
- running - означает, что задача была запущена на выполнение и работает
- success - означает, что что задача была выполнена
- missed - означает, что прошлый запуск этой задачи не завершился, до момента запуска текущей задачи. Другими словами в момент запуска этой задачи, она проверила эту таблицу и нашла запись с таким же job_code и статусом running. Если после этого задача так и не смогла запуститься в период указанный в настройках, например 5 минут, то она помечается, как missed. Период, задается в админке:
123Magento Admin > Store > Configuration > Advanced > System > Cron (Scheduled Tasks) > Cron configuration options > Missed If Not Run Within - error - указывает, что выполнение прошло с ошибкой. Пояснения к ошибке находятся в колонке messages.
- messages - поясняющее сообщение, используется в случае ошибок
- created_at - время, когда задача добавлена в таблицу
- scheduled_at - время, когда запланировано выполнение
- executed_at - время, когда задача была запущена на выполнение
- finished_at - время, когда задача закончила работу
Для каждой из задач в файле /etc/crontab.xml задается группа
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- vendor/magento/module-catalog/etc/crontab.xml --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd"> <group id="default"> <!-- <<<<<<<<<<<<<<<<<<<<<<<<<<< --> <job name="catalog_index_refresh_price" instance="Magento\Catalog\Cron\RefreshSpecialPrices" method="execute"> <schedule>0 * * * *</schedule> </job> ... </group> </config> |
Из коробки, у нас есть 4е группы:
- default - основная группа
- indexer - индексаторы
- consumers - группа очереди сообщений модуль Magento_MessageQueue
- ddg_automation - группа dotmailer модуля (dotdigital Engagement Cloud)
Например, в группу index входят задачи с такими id:
1 2 3 4 5 6 |
indexer_clean_all_changelogs indexer_reindex_all_invalid indexer_update_all_views magento_targetrule_index_reindex |
Если вам необходимо создать свою группу, например для того чтобы запускать интеграции со сторонними сервисами, то делается это с помощью файла <ModuleName>/etc/cron_groups.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron_groups.xsd"> <group id="example2"> <schedule_generate_every tooltip="Be careful changes in schedule may affect date picker for scheduled updates">1</schedule_generate_every> <schedule_ahead_for>4</schedule_ahead_for> <schedule_lifetime>2</schedule_lifetime> <history_cleanup_every>10</history_cleanup_every> <history_success_lifetime>60</history_success_lifetime> <history_failure_lifetime>600</history_failure_lifetime> <use_separate_process>1</use_separate_process> </group> </config> |
Далее используете id новой группы в файле <ModuleName>/etc/crontab.xml
Наиболее просто найти список всех задач и их расписание, можно при помощи утилиты n98-magerun2: https://github.com/netz98/n98-magerun2
Сделать это можно с помощью команды: php n98-magerun2.phar sys:cron:list
Для каждой из групп в админке задаются настройки. Найти настройки можно тут
1 2 3 |
Magento Admin > Store > Configuration > Advanced > System > Cron (Scheduled Tasks) |
Разберемся с настройками для групп
- Generate Schedules Every - добавлять задачи в таблицу cron_schedule каждые N минут
- Schedule Ahead for - время которое будет добавлено к времени scheduled_at
- Missed if Not Run Within - время после истечения которого задача будет отмечена как missed, в случае если в списке есть задача в статусе running. Т.е. если задача не была выполнена в течении N минут, после scheduled_at, она будет отмечена как missed. Задача не может быть выполнена, в случае, если уже есть другая задача с тем же job_code в статусе running.
- History Cleanup Every - История задач будет очищаться, каждые N минут
- Success History Lifetime - История задач со статусом success будет очищаться каждые N минут
- Failure History Lifetime - История задач со статусами missed и error будет очищаться каждые N минут
- Use Separate Process - Использовать отдельный процесс для запуска задач из этой группы.
На Use Separate Process остановимся подробнее. Эта опция используется в классе Magento\Cron\Observer\ProcessCronQueueObserver, в методе execute
и выглядит так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php public function execute(\Magento\Framework\Event\Observer $observer) { $pendingJobs = $this->_getPendingSchedules(); $currentTime = $this->dateTime->gmtTimestamp(); $jobGroupsRoot = $this->_config->getJobs(); $phpPath = $this->phpExecutableFinder->find() ?: 'php'; foreach ($jobGroupsRoot as $groupId => $jobsRoot) { $this->_cleanup($groupId); $this->_generate($groupId); if ($this->_request->getParam('group') !== null && $this->_request->getParam('group') !== '\'' . ($groupId) . '\'' && $this->_request->getParam('group') !== $groupId ) { continue; } if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && ( $this->_scopeConfig->getValue( 'system/cron/' . $groupId . '/use_separate_process', \Magento\Store\Model\ScopeInterface::SCOPE_STORE ) == 1 ) ) { $this->_shell->execute( // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '=' . self::STANDALONE_PROCESS_STARTED . '=1', [ BP . '/bin/magento' ] ); continue; } ... } |
т.е. при установке этого флага, запуск будет произведен не в текущем скрипте крона, а в отдельном, который будет запущен вот такой командой
1 2 3 |
php bin/magento cron:run --group=<имя-группы> --bootstrap=standaloneProcessStarted=1 |
Если изучить этот метод чуть дальше, станет понятно для чего нужен запуск в отдельном процессе, немного ниже видно как запускаются задачи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
// vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php public function execute(\Magento\Framework\Event\Observer $observer) { ... foreach ($jobGroupsRoot as $groupId => $jobsRoot) { // <<<<<<<<<<<<<<<<< ... /** @var \Magento\Cron\Model\Schedule $schedule */ foreach ($pendingJobs as $schedule) { // <<<<<<<<<<<<<<<<< $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null; if (!$jobConfig) { continue; } $scheduledTime = strtotime($schedule->getScheduledAt()); if ($scheduledTime > $currentTime) { continue; } try { if ($schedule->tryLockJob()) { $this->_runJob($scheduledTime, // <<<<<<<<<<<<<<<<< $currentTime, $jobConfig, $schedule, $groupId ); } } catch (\Exception $e) { $schedule->setMessages($e->getMessage()); if ($schedule->getStatus() === Schedule::STATUS_ERROR) { $this->logger->critical($e); } if ($schedule->getStatus() === Schedule::STATUS_MISSED && $this->state->getMode() === State::MODE_DEVELOPER ) { $this->logger->info( sprintf( "%s Schedule Id: %s Job Code: %s", $schedule->getMessages(), $schedule->getScheduleId(), $schedule->getJobCode() ) ); } } $schedule->save(); } } } |
Тут мы видим, что группы обрабатываются в цикле, затем во вложенном цикле обрабатываются задачи. Делается все поочередно, и для каждой из них вызывается метод
1 2 3 |
$this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); |
если посмотреть реализацию этого метода, которая находится в этом же классе, мы увидим что там в конечном итоге, запускается выполнение коллбека указанной задачи
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId) { ... try { call_user_func_array($callback, [$schedule]); // <<<<<<<<<<<<< } catch (\Exception $e) { $schedule->setStatus(Schedule::STATUS_ERROR); throw $e; } ... } |
Это означает, что в случае, если у нас будет какая-то длительная операция, например индексирование, на время её выполнения все другие задачи будут остановлены, и продолжены, только после того как она выполнится. Это может привести к тому, что они истекут по таймауту на выполнение. Поэтому для длительных задач, таких как индексирование или интеграции, необходимо использовать запуск в отдельном процессе.
После того, как задача будет выполнена, её статус в таблице cron_schedule обновится с running на success. Очень часто бывает, что в случае криво написанных интеграций или других задач, выполнение прерывается с фатальной ошибкой, например, при превышении памяти отведенной на запуск php процесса. В таком случае, задача останется висеть в статусе running, в этом случае все последующие добавленные задачи не будут выполнены и будут переводиться в статус missed. В этом случае, необходимо вручную изменить статус задачи на error, выполнив такой SQL код:
1 2 3 4 5 6 7 8 9 10 |
UPDATE cron_schedule SET `status` = 'error', `messages` = 'Status changed manually due to cron hung' WHERE `status` = 'running' AND `job_code` = '<id-нужного-индексатора>' LIMIT 1 |
Полезные ссылки
- Cron Job in Magento: https://belvg.com/blog/how-it-works-cron-job-in-magento-2.html
- Configure and run cron: https://devdocs.magento.com/guides/v2.3/config-guide/cli/config-cli-subcommands-cron.html
- Magento 2 Cron Job [Starter Guide]: https://amasty.com/blog/magento-2-cron-job-starter-guide/
- Magento Advanced/System/Cron settings: https://magento.stackexchange.com/questions/62646/magento-advanced-system-cron-settings
- Running cron jobs in Magento 2: https://inchoo.net/magento-2/running-cron-jobs-in-magento-2/
Author: | Tags: /
| Rating:
1 comment.
Write a comment