17 use Magento\Framework\Profiler\Driver\Standard\StatFactory;
136 private $lockManager;
141 private $invalid = [];
146 private $statProfiler;
166 \
Magento\Cron\Model\ScheduleFactory $scheduleFactory,
169 \
Magento\Framework\
App\Config\ScopeConfigInterface $scopeConfig,
173 \
Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory,
174 \Psr\Log\LoggerInterface $logger,
176 StatFactory $statFactory,
177 \
Magento\Framework\Lock\LockManagerInterface $lockManager
180 $this->_scheduleFactory = $scheduleFactory;
183 $this->_scopeConfig = $scopeConfig;
187 $this->phpExecutableFinder = $phpExecutableFinderFactory->create();
189 $this->state = $state;
190 $this->statProfiler = $statFactory->create();
191 $this->lockManager = $lockManager;
208 $currentTime = $this->dateTime->gmtTimestamp();
209 $jobGroupsRoot = $this->_config->getJobs();
214 return $this->getCronGroupConfigurationValue($b,
'use_separate_process')
215 - $this->getCronGroupConfigurationValue($a,
'use_separate_process');
219 $phpPath = $this->phpExecutableFinder->find() ?:
'php';
221 foreach ($jobGroupsRoot as
$groupId => $jobsRoot) {
222 if (!$this->isGroupInFilter(
$groupId)) {
225 if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !==
'1' 226 && $this->getCronGroupConfigurationValue(
$groupId,
'use_separate_process') == 1
228 $this->_shell->execute(
230 . self::STANDALONE_PROCESS_STARTED .
'=1',
240 function (
$groupId) use ($currentTime, $jobsRoot) {
241 $this->cleanupJobs(
$groupId, $currentTime);
243 $this->processPendingJobs(
$groupId, $jobsRoot, $currentTime);
259 private function lockGroup(
$groupId, callable $callback)
262 if (!$this->lockManager->lock(self::LOCK_PREFIX .
$groupId, self::LOCK_TIMEOUT)) {
263 $this->logger->warning(
265 "Could not acquire lock for cron group: %s, skipping run",
274 $this->lockManager->unlock(self::LOCK_PREFIX .
$groupId);
289 protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule,
$groupId)
291 $jobCode = $schedule->getJobCode();
292 $scheduleLifetime = $this->getCronGroupConfigurationValue(
$groupId, self::XML_PATH_SCHEDULE_LIFETIME);
294 if ($scheduledTime < $currentTime - $scheduleLifetime) {
296 throw new \Exception(sprintf(
'Cron Job %s is missed at %s', $jobCode, $schedule->getScheduledAt()));
299 if (!isset($jobConfig[
'instance'], $jobConfig[
'method'])) {
301 throw new \Exception(
'No callbacks found');
303 $model = $this->_objectManager->create($jobConfig[
'instance']);
304 $callback = [
$model, $jobConfig[
'method']];
305 if (!is_callable($callback)) {
307 throw new \Exception(
308 sprintf(
'Invalid callback: %s::%s can\'t be called', $jobConfig[
'instance'], $jobConfig[
'method'])
312 $schedule->setExecutedAt(strftime(
'%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save();
314 $this->startProfiling();
316 $this->logger->info(sprintf(
'Cron Job %s is run', $jobCode));
317 call_user_func_array($callback, [$schedule]);
318 }
catch (\Throwable $e) {
320 $this->logger->error(sprintf(
321 'Cron Job %s has an error: %s. Statistics: %s',
324 $this->getProfilingStat()
326 if (!$e instanceof \Exception) {
327 $e = new \RuntimeException(
328 'Error when running a cron job',
335 $this->stopProfiling();
340 $this->dateTime->gmtTimestamp()
343 $this->logger->info(sprintf(
344 'Cron Job %s is successfully finished. Statistics: %s',
346 $this->getProfilingStat()
355 private function startProfiling()
357 $this->statProfiler->clear();
366 private function stopProfiling()
376 private function getProfilingStat()
378 $stat = $this->statProfiler->get(
'job');
380 return json_encode($stat);
389 private function getPendingSchedules(
$groupId)
391 $jobs = $this->_config->getJobs();
392 $pendingJobs = $this->_scheduleFactory->create()->getCollection();
394 $pendingJobs->addFieldToFilter(
'job_code', [
'in' => array_keys($jobs[
$groupId])]);
404 private function generateSchedules(
$groupId)
409 $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT .
$groupId);
410 $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue(
412 self::XML_PATH_SCHEDULE_GENERATE_EVERY
415 if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) {
423 $this->dateTime->gmtTimestamp(),
424 self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT .
$groupId,
429 $schedules = $this->getPendingSchedules(
$groupId);
432 foreach ($schedules as $schedule) {
433 $exists[$schedule->getJobCode() .
'/' . $schedule->getScheduledAt()] = 1;
439 $jobs = $this->_config->getJobs();
442 $this->cleanupScheduleMismatches();
457 foreach ($jobs as $jobCode => $jobConfig) {
458 $cronExpression = $this->getCronExpression($jobConfig);
459 if (!$cronExpression) {
464 $this->
saveSchedule($jobCode, $cronExpression, $timeInterval, $exists);
475 private function cleanupJobs(
$groupId, $currentTime)
478 $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT .
$groupId);
479 $historyCleanUp = (int)$this->getCronGroupConfigurationValue(
$groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY);
485 $this->dateTime->gmtTimestamp(),
486 self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT .
$groupId,
491 $this->cleanupDisabledJobs(
$groupId);
493 $historySuccess = (int)$this->getCronGroupConfigurationValue(
$groupId, self::XML_PATH_HISTORY_SUCCESS);
494 $historyFailure = (int)$this->getCronGroupConfigurationValue(
$groupId, self::XML_PATH_HISTORY_FAILURE);
495 $historyLifetimes = [
502 $jobs = $this->_config->getJobs()[
$groupId];
503 $scheduleResource = $this->_scheduleFactory->create()->getResource();
506 foreach ($historyLifetimes as
$status => $time) {
508 $scheduleResource->getMainTable(),
511 'job_code in (?)' => array_keys($jobs),
512 'created_at < ?' =>
$connection->formatDate($currentTime - $time)
518 $this->logger->info(sprintf(
'%d cron jobs were cleaned',
$count));
530 $cronExpr = $this->_scopeConfig->getValue(
531 $jobConfig[
'config_path'],
532 \
Magento\Store\Model\ScopeInterface::SCOPE_STORE
547 protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists)
549 $currentTime = $this->dateTime->gmtTimestamp();
550 $timeAhead = $currentTime + $timeInterval;
552 $scheduledAt = strftime(
'%Y-%m-%d %H:%M:00', $time);
553 $alreadyScheduled = !empty($exists[$jobCode .
'/' . $scheduledAt]);
554 $schedule = $this->
createSchedule($jobCode, $cronExpression, $time);
555 $valid = $schedule->trySchedule();
557 if ($alreadyScheduled) {
558 if (!isset($this->invalid[$jobCode])) {
559 $this->invalid[$jobCode] = [];
561 $this->invalid[$jobCode][] = $scheduledAt;
565 if (!$alreadyScheduled) {
582 $schedule = $this->_scheduleFactory->create()
583 ->setCronExpr($cronExpression)
584 ->setJobCode($jobCode)
586 ->setCreatedAt(strftime(
'%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))
587 ->setScheduledAt(strftime(
'%Y-%m-%d %H:%M', $time));
600 $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue(
$groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR);
603 return $scheduleAheadFor;
614 private function cleanupDisabledJobs(
$groupId)
616 $jobs = $this->_config->getJobs();
618 foreach ($jobs[
$groupId] as $jobCode => $jobConfig) {
619 if (!$this->getCronExpression($jobConfig)) {
621 $jobsToCleanup[] = $jobCode;
625 if (count($jobsToCleanup) > 0) {
626 $scheduleResource = $this->_scheduleFactory->create()->getResource();
627 $count = $scheduleResource->getConnection()->delete(
628 $scheduleResource->getMainTable(),
631 'job_code in (?)' => $jobsToCleanup,
635 $this->logger->info(sprintf(
'%d cron jobs were cleaned',
$count));
645 private function getCronExpression($jobConfig)
647 $cronExpression =
null;
648 if (isset($jobConfig[
'config_path'])) {
652 if (!$cronExpression) {
653 if (isset($jobConfig[
'schedule'])) {
654 $cronExpression = $jobConfig[
'schedule'];
657 return $cronExpression;
667 private function cleanupScheduleMismatches()
670 $scheduleResource = $this->_scheduleFactory->create()->getResource();
671 foreach ($this->invalid as $jobCode => $scheduledAtList) {
672 $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [
674 'job_code = ?' => $jobCode,
675 'scheduled_at in (?)' => $scheduledAtList,
688 private function getCronGroupConfigurationValue(
$groupId,
$path)
690 return $this->_scopeConfig->getValue(
692 \
Magento\Store\Model\ScopeInterface::SCOPE_STORE
702 private function isGroupInFilter(
$groupId): bool
704 return !($this->_request->getParam(
'group') !==
null 705 && trim($this->_request->getParam(
'group'),
"'") !==
$groupId);
715 private function processPendingJobs(
$groupId, $jobsRoot, $currentTime)
718 $pendingJobs = $this->getPendingSchedules(
$groupId);
720 foreach ($pendingJobs as $schedule) {
721 if (isset($procesedJobs[$schedule->getJobCode()])) {
725 $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] :
null;
730 $scheduledTime = strtotime($schedule->getScheduledAt());
731 if ($scheduledTime > $currentTime) {
736 if ($schedule->tryLockJob()) {
737 $this->
_runJob($scheduledTime, $currentTime, $jobConfig, $schedule,
$groupId);
739 }
catch (\Exception $e) {
740 $this->processError($schedule, $e);
743 $procesedJobs[$schedule->getJobCode()] =
true;
756 private function processError(\
Magento\Cron\Model\Schedule $schedule, \Exception $exception)
758 $schedule->setMessages($exception->getMessage());
760 $this->logger->critical($exception);
765 $this->logger->info($schedule->getMessages());
const XML_PATH_HISTORY_CLEANUP_EVERY
const XML_PATH_SCHEDULE_LIFETIME
execute(\Magento\Framework\Event\Observer $observer)
const XML_PATH_HISTORY_FAILURE
const STANDALONE_PROCESS_STARTED
const XML_PATH_SCHEDULE_AHEAD_FOR
if(!file_exists($installConfigFile)) if(!defined('TESTS_INSTALLATION_DB_CONFIG_FILE')) $shell
const XML_PATH_HISTORY_SUCCESS
const XML_PATH_SCHEDULE_GENERATE_EVERY
saveSchedule($jobCode, $cronExpression, $timeInterval, $exists)
const INPUT_KEY_BOOTSTRAP
__construct(\Magento\Framework\ObjectManagerInterface $objectManager, \Magento\Cron\Model\ScheduleFactory $scheduleFactory, \Magento\Framework\App\CacheInterface $cache, \Magento\Cron\Model\ConfigInterface $config, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Framework\App\Console\Request $request, \Magento\Framework\ShellInterface $shell, \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory, \Psr\Log\LoggerInterface $logger, \Magento\Framework\App\State $state, StatFactory $statFactory, \Magento\Framework\Lock\LockManagerInterface $lockManager)
getConfigSchedule($jobConfig)
_generateJobs($jobs, $exists, $groupId)
getScheduleTimeInterval($groupId)
createSchedule($jobCode, $cronExpression, $time)
const CACHE_KEY_LAST_HISTORY_CLEANUP_AT
const CACHE_KEY_LAST_SCHEDULE_GENERATE_AT
_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId)