ProcessCronQueueObserver.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. /**
  7. * Handling cron jobs
  8. */
  9. namespace Magento\Cron\Observer;
  10. use Magento\Framework\App\State;
  11. use Magento\Framework\Console\Cli;
  12. use Magento\Framework\Event\ObserverInterface;
  13. use Magento\Cron\Model\Schedule;
  14. use Magento\Framework\Profiler\Driver\Standard\Stat;
  15. use Magento\Framework\Profiler\Driver\Standard\StatFactory;
  16. /**
  17. * The observer for processing cron jobs.
  18. *
  19. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  20. */
  21. class ProcessCronQueueObserver implements ObserverInterface
  22. {
  23. /**#@+
  24. * Cache key values
  25. */
  26. const CACHE_KEY_LAST_SCHEDULE_GENERATE_AT = 'cron_last_schedule_generate_at';
  27. const CACHE_KEY_LAST_HISTORY_CLEANUP_AT = 'cron_last_history_cleanup_at';
  28. /**
  29. * Flag for internal communication between processes for running
  30. * all jobs in a group in parallel as a separate process
  31. */
  32. const STANDALONE_PROCESS_STARTED = 'standaloneProcessStarted';
  33. /**#@-*/
  34. /**#@+
  35. * List of configurable constants used to calculate and validate during handling cron jobs
  36. */
  37. const XML_PATH_SCHEDULE_GENERATE_EVERY = 'schedule_generate_every';
  38. const XML_PATH_SCHEDULE_AHEAD_FOR = 'schedule_ahead_for';
  39. const XML_PATH_SCHEDULE_LIFETIME = 'schedule_lifetime';
  40. const XML_PATH_HISTORY_CLEANUP_EVERY = 'history_cleanup_every';
  41. const XML_PATH_HISTORY_SUCCESS = 'history_success_lifetime';
  42. const XML_PATH_HISTORY_FAILURE = 'history_failure_lifetime';
  43. /**#@-*/
  44. /**
  45. * Value of seconds in one minute
  46. */
  47. const SECONDS_IN_MINUTE = 60;
  48. /**
  49. * How long to wait for cron group to become unlocked
  50. */
  51. const LOCK_TIMEOUT = 5;
  52. /**
  53. * Static lock prefix for cron group locking
  54. */
  55. const LOCK_PREFIX = 'CRON_GROUP_';
  56. /**
  57. * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection
  58. */
  59. protected $_pendingSchedules;
  60. /**
  61. * @var \Magento\Cron\Model\ConfigInterface
  62. */
  63. protected $_config;
  64. /**
  65. * @var \Magento\Framework\App\ObjectManager
  66. */
  67. protected $_objectManager;
  68. /**
  69. * @var \Magento\Framework\App\CacheInterface
  70. */
  71. protected $_cache;
  72. /**
  73. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  74. */
  75. protected $_scopeConfig;
  76. /**
  77. * @var ScheduleFactory
  78. */
  79. protected $_scheduleFactory;
  80. /**
  81. * @var \Magento\Framework\App\Console\Request
  82. */
  83. protected $_request;
  84. /**
  85. * @var \Magento\Framework\ShellInterface
  86. */
  87. protected $_shell;
  88. /**
  89. * @var \Magento\Framework\Stdlib\DateTime\DateTime
  90. */
  91. protected $dateTime;
  92. /**
  93. * @var \Symfony\Component\Process\PhpExecutableFinder
  94. */
  95. protected $phpExecutableFinder;
  96. /**
  97. * @var \Psr\Log\LoggerInterface
  98. */
  99. private $logger;
  100. /**
  101. * @var \Magento\Framework\App\State
  102. */
  103. private $state;
  104. /**
  105. * @var \Magento\Framework\Lock\LockManagerInterface
  106. */
  107. private $lockManager;
  108. /**
  109. * @var array
  110. */
  111. private $invalid = [];
  112. /**
  113. * @var Stat
  114. */
  115. private $statProfiler;
  116. /**
  117. * @param \Magento\Framework\ObjectManagerInterface $objectManager
  118. * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory
  119. * @param \Magento\Framework\App\CacheInterface $cache
  120. * @param \Magento\Cron\Model\ConfigInterface $config
  121. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  122. * @param \Magento\Framework\App\Console\Request $request
  123. * @param \Magento\Framework\ShellInterface $shell
  124. * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
  125. * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory
  126. * @param \Psr\Log\LoggerInterface $logger
  127. * @param State $state
  128. * @param StatFactory $statFactory
  129. * @param \Magento\Framework\Lock\LockManagerInterface $lockManager
  130. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  131. */
  132. public function __construct(
  133. \Magento\Framework\ObjectManagerInterface $objectManager,
  134. \Magento\Cron\Model\ScheduleFactory $scheduleFactory,
  135. \Magento\Framework\App\CacheInterface $cache,
  136. \Magento\Cron\Model\ConfigInterface $config,
  137. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  138. \Magento\Framework\App\Console\Request $request,
  139. \Magento\Framework\ShellInterface $shell,
  140. \Magento\Framework\Stdlib\DateTime\DateTime $dateTime,
  141. \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory,
  142. \Psr\Log\LoggerInterface $logger,
  143. \Magento\Framework\App\State $state,
  144. StatFactory $statFactory,
  145. \Magento\Framework\Lock\LockManagerInterface $lockManager
  146. ) {
  147. $this->_objectManager = $objectManager;
  148. $this->_scheduleFactory = $scheduleFactory;
  149. $this->_cache = $cache;
  150. $this->_config = $config;
  151. $this->_scopeConfig = $scopeConfig;
  152. $this->_request = $request;
  153. $this->_shell = $shell;
  154. $this->dateTime = $dateTime;
  155. $this->phpExecutableFinder = $phpExecutableFinderFactory->create();
  156. $this->logger = $logger;
  157. $this->state = $state;
  158. $this->statProfiler = $statFactory->create();
  159. $this->lockManager = $lockManager;
  160. }
  161. /**
  162. * Process cron queue
  163. * Generate tasks schedule
  164. * Cleanup tasks schedule
  165. *
  166. * @param \Magento\Framework\Event\Observer $observer
  167. * @return void
  168. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  169. * @SuppressWarnings(PHPMD.NPathComplexity)
  170. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  171. */
  172. public function execute(\Magento\Framework\Event\Observer $observer)
  173. {
  174. $currentTime = $this->dateTime->gmtTimestamp();
  175. $jobGroupsRoot = $this->_config->getJobs();
  176. // sort jobs groups to start from used in separated process
  177. uksort(
  178. $jobGroupsRoot,
  179. function ($a, $b) {
  180. return $this->getCronGroupConfigurationValue($b, 'use_separate_process')
  181. - $this->getCronGroupConfigurationValue($a, 'use_separate_process');
  182. }
  183. );
  184. $phpPath = $this->phpExecutableFinder->find() ?: 'php';
  185. foreach ($jobGroupsRoot as $groupId => $jobsRoot) {
  186. if (!$this->isGroupInFilter($groupId)) {
  187. continue;
  188. }
  189. if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1'
  190. && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1
  191. ) {
  192. $this->_shell->execute(
  193. $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '='
  194. . self::STANDALONE_PROCESS_STARTED . '=1',
  195. [
  196. BP . '/bin/magento'
  197. ]
  198. );
  199. continue;
  200. }
  201. $this->lockGroup(
  202. $groupId,
  203. function ($groupId) use ($currentTime, $jobsRoot) {
  204. $this->cleanupJobs($groupId, $currentTime);
  205. $this->generateSchedules($groupId);
  206. $this->processPendingJobs($groupId, $jobsRoot, $currentTime);
  207. }
  208. );
  209. }
  210. }
  211. /**
  212. * Lock group
  213. *
  214. * It should be taken by standalone (child) process, not by the parent process.
  215. *
  216. * @param int $groupId
  217. * @param callable $callback
  218. *
  219. * @return void
  220. */
  221. private function lockGroup($groupId, callable $callback)
  222. {
  223. if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) {
  224. $this->logger->warning(
  225. sprintf(
  226. "Could not acquire lock for cron group: %s, skipping run",
  227. $groupId
  228. )
  229. );
  230. return;
  231. }
  232. try {
  233. $callback($groupId);
  234. } finally {
  235. $this->lockManager->unlock(self::LOCK_PREFIX . $groupId);
  236. }
  237. }
  238. /**
  239. * Execute job by calling specific class::method
  240. *
  241. * @param int $scheduledTime
  242. * @param int $currentTime
  243. * @param string[] $jobConfig
  244. * @param Schedule $schedule
  245. * @param string $groupId
  246. * @return void
  247. * @throws \Exception
  248. */
  249. protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId)
  250. {
  251. $jobCode = $schedule->getJobCode();
  252. $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME);
  253. $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE;
  254. if ($scheduledTime < $currentTime - $scheduleLifetime) {
  255. $schedule->setStatus(Schedule::STATUS_MISSED);
  256. throw new \Exception(sprintf('Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt()));
  257. }
  258. if (!isset($jobConfig['instance'], $jobConfig['method'])) {
  259. $schedule->setStatus(Schedule::STATUS_ERROR);
  260. throw new \Exception(sprintf('No callbacks found for cron job %s', $jobCode));
  261. }
  262. $model = $this->_objectManager->create($jobConfig['instance']);
  263. $callback = [$model, $jobConfig['method']];
  264. if (!is_callable($callback)) {
  265. $schedule->setStatus(Schedule::STATUS_ERROR);
  266. throw new \Exception(
  267. sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method'])
  268. );
  269. }
  270. $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save();
  271. $this->startProfiling();
  272. try {
  273. $this->logger->info(sprintf('Cron Job %s is run', $jobCode));
  274. call_user_func_array($callback, [$schedule]);
  275. } catch (\Throwable $e) {
  276. $schedule->setStatus(Schedule::STATUS_ERROR);
  277. $this->logger->error(sprintf(
  278. 'Cron Job %s has an error: %s. Statistics: %s',
  279. $jobCode,
  280. $e->getMessage(),
  281. $this->getProfilingStat()
  282. ));
  283. if (!$e instanceof \Exception) {
  284. $e = new \RuntimeException(
  285. 'Error when running a cron job',
  286. 0,
  287. $e
  288. );
  289. }
  290. throw $e;
  291. } finally {
  292. $this->stopProfiling();
  293. }
  294. $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime(
  295. '%Y-%m-%d %H:%M:%S',
  296. $this->dateTime->gmtTimestamp()
  297. ));
  298. $this->logger->info(sprintf(
  299. 'Cron Job %s is successfully finished. Statistics: %s',
  300. $jobCode,
  301. $this->getProfilingStat()
  302. ));
  303. }
  304. /**
  305. * Starts profiling
  306. *
  307. * @return void
  308. */
  309. private function startProfiling()
  310. {
  311. $this->statProfiler->clear();
  312. $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage());
  313. }
  314. /**
  315. * Stops profiling
  316. *
  317. * @return void
  318. */
  319. private function stopProfiling()
  320. {
  321. $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage());
  322. }
  323. /**
  324. * Retrieves statistics in the JSON format
  325. *
  326. * @return string
  327. */
  328. private function getProfilingStat()
  329. {
  330. $stat = $this->statProfiler->get('job');
  331. unset($stat[Stat::START]);
  332. return json_encode($stat);
  333. }
  334. /**
  335. * Return job collection from data base with status 'pending'.
  336. *
  337. * @param string $groupId
  338. * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection
  339. */
  340. private function getPendingSchedules($groupId)
  341. {
  342. $jobs = $this->_config->getJobs();
  343. $pendingJobs = $this->_scheduleFactory->create()->getCollection();
  344. $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING);
  345. $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]);
  346. return $pendingJobs;
  347. }
  348. /**
  349. * Generate cron schedule
  350. *
  351. * @param string $groupId
  352. * @return $this
  353. */
  354. private function generateSchedules($groupId)
  355. {
  356. /**
  357. * check if schedule generation is needed
  358. */
  359. $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId);
  360. $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue(
  361. $groupId,
  362. self::XML_PATH_SCHEDULE_GENERATE_EVERY
  363. );
  364. $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE;
  365. if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) {
  366. return $this;
  367. }
  368. /**
  369. * save time schedules generation was ran with no expiration
  370. */
  371. $this->_cache->save(
  372. $this->dateTime->gmtTimestamp(),
  373. self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId,
  374. ['crontab'],
  375. null
  376. );
  377. $schedules = $this->getPendingSchedules($groupId);
  378. $exists = [];
  379. /** @var Schedule $schedule */
  380. foreach ($schedules as $schedule) {
  381. $exists[$schedule->getJobCode() . '/' . $schedule->getScheduledAt()] = 1;
  382. }
  383. /**
  384. * generate global crontab jobs
  385. */
  386. $jobs = $this->_config->getJobs();
  387. $this->invalid = [];
  388. $this->_generateJobs($jobs[$groupId], $exists, $groupId);
  389. $this->cleanupScheduleMismatches();
  390. return $this;
  391. }
  392. /**
  393. * Generate jobs for config information
  394. *
  395. * @param array $jobs
  396. * @param array $exists
  397. * @param string $groupId
  398. * @return void
  399. */
  400. protected function _generateJobs($jobs, $exists, $groupId)
  401. {
  402. foreach ($jobs as $jobCode => $jobConfig) {
  403. $cronExpression = $this->getCronExpression($jobConfig);
  404. if (!$cronExpression) {
  405. continue;
  406. }
  407. $timeInterval = $this->getScheduleTimeInterval($groupId);
  408. $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists);
  409. }
  410. }
  411. /**
  412. * Clean expired jobs
  413. *
  414. * @param string $groupId
  415. * @param int $currentTime
  416. * @return void
  417. */
  418. private function cleanupJobs($groupId, $currentTime)
  419. {
  420. // check if history cleanup is needed
  421. $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId);
  422. $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY);
  423. if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) {
  424. return $this;
  425. }
  426. // save time history cleanup was ran with no expiration
  427. $this->_cache->save(
  428. $this->dateTime->gmtTimestamp(),
  429. self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId,
  430. ['crontab'],
  431. null
  432. );
  433. $this->cleanupDisabledJobs($groupId);
  434. $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS);
  435. $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE);
  436. $historyLifetimes = [
  437. Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE,
  438. Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE,
  439. Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE,
  440. Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE,
  441. ];
  442. $jobs = $this->_config->getJobs()[$groupId];
  443. $scheduleResource = $this->_scheduleFactory->create()->getResource();
  444. $connection = $scheduleResource->getConnection();
  445. $count = 0;
  446. foreach ($historyLifetimes as $status => $time) {
  447. $count += $connection->delete(
  448. $scheduleResource->getMainTable(),
  449. [
  450. 'status = ?' => $status,
  451. 'job_code in (?)' => array_keys($jobs),
  452. 'created_at < ?' => $connection->formatDate($currentTime - $time)
  453. ]
  454. );
  455. }
  456. if ($count) {
  457. $this->logger->info(sprintf('%d cron jobs were cleaned', $count));
  458. }
  459. }
  460. /**
  461. * Get config of schedule.
  462. *
  463. * @param array $jobConfig
  464. * @return mixed
  465. */
  466. protected function getConfigSchedule($jobConfig)
  467. {
  468. $cronExpr = $this->_scopeConfig->getValue(
  469. $jobConfig['config_path'],
  470. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  471. );
  472. return $cronExpr;
  473. }
  474. /**
  475. * Save a schedule of cron job.
  476. *
  477. * @param string $jobCode
  478. * @param string $cronExpression
  479. * @param int $timeInterval
  480. * @param array $exists
  481. * @return void
  482. */
  483. protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists)
  484. {
  485. $currentTime = $this->dateTime->gmtTimestamp();
  486. $timeAhead = $currentTime + $timeInterval;
  487. for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) {
  488. $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time);
  489. $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]);
  490. $schedule = $this->createSchedule($jobCode, $cronExpression, $time);
  491. $valid = $schedule->trySchedule();
  492. if (!$valid) {
  493. if ($alreadyScheduled) {
  494. if (!isset($this->invalid[$jobCode])) {
  495. $this->invalid[$jobCode] = [];
  496. }
  497. $this->invalid[$jobCode][] = $scheduledAt;
  498. }
  499. continue;
  500. }
  501. if (!$alreadyScheduled) {
  502. // time matches cron expression
  503. $schedule->save();
  504. }
  505. }
  506. }
  507. /**
  508. * Create a schedule of cron job.
  509. *
  510. * @param string $jobCode
  511. * @param string $cronExpression
  512. * @param int $time
  513. * @return Schedule
  514. */
  515. protected function createSchedule($jobCode, $cronExpression, $time)
  516. {
  517. $schedule = $this->_scheduleFactory->create()
  518. ->setCronExpr($cronExpression)
  519. ->setJobCode($jobCode)
  520. ->setStatus(Schedule::STATUS_PENDING)
  521. ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))
  522. ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time));
  523. return $schedule;
  524. }
  525. /**
  526. * Get time interval for scheduling.
  527. *
  528. * @param string $groupId
  529. * @return int
  530. */
  531. protected function getScheduleTimeInterval($groupId)
  532. {
  533. $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR);
  534. $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE;
  535. return $scheduleAheadFor;
  536. }
  537. /**
  538. * Clean up scheduled jobs that are disabled in the configuration.
  539. *
  540. * This can happen when you turn off a cron job in the config and flush the cache.
  541. *
  542. * @param string $groupId
  543. * @return void
  544. */
  545. private function cleanupDisabledJobs($groupId)
  546. {
  547. $jobs = $this->_config->getJobs();
  548. $jobsToCleanup = [];
  549. foreach ($jobs[$groupId] as $jobCode => $jobConfig) {
  550. if (!$this->getCronExpression($jobConfig)) {
  551. /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */
  552. $jobsToCleanup[] = $jobCode;
  553. }
  554. }
  555. if (count($jobsToCleanup) > 0) {
  556. $scheduleResource = $this->_scheduleFactory->create()->getResource();
  557. $count = $scheduleResource->getConnection()->delete(
  558. $scheduleResource->getMainTable(),
  559. [
  560. 'status = ?' => Schedule::STATUS_PENDING,
  561. 'job_code in (?)' => $jobsToCleanup,
  562. ]
  563. );
  564. $this->logger->info(sprintf('%d cron jobs were cleaned', $count));
  565. }
  566. }
  567. /**
  568. * Get cron expression of cron job.
  569. *
  570. * @param array $jobConfig
  571. * @return null|string
  572. */
  573. private function getCronExpression($jobConfig)
  574. {
  575. $cronExpression = null;
  576. if (isset($jobConfig['config_path'])) {
  577. $cronExpression = $this->getConfigSchedule($jobConfig) ?: null;
  578. }
  579. if (!$cronExpression) {
  580. if (isset($jobConfig['schedule'])) {
  581. $cronExpression = $jobConfig['schedule'];
  582. }
  583. }
  584. return $cronExpression;
  585. }
  586. /**
  587. * Clean up scheduled jobs that do not match their cron expression anymore.
  588. *
  589. * This can happen when you change the cron expression and flush the cache.
  590. *
  591. * @return $this
  592. */
  593. private function cleanupScheduleMismatches()
  594. {
  595. /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */
  596. $scheduleResource = $this->_scheduleFactory->create()->getResource();
  597. foreach ($this->invalid as $jobCode => $scheduledAtList) {
  598. $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [
  599. 'status = ?' => Schedule::STATUS_PENDING,
  600. 'job_code = ?' => $jobCode,
  601. 'scheduled_at in (?)' => $scheduledAtList,
  602. ]);
  603. }
  604. return $this;
  605. }
  606. /**
  607. * Get CronGroup Configuration Value.
  608. *
  609. * @param string $groupId
  610. * @param string $path
  611. * @return int
  612. */
  613. private function getCronGroupConfigurationValue($groupId, $path)
  614. {
  615. return $this->_scopeConfig->getValue(
  616. 'system/cron/' . $groupId . '/' . $path,
  617. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  618. );
  619. }
  620. /**
  621. * Is Group In Filter.
  622. *
  623. * @param string $groupId
  624. * @return bool
  625. */
  626. private function isGroupInFilter($groupId): bool
  627. {
  628. return !($this->_request->getParam('group') !== null
  629. && trim($this->_request->getParam('group'), "'") !== $groupId);
  630. }
  631. /**
  632. * Process pending jobs.
  633. *
  634. * @param string $groupId
  635. * @param array $jobsRoot
  636. * @param int $currentTime
  637. */
  638. private function processPendingJobs($groupId, $jobsRoot, $currentTime)
  639. {
  640. $procesedJobs = [];
  641. $pendingJobs = $this->getPendingSchedules($groupId);
  642. /** @var \Magento\Cron\Model\Schedule $schedule */
  643. foreach ($pendingJobs as $schedule) {
  644. if (isset($procesedJobs[$schedule->getJobCode()])) {
  645. // process only on job per run
  646. continue;
  647. }
  648. $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null;
  649. if (!$jobConfig) {
  650. continue;
  651. }
  652. $scheduledTime = strtotime($schedule->getScheduledAt());
  653. if ($scheduledTime > $currentTime) {
  654. continue;
  655. }
  656. try {
  657. if ($schedule->tryLockJob()) {
  658. $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId);
  659. }
  660. } catch (\Exception $e) {
  661. $this->processError($schedule, $e);
  662. }
  663. if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) {
  664. $procesedJobs[$schedule->getJobCode()] = true;
  665. }
  666. $schedule->save();
  667. }
  668. }
  669. /**
  670. * Process error messages.
  671. *
  672. * @param Schedule $schedule
  673. * @param \Exception $exception
  674. * @return void
  675. */
  676. private function processError(\Magento\Cron\Model\Schedule $schedule, \Exception $exception)
  677. {
  678. $schedule->setMessages($exception->getMessage());
  679. if ($schedule->getStatus() === Schedule::STATUS_ERROR) {
  680. $this->logger->critical($exception);
  681. }
  682. if ($schedule->getStatus() === Schedule::STATUS_MISSED
  683. && $this->state->getMode() === State::MODE_DEVELOPER
  684. ) {
  685. $this->logger->info($schedule->getMessages());
  686. }
  687. }
  688. }