Skip to content

Commit 7250f54

Browse files
authored
Merge pull request #37835 from nextcloud/feat/background-allow-parallel-runs
feat(BackgroundJobs): Allow preventing parallel runs for a job class
2 parents 77bb867 + 06d6cf4 commit 7250f54

File tree

11 files changed

+217
-13
lines changed

11 files changed

+217
-13
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
'OCP\\AutoloadNotAllowedException' => $baseDir . '/lib/public/AutoloadNotAllowedException.php',
121121
'OCP\\BackgroundJob\\IJob' => $baseDir . '/lib/public/BackgroundJob/IJob.php',
122122
'OCP\\BackgroundJob\\IJobList' => $baseDir . '/lib/public/BackgroundJob/IJobList.php',
123+
'OCP\\BackgroundJob\\IParallelAwareJob' => $baseDir . '/lib/public/BackgroundJob/IParallelAwareJob.php',
123124
'OCP\\BackgroundJob\\Job' => $baseDir . '/lib/public/BackgroundJob/Job.php',
124125
'OCP\\BackgroundJob\\QueuedJob' => $baseDir . '/lib/public/BackgroundJob/QueuedJob.php',
125126
'OCP\\BackgroundJob\\TimedJob' => $baseDir . '/lib/public/BackgroundJob/TimedJob.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
153153
'OCP\\AutoloadNotAllowedException' => __DIR__ . '/../../..' . '/lib/public/AutoloadNotAllowedException.php',
154154
'OCP\\BackgroundJob\\IJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJob.php',
155155
'OCP\\BackgroundJob\\IJobList' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJobList.php',
156+
'OCP\\BackgroundJob\\IParallelAwareJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IParallelAwareJob.php',
156157
'OCP\\BackgroundJob\\Job' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/Job.php',
157158
'OCP\\BackgroundJob\\QueuedJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/QueuedJob.php',
158159
'OCP\\BackgroundJob\\TimedJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/TimedJob.php',

lib/private/BackgroundJob/JobList.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,23 @@
3535
use OCP\AutoloadNotAllowedException;
3636
use OCP\BackgroundJob\IJob;
3737
use OCP\BackgroundJob\IJobList;
38+
use OCP\DB\Exception;
3839
use OCP\DB\QueryBuilder\IQueryBuilder;
3940
use OCP\IConfig;
4041
use OCP\IDBConnection;
42+
use Psr\Log\LoggerInterface;
4143

