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)  | 
					

Magento 2: Настройки крон
Разберемся с настройками для групп
- 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: | Rating: / | Tags:

1 comment.
Write a comment