Skip to content

Commit 477ab92

Browse files
oshmyheliukshiftedreality
authored andcommitted
MAGECLOUD-1367: Improve cron:unlock Command (#133)
1 parent ae6826f commit 477ab92

File tree

7 files changed

+240
-68
lines changed

7 files changed

+240
-68
lines changed

src/App/Container.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
use Magento\MagentoCloud\Command\Build;
99
use Magento\MagentoCloud\Command\DbDump;
10-
use Magento\MagentoCloud\Command\CronUnlock;
1110
use Magento\MagentoCloud\Command\Deploy;
1211
use Magento\MagentoCloud\Command\ConfigDump;
1312
use Magento\MagentoCloud\Command\Prestart;
@@ -275,15 +274,6 @@ function () {
275274
'system/websites',
276275
];
277276
});
278-
$this->container->when(CronUnlock::class)
279-
->needs(ProcessInterface::class)
280-
->give(function () {
281-
return $this->container->makeWith(ProcessComposite::class, [
282-
'processes' => [
283-
$this->container->make(DeployProcess\UnlockCronJobs::class),
284-
],
285-
]);
286-
});
287277
$this->container->when(DeployProcess\PreDeploy::class)
288278
->needs(ProcessInterface::class)
289279
->give(function () {

src/Command/CronUnlock.php

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
*/
66
namespace Magento\MagentoCloud\Command;
77

8-
use Magento\MagentoCloud\Process\ProcessInterface;
8+
use Magento\MagentoCloud\Cron\JobUnlocker;
99
use Psr\Log\LoggerInterface;
1010
use Symfony\Component\Console\Command\Command;
1111
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Input\InputOption;
1213
use Symfony\Component\Console\Output\OutputInterface;
1314

1415
/**
@@ -18,23 +19,27 @@ class CronUnlock extends Command
1819
{
1920
const NAME = 'cron:unlock';
2021

21-
/**
22-
* @var ProcessInterface
23-
*/
24-
private $process;
22+
const OPTION_JOB_CODE = 'job-code';
23+
24+
const UNLOCK_MESSAGE = 'The job is terminated by cron:unlock command';
2525

2626
/**
2727
* @var LoggerInterface
2828
*/
2929
private $logger;
3030

3131
/**
32-
* @param ProcessInterface $process
32+
* @var JobUnlocker
33+
*/
34+
private $jobUnlocker;
35+
36+
/**
37+
* @param JobUnlocker $jobUnlocker
3338
* @param LoggerInterface $logger
3439
*/
35-
public function __construct(ProcessInterface $process, LoggerInterface $logger)
40+
public function __construct(JobUnlocker $jobUnlocker, LoggerInterface $logger)
3641
{
37-
$this->process = $process;
42+
$this->jobUnlocker = $jobUnlocker;
3843
$this->logger = $logger;
3944

4045
parent::__construct();
@@ -48,6 +53,13 @@ protected function configure()
4853
$this->setName(static::NAME)
4954
->setDescription('Unlock cron jobs that stuck in "running" state.');
5055

56+
$this->addOption(
57+
self::OPTION_JOB_CODE,
58+
null,
59+
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
60+
'Cron job code to unlock.'
61+
);
62+
5163
parent::configure();
5264
}
5365

@@ -58,7 +70,19 @@ public function execute(InputInterface $input, OutputInterface $output)
5870
{
5971
try {
6072
$this->logger->info('Starting unlocking.');
61-
$this->process->execute();
73+
74+
$jobCodesToUnlock = array_filter($input->getOption(self::OPTION_JOB_CODE));
75+
76+
if (count($jobCodesToUnlock)) {
77+
foreach ($jobCodesToUnlock as $jobCode) {
78+
$this->jobUnlocker->unlockByJobCode($jobCode, self::UNLOCK_MESSAGE);
79+
$this->logger->info(sprintf('Unlocking cron jobs with code #%s.', $jobCode));
80+
}
81+
} else {
82+
$this->jobUnlocker->unlockAll(self::UNLOCK_MESSAGE);
83+
$this->logger->info('Unlocking all cron jobs.');
84+
}
85+
6286
$this->logger->info('Unlocking completed.');
6387
} catch (\Exception $exception) {
6488
$this->logger->critical($exception->getMessage());

src/Cron/JobUnlocker.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\MagentoCloud\Cron;
7+
8+
use Magento\MagentoCloud\DB\ConnectionInterface;
9+
10+
/**
11+
* Unlocks cron jobs stacked in status 'running'.
12+
*/
13+
class JobUnlocker
14+
{
15+
const STATUS_RUNNING = 'running';
16+
const STATUS_ERROR = 'error';
17+
18+
const UPGRADE_UNLOCK_MESSAGE = 'The job is terminated due to system upgrade';
19+
20+
/**
21+
* @var ConnectionInterface
22+
*/
23+
private $connection;
24+
25+
/**
26+
* @param ConnectionInterface $connection
27+
*/
28+
public function __construct(ConnectionInterface $connection)
29+
{
30+
$this->connection = $connection;
31+
}
32+
33+
/**
34+
* Moves all cron jobs from status running to status missed.
35+
*
36+
* @param string $message
37+
* @return int Count of updated rows.
38+
*/
39+
public function unlockAll(string $message = self::UPGRADE_UNLOCK_MESSAGE): int
40+
{
41+
$updateCronStatusQuery = 'UPDATE `cron_schedule` SET `status` = :to_status, `messages` = :messages'
42+
. ' WHERE `status` = :from_status';
43+
44+
return $this->connection->affectingQuery(
45+
$updateCronStatusQuery,
46+
[
47+
':to_status' => self::STATUS_ERROR,
48+
':from_status' => self::STATUS_RUNNING,
49+
':messages' => $message
50+
]
51+
);
52+
}
53+
54+
/**
55+
* Moves cron jobs with given job_code from status running to status missed.
56+
*
57+
* @param string $jobCode Cron job code.
58+
* @param string $message
59+
* @return int Count of updated rows.
60+
*/
61+
public function unlockByJobCode(string $jobCode, string $message = self::UPGRADE_UNLOCK_MESSAGE): int
62+
{
63+
$updateCronStatusQuery = 'UPDATE `cron_schedule` SET `status` = :to_status, `messages` = :messages'
64+
. ' WHERE `status` = :from_status AND `job_code` = :job_code';
65+
66+
return $this->connection->affectingQuery(
67+
$updateCronStatusQuery,
68+
[
69+
':to_status' => self::STATUS_ERROR,
70+
':from_status' => self::STATUS_RUNNING,
71+
':job_code' => $jobCode,
72+
':messages' => $message
73+
]
74+
);
75+
}
76+
}

src/Process/Deploy/UnlockCronJobs.php

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66
namespace Magento\MagentoCloud\Process\Deploy;
77

8-
use Magento\MagentoCloud\DB\ConnectionInterface;
98
use Magento\MagentoCloud\Process\ProcessInterface;
9+
use Magento\MagentoCloud\Cron\JobUnlocker;
1010
use Psr\Log\LoggerInterface;
1111

1212
/**
@@ -18,28 +18,25 @@
1818
*/
1919
class UnlockCronJobs implements ProcessInterface
2020
{
21-
const STATUS_RUNNING = 'running';
22-
const STATUS_MISSED = 'missed';
23-
2421
/**
25-
* @var ConnectionInterface
22+
* @var LoggerInterface
2623
*/
27-
private $connection;
24+
private $logger;
2825

2926
/**
30-
* @var LoggerInterface
27+
* @var JobUnlocker
3128
*/
32-
private $logger;
29+
private $jobUnlocker;
3330

3431
/**
35-
* @param ConnectionInterface $connection
32+
* @param JobUnlocker $jobUnlocker
3633
* @param LoggerInterface $logger
3734
*/
3835
public function __construct(
39-
ConnectionInterface $connection,
36+
JobUnlocker $jobUnlocker,
4037
LoggerInterface $logger
4138
) {
42-
$this->connection = $connection;
39+
$this->jobUnlocker = $jobUnlocker;
4340
$this->logger = $logger;
4441
}
4542

@@ -50,23 +47,15 @@ public function __construct(
5047
*/
5148
public function execute()
5249
{
53-
$updateCronStatusQuery = 'UPDATE `cron_schedule` SET `status` = :to_status WHERE `status` = :from_status';
54-
55-
$updatedJobsCount = $this->connection->affectingQuery(
56-
$updateCronStatusQuery,
57-
[
58-
':to_status' => self::STATUS_MISSED,
59-
':from_status' => self::STATUS_RUNNING
60-
]
61-
);
50+
$updatedJobsCount = $this->jobUnlocker->unlockAll();
6251

6352
if ($updatedJobsCount) {
6453
$this->logger->info(
6554
sprintf(
6655
'%d cron jobs were updated from status "%s" to status "%s"',
6756
$updatedJobsCount,
68-
self::STATUS_RUNNING,
69-
self::STATUS_MISSED
57+
JobUnlocker::STATUS_RUNNING,
58+
JobUnlocker::STATUS_ERROR
7059
)
7160
);
7261
}

src/Test/Unit/Command/CronUnlockTest.php

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace Magento\MagentoCloud\Test\Unit\Command;
77

88
use Magento\MagentoCloud\Command\CronUnlock;
9-
use Magento\MagentoCloud\Process\ProcessInterface;
9+
use Magento\MagentoCloud\Cron\JobUnlocker;
1010
use PHPUnit\Framework\TestCase;
1111
use Psr\Log\LoggerInterface;
1212
use Symfony\Component\Console\Tester\CommandTester;
@@ -15,14 +15,14 @@
1515
class CronUnlockTest extends TestCase
1616
{
1717
/**
18-
* @var ProcessInterface|Mock
18+
* @var LoggerInterface|Mock
1919
*/
20-
private $processMock;
20+
private $loggerMock;
2121

2222
/**
23-
* @var LoggerInterface|Mock
23+
* @var JobUnlocker|Mock
2424
*/
25-
private $loggerMock;
25+
private $jobUnlockerMock;
2626

2727
/**
2828
* @var CronUnlock
@@ -34,25 +34,29 @@ class CronUnlockTest extends TestCase
3434
*/
3535
protected function setUp()
3636
{
37-
$this->processMock = $this->getMockForAbstractClass(ProcessInterface::class);
37+
$this->jobUnlockerMock = $this->createMock(JobUnlocker::class);
3838
$this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class);
3939

4040
$this->cronUnlockCommand = new CronUnlock(
41-
$this->processMock,
41+
$this->jobUnlockerMock,
4242
$this->loggerMock
4343
);
4444
}
4545

4646
public function testExecute()
4747
{
48-
$this->loggerMock->expects($this->exactly(2))
48+
$this->loggerMock->expects($this->exactly(3))
4949
->method('info')
5050
->withConsecutive(
5151
['Starting unlocking.'],
52+
['Unlocking all cron jobs.'],
5253
['Unlocking completed.']
5354
);
54-
$this->processMock->expects($this->once())
55-
->method('execute');
55+
$this->jobUnlockerMock->expects($this->once())
56+
->method('unlockAll')
57+
->with(CronUnlock::UNLOCK_MESSAGE);
58+
$this->jobUnlockerMock->expects($this->never())
59+
->method('unlockByJobCode');
5660

5761
$tester = new CommandTester(
5862
$this->cronUnlockCommand
@@ -62,6 +66,35 @@ public function testExecute()
6266
$this->assertSame(0, $tester->getStatusCode());
6367
}
6468

69+
public function testExecuteWithJobCode()
70+
{
71+
$this->loggerMock->expects($this->exactly(4))
72+
->method('info')
73+
->withConsecutive(
74+
['Starting unlocking.'],
75+
['Unlocking cron jobs with code #code1.'],
76+
['Unlocking cron jobs with code #code2.'],
77+
['Unlocking completed.']
78+
);
79+
$this->jobUnlockerMock->expects($this->never())
80+
->method('unlockAll');
81+
$this->jobUnlockerMock->expects($this->exactly(2))
82+
->method('unlockByJobCode')
83+
->withConsecutive(
84+
['code1', CronUnlock::UNLOCK_MESSAGE],
85+
['code2', CronUnlock::UNLOCK_MESSAGE]
86+
);
87+
88+
$tester = new CommandTester(
89+
$this->cronUnlockCommand
90+
);
91+
$tester->execute([
92+
'--job-code' => ['code1', 'code2']
93+
]);
94+
95+
$this->assertSame(0, $tester->getStatusCode());
96+
}
97+
6598
/**
6699
* @expectedException \Exception
67100
* @expectedExceptionMessage Some error
@@ -74,8 +107,8 @@ public function testExecuteWithException()
74107
$this->loggerMock->expects($this->once())
75108
->method('critical')
76109
->with('Some error');
77-
$this->processMock->expects($this->once())
78-
->method('execute')
110+
$this->jobUnlockerMock->expects($this->once())
111+
->method('unlockAll')
79112
->willThrowException(new \Exception('Some error'));
80113

81114
$tester = new CommandTester(

0 commit comments

Comments
 (0)