Skip to content

Commit 50df29d

Browse files
authored
Merge pull request #9900 from nextcloud/feature/noid/wait-for-cron-to-finish
Wait for cron to finish before running upgrade command
2 parents f9b24cb + 18e9631 commit 50df29d

File tree

5 files changed

+73
-12
lines changed

5 files changed

+73
-12
lines changed

core/Command/Upgrade.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ protected function execute(InputInterface $input, OutputInterface $output) {
9999
$this->config,
100100
\OC::$server->getIntegrityCodeChecker(),
101101
$this->logger,
102-
$this->installer
102+
$this->installer,
103+
\OC::$server->getJobList()
103104
);
104105

105106
$dispatcher = \OC::$server->getEventDispatcher();
@@ -191,6 +192,9 @@ protected function execute(InputInterface $input, OutputInterface $output) {
191192
$updater->listen('\OC\Updater', 'maintenanceActive', function () use($output) {
192193
$output->writeln('<info>Maintenance mode is kept active</info>');
193194
});
195+
$updater->listen('\OC\Updater', 'waitForCronToFinish', function () use($output) {
196+
$output->writeln('<info>Waiting for cron to finish (checks again in 5 seconds)...</info>');
197+
});
194198
$updater->listen('\OC\Updater', 'updateEnd',
195199
function ($success) use($output, $self) {
196200
if ($success) {

core/ajax/update.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ public function handleRepairFeedback($event) {
119119
$config,
120120
\OC::$server->getIntegrityCodeChecker(),
121121
$logger,
122-
\OC::$server->query(\OC\Installer::class)
122+
\OC::$server->query(\OC\Installer::class),
123+
\OC::$server->getJobList()
123124
);
124125
$incompatibleApps = [];
125126

@@ -152,6 +153,9 @@ public function handleRepairFeedback($event) {
152153
$updater->listen('\OC\Updater', 'maintenanceActive', function () use ($eventSource, $l) {
153154
$eventSource->send('success', (string)$l->t('Maintenance mode is kept active'));
154155
});
156+
$updater->listen('\OC\Updater', 'waitForCronToFinish', function () use ($eventSource, $l) {
157+
$eventSource->send('success', (string)$l->t('Waiting for cron to finish (checks again in 5 seconds)...'));
158+
});
155159
$updater->listen('\OC\Updater', 'dbUpgradeBefore', function () use($eventSource, $l) {
156160
$eventSource->send('success', (string)$l->t('Updating database schema'));
157161
});

lib/private/BackgroundJob/JobList.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ class JobList implements IJobList {
4848
/**@var ITimeFactory */
4949
protected $timeFactory;
5050

51+
/** @var int - 12 hours * 3600 seconds*/
52+
private $jobTimeOut = 43200;
53+
5154
/**
5255
* @param IDBConnection $connection
5356
* @param IConfig $config
@@ -182,7 +185,7 @@ public function getNext() {
182185
$query = $this->connection->getQueryBuilder();
183186
$query->select('*')
184187
->from('jobs')
185-
->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - 12 * 3600, IQueryBuilder::PARAM_INT)))
188+
->where($query->expr()->lte('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - $this->jobTimeOut, IQueryBuilder::PARAM_INT)))
186189
->orderBy('last_checked', 'ASC')
187190
->setMaxResults(1);
188191

@@ -333,4 +336,39 @@ public function setExecutionTime(IJob $job, $timeTaken) {
333336
->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT)));
334337
$query->execute();
335338
}
339+
340+
/**
341+
* checks if a job is still running (reserved_at time is smaller than 12 hours ago)
342+
*
343+
* Background information:
344+
*
345+
* The 12 hours is the same timeout that is also used to re-schedule an non-terminated
346+
* job (see getNext()). The idea here is to give a job enough time to run very
347+
* long but still be able to recognize that it maybe crashed and re-schedule it
348+
* after the timeout. It's more likely to be crashed at that time than it ran
349+
* that long.
350+
*
351+
* In theory it could lead to an nearly endless loop (as in - at most 12 hours).
352+
* The cron command will not start new jobs when maintenance mode is active and
353+
* this method is only executed in maintenance mode (see where it is called in
354+
* the upgrader class. So this means in the worst case we wait 12 hours when a
355+
* job has crashed. On the other hand: then the instance should be fixed anyways.
356+
*
357+
* @return bool
358+
*/
359+
public function isAnyJobRunning(): bool {
360+
$query = $this->connection->getQueryBuilder();
361+
$query->select('*')
362+
->from('jobs')
363+
->where($query->expr()->gt('reserved_at', $query->createNamedParameter($this->timeFactory->getTime() - $this->jobTimeOut, IQueryBuilder::PARAM_INT)))
364+
->setMaxResults(1);
365+
$result = $query->execute();
366+
$row = $result->fetch();
367+
$result->closeCursor();
368+
369+
if ($row) {
370+
return true;
371+
}
372+
return false;
373+
}
336374
}

lib/private/Updater.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use OC\Hooks\BasicEmitter;
3939
use OC\IntegrityCheck\Checker;
4040
use OC_App;
41+
use OCP\BackgroundJob\IJobList;
4142
use OCP\IConfig;
4243
use OCP\ILogger;
4344
use OCP\Util;
@@ -66,6 +67,9 @@ class Updater extends BasicEmitter {
6667
/** @var Installer */
6768
private $installer;
6869

70+
/** @var IJobList */
71+
private $jobList;
72+
6973
private $logLevelNames = [
7074
0 => 'Debug',
7175
1 => 'Info',
@@ -74,20 +78,16 @@ class Updater extends BasicEmitter {
7478
4 => 'Fatal',
7579
];
7680

77-
/**
78-
* @param IConfig $config
79-
* @param Checker $checker
80-
* @param ILogger $log
81-
* @param Installer $installer
82-
*/
8381
public function __construct(IConfig $config,
8482
Checker $checker,
85-
ILogger $log = null,
86-
Installer $installer) {
83+
ILogger $log,
84+
Installer $installer,
85+
IJobList $jobList) {
8786
$this->log = $log;
8887
$this->config = $config;
8988
$this->checker = $checker;
9089
$this->installer = $installer;
90+
$this->jobList = $jobList;
9191
}
9292

9393
/**
@@ -111,6 +111,8 @@ public function upgrade() {
111111
$this->emit('\OC\Updater', 'maintenanceEnabled');
112112
}
113113

114+
$this->waitForCronToFinish();
115+
114116
$installedVersion = $this->config->getSystemValue('version', '0.0.0');
115117
$currentVersion = implode('.', \OCP\Util::getVersion());
116118
$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, array('app' => 'core'));
@@ -604,6 +606,12 @@ private function logAllEvents() {
604606
});
605607

606608
}
609+
private function waitForCronToFinish() {
610+
while ($this->jobList->isAnyJobRunning()) {
611+
$this->emit('\OC\Updater', 'waitForCronToFinish');
612+
sleep(5);
613+
}
614+
}
607615

608616
}
609617

tests/lib/UpdaterTest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
use OC\Installer;
2626
use OC\Updater;
27+
use OCP\BackgroundJob\IJobList;
2728
use OCP\IConfig;
2829
use OCP\ILogger;
2930
use OC\IntegrityCheck\Checker;
@@ -39,6 +40,8 @@ class UpdaterTest extends TestCase {
3940
private $checker;
4041
/** @var Installer|\PHPUnit_Framework_MockObject_MockObject */
4142
private $installer;
43+
/** @var IJobList|\PHPUnit_Framework_MockObject_MockObject */
44+
private $jobList;
4245

4346
public function setUp() {
4447
parent::setUp();
@@ -54,12 +57,16 @@ public function setUp() {
5457
$this->installer = $this->getMockBuilder(Installer::class)
5558
->disableOriginalConstructor()
5659
->getMock();
60+
$this->jobList = $this->getMockBuilder(IJobList::class)
61+
->disableOriginalConstructor()
62+
->getMock();
5763

5864
$this->updater = new Updater(
5965
$this->config,
6066
$this->checker,
6167
$this->logger,
62-
$this->installer
68+
$this->installer,
69+
$this->jobList
6370
);
6471
}
6572

0 commit comments

Comments
 (0)