4244
class JobList implements IJobList {
4345
protected IDBConnection $connection;
4446
protected IConfig $config;
4547
protected ITimeFactory $timeFactory;
48+
protected LoggerInterface $logger;
4649

47-
public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory) {
50+
public function __construct(IDBConnection $connection, IConfig $config, ITimeFactory $timeFactory, LoggerInterface $logger) {
4851
$this->connection = $connection;
4952
$this->config = $config;
5053
$this->timeFactory = $timeFactory;
54+
$this->logger = $logger;
5155
}
5256

5357
/**
@@ -382,4 +386,26 @@ public function resetBackgroundJob(IJob $job): void {
382386
->where($query->expr()->eq('id', $query->createNamedParameter($job->getId()), IQueryBuilder::PARAM_INT));
383387
$query->executeStatement();
384388
}
389+
390+
public function hasReservedJob(?string $className = null): bool {
391+
$query = $this->connection->getQueryBuilder();
392+
$query->select('*')
393+
->from('jobs')
394+
->where($query->expr()->neq('reserved_at', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
395+
->setMaxResults(1);
396+
397+
if ($className !== null) {
398+
$query->andWhere($query->expr()->eq('class', $query->createNamedParameter($className)));
399+
}
400+
401+
try {
402+
$result = $query->executeQuery();
403+
$hasReservedJobs = $result->fetch() !== false;
404+
$result->closeCursor();
405+
return $hasReservedJobs;
406+
} catch (Exception $e) {
407+
$this->logger->debug('Querying reserved jobs failed', ['exception' => $e]);
408+
return false;
409+
}
410+
}
385411
}

lib/private/SpeechToText/TranscriptionJob.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function __construct(
4949
private LoggerInterface $logger,
5050
) {
5151
parent::__construct($timeFactory);
52+
$this->setAllowParallelRuns(false);
5253
}
5354

5455

lib/public/BackgroundJob/IJobList.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,13 @@ public function setExecutionTime(IJob $job, int $timeTaken): void;
145145
* @since 23.0.0
146146
*/
147147
public function resetBackgroundJob(IJob $job): void;
148+
149+
/**
150+
* Checks whether a job of the passed class is reserved to run
151+
*
152+
* @param string|null $className
153+
* @return bool
154+
* @since 27.0.0
155+
*/
156+
public function hasReservedJob(?string $className): bool;
148157
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
6+
/**
7+
* @copyright Copyright (c) 2023, Marcel Klehr <[email protected]>
8+
*
9+
* @author Marcel Klehr <[email protected]>
10+
*
11+
* @license GNU AGPL version 3 or any later version
12+
*
13+
* This program is free software: you can redistribute it and/or modify
14+
* it under the terms of the GNU Affero General Public License as
15+
* published by the Free Software Foundation, either version 3 of the
16+
* License, or (at your option) any later version.
17+
*
18+
* This program is distributed in the hope that it will be useful,
19+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
20+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+
* GNU Affero General Public License for more details.
22+
*
23+
* You should have received a copy of the GNU Affero General Public License
24+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
25+
*
26+
*/
27+
namespace OCP\BackgroundJob;
28+
29+
/**
30+
* @since 27.0.0
31+
*/
32+
interface IParallelAwareJob {
33+
/**
34+
* Set this to false to prevent two Jobs from the same class from running in parallel
35+
*
36+
* @param bool $allow
37+
* @return void
38+
* @since 27.0.0
39+
*/
40+
public function setAllowParallelRuns(bool $allow): void;
41+
42+
/**
43+
* @return bool
44+
* @since 27.0.0
45+
*/
46+
public function getAllowParallelRuns(): bool;
47+
}

lib/public/BackgroundJob/Job.php

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
*
3939
* @since 15.0.0
4040
*/
41-
abstract class Job implements IJob {
41+
abstract class Job implements IJob, IParallelAwareJob {
4242
protected int $id = 0;
4343
protected int $lastRun = 0;
4444
protected $argument;
4545
protected ITimeFactory $time;
46+
protected bool $allowParallelRuns = true;
47+
private ?ILogger $logger = null;
4648

4749
/**
4850
* @since 15.0.0
@@ -61,6 +63,7 @@ public function __construct(ITimeFactory $time) {
6163
* @since 15.0.0
6264
*/
6365
public function execute(IJobList $jobList, ILogger $logger = null) {
66+
$this->logger = $logger;
6467
$this->start($jobList);
6568
}
6669

@@ -70,7 +73,12 @@ public function execute(IJobList $jobList, ILogger $logger = null) {
7073
*/
7174
public function start(IJobList $jobList): void {
7275
$jobList->setLastRun($this);
73-
$logger = \OCP\Server::get(LoggerInterface::class);
76+
$logger = $this->logger ?? \OCP\Server::get(LoggerInterface::class);
77+
78+
if (!$this->getAllowParallelRuns() && $jobList->hasReservedJob(get_class($this))) {
79+
$logger->debug('Skipping ' . get_class($this) . ' job with ID ' . $this->getId() . ' because another job with the same class is already running', ['app' => 'cron']);
80+
return;
81+
}
7482

7583
try {
7684
$jobStartTime = $this->time->getTime();
@@ -80,7 +88,7 @@ public function start(IJobList $jobList): void {
8088

8189
$logger->debug('Finished ' . get_class($this) . ' job with ID ' . $this->getId() . ' in ' . $timeTaken . ' seconds', ['app' => 'cron']);
8290
$jobList->setExecutionTime($this, $timeTaken);
83-
} catch (\Exception $e) {
91+
} catch (\Throwable $e) {
8492
if ($logger) {
8593
$logger->error('Error while running background job (class: ' . get_class($this) . ', arguments: ' . print_r($this->argument, true) . ')', [
8694
'app' => 'core',
@@ -132,6 +140,25 @@ public function getArgument() {
132140
return $this->argument;
133141
}
134142

143+
/**
144+
* Set this to false to prevent two Jobs from this class from running in parallel
145+
*
146+
* @param bool $allow
147+
* @return void
148+
* @since 27.0.0
149+
*/
150+
public function setAllowParallelRuns(bool $allow): void {
151+
$this->allowParallelRuns = $allow;
152+
}
153+
154+
/**
155+
* @return bool
156+
* @since 27.0.0
157+
*/
158+
public function getAllowParallelRuns(): bool {
159+
return $this->allowParallelRuns;
160+
}
161+
135162
/**
136163
* The actual function that is called to run the job
137164
*

tests/lib/BackgroundJob/DummyJobList.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class DummyJobList extends \OC\BackgroundJob\JobList {
2121
*/
2222
private array $jobs = [];
2323

24+
/**
25+
* @var bool[]
26+
*/
27+
private array $reserved = [];
28+
2429
private int $last = 0;
2530

2631
public function __construct() {
@@ -135,6 +140,14 @@ public function setLastRun(IJob $job): void {
135140
$job->setLastRun(time());
136141
}
137142

143+
public function hasReservedJob(?string $className = null): bool {
144+
return $this->reserved[$className ?? ''];
145+
}
146+
147+
public function setHasReservedJob(?string $className, bool $hasReserved): void {
148+
$this->reserved[$className ?? ''] = $hasReserved;
149+
}
150+
138151
public function setExecutionTime(IJob $job, $timeTaken): void {
139152
}
140153

tests/lib/BackgroundJob/JobListTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCP\BackgroundJob\IJob;
1313
use OCP\DB\QueryBuilder\IQueryBuilder;
1414
use OCP\IConfig;
15+
use Psr\Log\LoggerInterface;
1516
use Test\TestCase;
1617

1718
/**
@@ -32,6 +33,7 @@ class JobListTest extends TestCase {
3233

3334
/** @var \OCP\AppFramework\Utility\ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
3435
protected $timeFactory;
36+
private bool $ran = false;
3537

3638
protected function setUp(): void {
3739
parent::setUp();
@@ -43,7 +45,8 @@ protected function setUp(): void {
4345
$this->instance = new \OC\BackgroundJob\JobList(
4446
$this->connection,
4547
$this->config,
46-
$this->timeFactory
48+
$this->timeFactory,
49+
\OC::$server->get(LoggerInterface::class),
4750
);
4851
}
4952

@@ -244,4 +247,24 @@ public function testSetLastRun() {
244247
$this->assertGreaterThanOrEqual($timeStart, $addedJob->getLastRun());
245248
$this->assertLessThanOrEqual($timeEnd, $addedJob->getLastRun());
246249
}
250+
251+
public function testHasReservedJobs() {
252+
$this->clearJobsList();
253+
$job = new TestJob($this->timeFactory, $this, function () {
254+
$this->assertTrue($this->instance->hasReservedJob());
255+
$this->assertTrue($this->instance->hasReservedJob(TestJob::class));
256+
});
257+
$this->instance->add($job);
258+
259+
$this->assertFalse($this->instance->hasReservedJob());
260+
$this->assertFalse($this->instance->hasReservedJob(TestJob::class));
261+
262+
$job->start($this->instance);
263+
264+
$this->assertTrue($this->ran);
265+
}
266+
267+
public function markRun() {
268+
$this->ran = true;
269+
}
247270
}

0 commit comments

Comments
 (0)