Skip to content

Commit

Permalink
Merge pull request magento#1227 from magento-okapis/Okapis-develop-pr
Browse files Browse the repository at this point in the history
[okapis] MAGETWO-69629: Deadlock Error While indexing Category products
  • Loading branch information
cpartica authored and dmanners committed Dec 19, 2017
1 parent ca8ee57 commit 6a1a94e
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 10 deletions.
44 changes: 41 additions & 3 deletions app/code/Magento/Cron/Model/ResourceModel/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ public function _construct()
}

/**
* If job is currently in $currentStatus, set it to $newStatus
* and return true. Otherwise, return false and do not change the job.
* This method is used to implement locking for cron jobs.
* Sets new schedule status only if it's in the expected current status.
*
* If schedule is currently in $currentStatus, set it to $newStatus and
* return true. Otherwise, return false.
*
* @param string $scheduleId
* @param string $newStatus
Expand All @@ -45,4 +46,41 @@ public function trySetJobStatusAtomic($scheduleId, $newStatus, $currentStatus)
}
return false;
}

/**
* Sets schedule status only if no existing schedules with the same job code
* have that status. This is used to implement locking for cron jobs.
*
* If the schedule is currently in $currentStatus and there are no existing
* schedules with the same job code and $newStatus, set the schedule to
* $newStatus and return true. Otherwise, return false.
*
* @param string $scheduleId
* @param string $newStatus
* @param string $currentStatus
* @return bool
*/
public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus)
{
$connection = $this->getConnection();

$match = $connection->quoteInto('existing.job_code = current.job_code AND existing.status = ?', $newStatus);
$selectIfUnlocked = $connection->select()
->joinLeft(
['existing' => $this->getTable('cron_schedule')],
$match,
['status' => new \Zend_Db_Expr($connection->quote($newStatus))]
)
->where('current.schedule_id = ?', $scheduleId)
->where('current.status = ?', $currentStatus)
->where('existing.schedule_id IS NULL');

$update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]);
$result = $connection->query($update)->rowCount();

if ($result == 1) {
return true;
}
return false;
}
}
9 changes: 5 additions & 4 deletions app/code/Magento/Cron/Model/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,16 +221,17 @@ public function getNumeric($value)
}

/**
* Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING.
* Returns true if status was changed and false otherwise.
* Lock the cron job so no other scheduled instances run simultaneously.
*
* This is used to implement locking for cron jobs.
* Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING
* and no other jobs of the same code are currently in STATUS_RUNNING.
* Returns true if status was changed and false otherwise.
*
* @return boolean
*/
public function tryLockJob()
{
if ($this->_getResource()->trySetJobStatusAtomic(
if ($this->_getResource()->trySetJobUniqueStatusAtomic(
$this->getId(),
self::STATUS_RUNNING,
self::STATUS_PENDING
Expand Down
6 changes: 3 additions & 3 deletions app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protected function setUp()

$this->resourceJobMock = $this->getMockBuilder('Magento\Cron\Model\ResourceModel\Schedule')
->disableOriginalConstructor()
->setMethods(['trySetJobStatusAtomic', '__wakeup', 'getIdFieldName'])
->setMethods(['trySetJobUniqueStatusAtomic', '__wakeup', 'getIdFieldName'])
->getMockForAbstractClass();

$this->resourceJobMock->expects($this->any())
Expand Down Expand Up @@ -336,7 +336,7 @@ public function testTryLockJobSuccess()
$scheduleId = 1;

$this->resourceJobMock->expects($this->once())
->method('trySetJobStatusAtomic')
->method('trySetJobUniqueStatusAtomic')
->with($scheduleId, Schedule::STATUS_RUNNING, Schedule::STATUS_PENDING)
->will($this->returnValue(true));

Expand All @@ -360,7 +360,7 @@ public function testTryLockJobFailure()
$scheduleId = 1;

$this->resourceJobMock->expects($this->once())
->method('trySetJobStatusAtomic')
->method('trySetJobUniqueStatusAtomic')
->with($scheduleId, Schedule::STATUS_RUNNING, Schedule::STATUS_PENDING)
->will($this->returnValue(false));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Cron\Model;

use Magento\Framework\Stdlib\DateTime\DateTime;
use \Magento\TestFramework\Helper\Bootstrap;

/**
* Test \Magento\Cron\Model\Schedule
*
* @magentoDbIsolation enabled
*/
class ScheduleTest extends \PHPUnit_Framework_TestCase
{
/**
* @var ScheduleFactory
*/
private $scheduleFactory;

/**
* @var DateTime
*/
protected $dateTime;

public function setUp()
{
$this->dateTime = Bootstrap::getObjectManager()->create(DateTime::class);
$this->scheduleFactory = Bootstrap::getObjectManager()->create(ScheduleFactory::class);
}

/**
* If there are no currently locked jobs, locking one of them should succeed
*/
public function testTryLockJobNoLockedJobsSucceeds()
{
for ($i = 1; $i < 6; $i++) {
$this->createSchedule("test_job", Schedule::STATUS_PENDING, 60 * $i);
}
$schedule = $this->createSchedule("test_job", Schedule::STATUS_PENDING);

$this->assertTrue($schedule->tryLockJob());
}

/**
* If the job is already locked, attempting to lock it again should fail
*/
public function testTryLockJobAlreadyLockedFails()
{
$schedule = $this->createSchedule("test_job", Schedule::STATUS_RUNNING);

$this->assertFalse($schedule->tryLockJob());
}

/**
* If there's a job already locked, should not be able to lock another job
*/
public function testTryLockJobOtherLockedFails()
{
$this->createSchedule("test_job", Schedule::STATUS_RUNNING);
$schedule = $this->createSchedule("test_job", Schedule::STATUS_PENDING, 60);

$this->assertFalse($schedule->tryLockJob());
}

/**
* Should be able to lock a job if a job with a different code is locked
*/
public function testTryLockJobDifferentJobLocked()
{
$this->createSchedule("test_job_other", Schedule::STATUS_RUNNING);
$schedule = $this->createSchedule("test_job", Schedule::STATUS_PENDING);

$this->assertTrue($schedule->tryLockJob());
}

/**
* Creates a schedule with the given job code, status, and schedule time offset
*
* @param string $jobCode
* @param string $status
* @param int $timeOffset
* @return Schedule
*/
private function createSchedule($jobCode, $status, $timeOffset = 0)
{
$schedule = $this->scheduleFactory->create()
->setCronExpr("* * * * *")
->setJobCode($jobCode)
->setStatus($status)
->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))
->setScheduledAt(strftime('%Y-%m-%d %H:%M', $this->dateTime->gmtTimestamp() + $timeOffset));
$schedule->save();

return $schedule;
}
}

0 comments on commit 6a1a94e

Please sign in to comment.