Skip to content

Commit dda5c72

Browse files
committed
Cron cleanup repeatedly hits deadlocks on large environments
Fix failing tests
1 parent a901619 commit dda5c72

File tree

3 files changed

+87
-55
lines changed

3 files changed

+87
-55
lines changed

app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
namespace Magento\Cron\Observer;
1111

12+
use Magento\Cron\Model\ResourceModel\Schedule\Collection as ScheduleCollection;
1213
use Magento\Cron\Model\Schedule;
1314
use Magento\Framework\App\State;
1415
use Magento\Framework\Console\Cli;
@@ -83,7 +84,7 @@ class ProcessCronQueueObserver implements ObserverInterface
8384
const MAX_RETRIES = 5;
8485

8586
/**
86-
* @var \Magento\Cron\Model\ResourceModel\Schedule\Collection
87+
* @var ScheduleCollection
8788
*/
8889
protected $_pendingSchedules;
8990

@@ -278,12 +279,12 @@ function ($groupId) use ($currentTime) {
278279
*
279280
* It should be taken by standalone (child) process, not by the parent process.
280281
*
281-
* @param int $groupId
282+
* @param string $groupId
282283
* @param callable $callback
283284
*
284285
* @return void
285286
*/
286-
private function lockGroup($groupId, callable $callback)
287+
private function lockGroup(string $groupId, callable $callback): void
287288
{
288289
if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) {
289290
$this->logger->warning(
@@ -399,7 +400,7 @@ function () use ($schedule) {
399400
* @param string $jobName
400401
* @return void
401402
*/
402-
private function startProfiling(string $jobName = '')
403+
private function startProfiling(string $jobName = ''): void
403404
{
404405
$this->statProfiler->clear();
405406
$this->statProfiler->start(
@@ -416,7 +417,7 @@ private function startProfiling(string $jobName = '')
416417
* @param string $jobName
417418
* @return void
418419
*/
419-
private function stopProfiling(string $jobName = '')
420+
private function stopProfiling(string $jobName = ''): void
420421
{
421422
$this->statProfiler->stop(
422423
sprintf(self::CRON_TIMERID, $jobName),
@@ -445,9 +446,9 @@ private function getProfilingStat(string $jobName): string
445446
* Return job collection from data base with status 'pending'.
446447
*
447448
* @param string $groupId
448-
* @return \Magento\Cron\Model\ResourceModel\Schedule\Collection
449+
* @return ScheduleCollection
449450
*/
450-
private function getPendingSchedules($groupId)
451+
private function getPendingSchedules(string $groupId): ScheduleCollection
451452
{
452453
$jobs = $this->_config->getJobs();
453454
$pendingJobs = $this->_scheduleFactory->create()->getCollection();
@@ -462,7 +463,7 @@ private function getPendingSchedules($groupId)
462463
* @param string $groupId
463464
* @return $this
464465
*/
465-
private function generateSchedules($groupId)
466+
private function generateSchedules(string $groupId): self
466467
{
467468
/**
468469
* check if schedule generation is needed
@@ -533,13 +534,13 @@ protected function _generateJobs($jobs, $exists, $groupId)
533534
* @param int $currentTime
534535
* @return void
535536
*/
536-
private function cleanupJobs($groupId, $currentTime)
537+
private function cleanupJobs(string $groupId, int $currentTime): void
537538
{
538539
// check if history cleanup is needed
539540
$lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId);
540541
$historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY);
541542
if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) {
542-
return $this;
543+
return;
543544
}
544545
// save time history cleanup was ran with no expiration
545546
$this->_cache->save(
@@ -674,7 +675,7 @@ protected function getScheduleTimeInterval($groupId)
674675
* @param string $groupId
675676
* @return void
676677
*/
677-
private function cleanupDisabledJobs($groupId)
678+
private function cleanupDisabledJobs(string $groupId): void
678679
{
679680
$jobs = $this->_config->getJobs();
680681
$jobsToCleanup = [];
@@ -703,7 +704,7 @@ private function cleanupDisabledJobs($groupId)
703704
* @param string $groupId
704705
* @return void
705706
*/
706-
private function cleanupRunningJobs($groupId)
707+
private function cleanupRunningJobs(string $groupId): void
707708
{
708709
$scheduleResource = $this->_scheduleFactory->create()->getResource();
709710
$connection = $scheduleResource->getConnection();
@@ -716,13 +717,11 @@ private function cleanupRunningJobs($groupId)
716717
'status' => \Magento\Cron\Model\Schedule::STATUS_ERROR,
717718
'messages' => 'Time out'
718719
],
719-
$connection->quoteInto(
720-
'status = ? ' .
721-
'AND job_code IN (?) ' .
722-
'AND (scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY)',
723-
\Magento\Cron\Model\Schedule::STATUS_RUNNING,
724-
array_keys($jobs[$groupId])
725-
)
720+
[
721+
$connection->quoteInto('status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING),
722+
$connection->quoteInto('job_code IN (?)', array_keys($jobs[$groupId])),
723+
'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY'
724+
]
726725
);
727726
}
728727

@@ -803,13 +802,13 @@ private function isGroupInFilter($groupId): bool
803802
* @param array $jobsRoot
804803
* @param int $currentTime
805804
*/
806-
private function processPendingJobs($groupId, $jobsRoot, $currentTime)
805+
private function processPendingJobs(string $groupId, array $jobsRoot, int $currentTime): void
807806
{
808-
$procesedJobs = [];
807+
$processedJobs = [];
809808
$pendingJobs = $this->getPendingSchedules($groupId);
810809
/** @var Schedule $schedule */
811810
foreach ($pendingJobs as $schedule) {
812-
if (isset($procesedJobs[$schedule->getJobCode()])) {
811+
if (isset($processedJobs[$schedule->getJobCode()])) {
813812
// process only on job per run
814813
continue;
815814
}
@@ -826,7 +825,7 @@ private function processPendingJobs($groupId, $jobsRoot, $currentTime)
826825
$this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId);
827826

828827
if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) {
829-
$procesedJobs[$schedule->getJobCode()] = true;
828+
$processedJobs[$schedule->getJobCode()] = true;
830829
}
831830

832831
$this->retrier->execute(
@@ -851,7 +850,7 @@ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule,
851850
{
852851
// use sha1 to limit length
853852
// phpcs:ignore Magento2.Security.InsecureFunction
854-
$lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode());
853+
$lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode());
855854

856855
try {
857856
for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) {

app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,53 +1045,85 @@ public function testMissedJobsCleanedInTime()
10451045
$scheduleMock->expects($this->exactly(10))->method('getResource')->willReturn($this->scheduleResourceMock);
10461046
$this->scheduleFactoryMock->expects($this->exactly(11))->method('create')->willReturn($scheduleMock);
10471047

1048+
$connectionMock = $this->prepareConnectionMock($tableName);
1049+
1050+
$this->scheduleResourceMock->expects($this->exactly(6))
1051+
->method('getTable')
1052+
->with($tableName)
1053+
->willReturn($tableName);
1054+
$this->scheduleResourceMock->expects($this->exactly(15))
1055+
->method('getConnection')
1056+
->willReturn($connectionMock);
1057+
1058+
$this->retrierMock->expects($this->exactly(5))
1059+
->method('execute')
1060+
->willReturnCallback(
1061+
function ($callback) {
1062+
return $callback();
1063+
}
1064+
);
1065+
1066+
$this->cronQueueObserver->execute($this->observerMock);
1067+
}
1068+
1069+
/**
1070+
* @param string $tableName
1071+
* @return AdapterInterface|MockObject
1072+
*/
1073+
private function prepareConnectionMock(string $tableName)
1074+
{
10481075
$connectionMock = $this->getMockForAbstractClass(AdapterInterface::class);
10491076

10501077
$connectionMock->expects($this->exactly(5))
10511078
->method('delete')
10521079
->withConsecutive(
1053-
[$tableName, ['status = ?' => 'pending', 'job_code in (?)' => ['test_job1']]],
1054-
[$tableName, ['status = ?' => 'success', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]],
1055-
[$tableName, ['status = ?' => 'missed', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]],
1056-
[$tableName, ['status = ?' => 'error', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]],
1057-
[$tableName, ['status = ?' => 'pending', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]]
1080+
[
1081+
$tableName,
1082+
['status = ?' => 'pending', 'job_code in (?)' => ['test_job1']]
1083+
],
1084+
[
1085+
$tableName,
1086+
['status = ?' => 'success', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]
1087+
],
1088+
[
1089+
$tableName,
1090+
['status = ?' => 'missed', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]
1091+
],
1092+
[
1093+
$tableName,
1094+
['status = ?' => 'error', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]
1095+
],
1096+
[
1097+
$tableName,
1098+
['status = ?' => 'pending', 'job_code in (?)' => ['test_job1'], 'scheduled_at < ?' => null]
1099+
]
10581100
)
10591101
->willReturn(1);
10601102

1061-
$connectionMock->expects($this->once())
1103+
$connectionMock->expects($this->any())
10621104
->method('quoteInto')
1063-
->with(
1064-
'status = ? AND job_code IN (?) AND (scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY)',
1065-
'running',
1066-
['test_job1']
1105+
->withConsecutive(
1106+
['status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING],
1107+
['job_code IN (?)', ['test_job1']],
10671108
)
1068-
->willReturn('');
1109+
->willReturnOnConsecutiveCalls(
1110+
"status = 'running'",
1111+
"job_code IN ('test_job1')"
1112+
);
10691113

10701114
$connectionMock->expects($this->once())
10711115
->method('update')
10721116
->with(
10731117
$tableName,
10741118
['status' => 'error', 'messages' => 'Time out'],
1075-
''
1119+
[
1120+
"status = 'running'",
1121+
"job_code IN ('test_job1')",
1122+
'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY'
1123+
]
10761124
)
10771125
->willReturn(0);
10781126

1079-
$this->scheduleResourceMock->expects($this->exactly(6))
1080-
->method('getTable')
1081-
->with($tableName)
1082-
->willReturn($tableName);
1083-
$this->scheduleResourceMock->expects($this->exactly(15))
1084-
->method('getConnection')
1085-
->willReturn($connectionMock);
1086-
1087-
$this->retrierMock->expects($this->exactly(5))
1088-
->method('execute')
1089-
->willReturnCallback(
1090-
function ($callback) {
1091-
return $callback();
1092-
}
1093-
);
1094-
1095-
$this->cronQueueObserver->execute($this->observerMock);
1127+
return $connectionMock;
10961128
}
10971129
}

app/code/Magento/Cron/etc/db_schema_whitelist.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
},
1313
"index": {
1414
"CRON_SCHEDULE_JOB_CODE": true,
15-
"CRON_SCHEDULE_SCHEDULED_AT_STATUS": true
15+
"CRON_SCHEDULE_SCHEDULED_AT_STATUS": true,
16+
"CRON_SCHEDULE_JOB_CODE_STATUS_SCHEDULED_AT": true
1617
},
1718
"constraint": {
1819
"PRIMARY": true
1920
}
2021
}
21-
}
22+
}

0 commit comments

Comments
 (0)