Skip to content

Commit 533f84f

Browse files
Merge pull request #60795 from nextcloud/backport/60765/stable34
[stable34] Background jobs improvements
2 parents 9db1e37 + 75853a2 commit 533f84f

16 files changed

Lines changed: 731 additions & 3 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Core\Command\Background;
11+
12+
use OC\BackgroundJob\JobRuns;
13+
use OC\Core\Command\Base;
14+
use OCP\IConfig;
15+
use Override;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Input\InputOption;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
20+
final class RunningJobs extends Base {
21+
public function __construct(
22+
private readonly JobRuns $jobRuns,
23+
private IConfig $config,
24+
) {
25+
parent::__construct();
26+
}
27+
28+
#[Override]
29+
protected function configure(): void {
30+
parent::configure();
31+
32+
$help = <<<EOF
33+
Display all currently running background jobs.
34+
35+
You can find the following informations:
36+
- <info>Run ID:</info> job identifier a found in database (Snowflake ID)
37+
- <info>Class:</info> class of the job
38+
- <info>Started at:</info> start time of the job
39+
- <info>Server ID:</info> server ID as defined in <options=bold>config.php</> (see `serverid`). Highlighted if it’s running on current server.
40+
- <info>PID:</info> PID of process executing the job
41+
- <info>Running since:</info> human readable elapsed time since job started
42+
43+
EOF;
44+
45+
$this
46+
->setName('background-job:running')
47+
->setDescription('Show currently running jobs')
48+
->setHelp($help)
49+
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Maximum number of results returned by the command', 200);
50+
}
51+
52+
#[Override]
53+
protected function execute(InputInterface $input, OutputInterface $output): int {
54+
$limit = (int)$input->getOption('limit');
55+
$jobs = $this->jobRuns->runningJobs($limit);
56+
$this->writeStreamingTableInOutputFormat($input, $output, $this->formatLine($jobs), 20);
57+
58+
return Base::SUCCESS;
59+
}
60+
61+
private function formatLine(iterable $jobs): \Generator {
62+
$now = time();
63+
$currentServerId = $this->config->getSystemValueInt('serverid', -1);
64+
foreach ($jobs as $job) {
65+
yield [
66+
'Run ID' => $job->runId,
67+
'Class' => $job->className,
68+
'Started at' => $job->startedAt->format('Y-m-d H:i:s'),
69+
'Server ID' => $job->serverId === $currentServerId ? '<info>' . $job->serverId . '</info>' : $job->serverId,
70+
'PID' => $job->pid,
71+
'Running since' => $this->formatDuration($now - $job->startedAt->format('U')),
72+
];
73+
}
74+
}
75+
76+
/**
77+
* TODO Move this function to utils class with better formatting (plural, i18n…)
78+
*/
79+
private function formatDuration(int $seconds): string {
80+
if ($seconds < 60) {
81+
return sprintf('%d seconds', $seconds);
82+
}
83+
if ($seconds < 3600) {
84+
return sprintf('%d minutes', $seconds / 60);
85+
}
86+
if ($seconds < (3600 * 24)) {
87+
return sprintf('> %d hours', $seconds / 3600);
88+
}
89+
90+
return sprintf('> %d days', $seconds / (3600 * 24));
91+
}
92+
}
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+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Core\Migrations;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\Attributes\AddIndex;
16+
use OCP\Migration\Attributes\CreateTable;
17+
use OCP\Migration\Attributes\IndexType;
18+
use OCP\Migration\IOutput;
19+
use OCP\Migration\SimpleMigrationStep;
20+
use Override;
21+
22+
#[CreateTable(table: 'job_classes_registry', columns: ['class_id', 'class_name'], description: 'New table to map job class name to an ID')]
23+
#[AddIndex(table: 'job_classes_registry', type: IndexType::PRIMARY)]
24+
#[AddIndex(table: 'job_classes_registry', type: IndexType::UNIQUE, description: 'Ensure each class is registered only once')]
25+
class Version34000Date20260518163022 extends SimpleMigrationStep {
26+
#[Override]
27+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
28+
/** @var ISchemaWrapper $schema */
29+
$schema = $schemaClosure();
30+
31+
if (!$schema->hasTable('job_classes_registry')) {
32+
$table = $schema->createTable('job_classes_registry');
33+
$table->addColumn('class_id', Types::BIGINT, [
34+
'autoincrement' => true,
35+
'notnull' => true,
36+
'unsigned' => true,
37+
]);
38+
$table->addColumn('class_name', Types::STRING, ['notnull' => true, 'length' => 255]);
39+
$table->setPrimaryKey(['class_id']);
40+
$table->addUniqueConstraint(['class_name'], 'class_index');
41+
42+
return $schema;
43+
}
44+
45+
return null;
46+
}
47+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Core\Migrations;
11+
12+
use Closure;
13+
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
15+
use OCP\Migration\Attributes\AddIndex;
16+
use OCP\Migration\Attributes\CreateTable;
17+
use OCP\Migration\Attributes\IndexType;
18+
use OCP\Migration\IOutput;
19+
use OCP\Migration\SimpleMigrationStep;
20+
use Override;
21+
22+
#[CreateTable(
23+
table: 'job_runs',
24+
columns: ['class_id', 'pid', 'status', 'duration', 'ram_peak_usage'],
25+
description: 'New table to store executions of background jobs',
26+
)]
27+
#[AddIndex(table: 'job_runs', type: IndexType::PRIMARY)]
28+
#[AddIndex(table: 'job_runs', type: IndexType::INDEX, description: 'Allows to search on job status')]
29+
class Version34000Date20260521110333 extends SimpleMigrationStep {
30+
#[Override]
31+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
32+
/** @var ISchemaWrapper $schema */
33+
$schema = $schemaClosure();
34+
35+
if (!$schema->hasTable('job_runs')) {
36+
$table = $schema->createTable('job_runs');
37+
$table->addColumn('run_id', Types::BIGINT, [
38+
'notnull' => true,
39+
'unsigned' => true,
40+
]);
41+
$table->addColumn('class_id', Types::BIGINT, ['notnull' => true]);
42+
$table->addColumn('pid', Types::INTEGER, ['notnull' => true]); // Should be MEDIUMINT
43+
$table->addColumn('status', Types::SMALLINT, ['notnull' => true]); // Should be TINYINT
44+
$table->addColumn('duration', Types::INTEGER, ['notnull' => true, 'default' => 0]);
45+
$table->addColumn('ram_peak_usage', Types::INTEGER, ['notnull' => true, 'default' => 0]); // Should be MEDIUMINT
46+
$table->setPrimaryKey(['run_id']);
47+
$table->addIndex(['status'], 'status');
48+
49+
return $schema;
50+
}
51+
52+
return null;
53+
}
54+
}

core/Service/CronService.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use OC;
1515
use OC\Authentication\LoginCredentials\Store;
16+
use OC\BackgroundJob\JobClassesRegistry;
17+
use OC\BackgroundJob\JobRuns;
1618
use OC\DB\Connection;
1719
use OC\Security\CSRF\TokenStorage\SessionStorage;
1820
use OC\Session\CryptoWrapper;
@@ -49,6 +51,8 @@ public function __construct(
4951
private readonly ITempManager $tempManager,
5052
private readonly IAppConfig $appConfig,
5153
private readonly IJobList $jobList,
54+
private readonly JobRuns $jobRuns,
55+
private readonly JobClassesRegistry $jobClassesRegistry,
5256
private readonly ISetupManager $setupManager,
5357
private readonly bool $isCLI,
5458
) {
@@ -185,20 +189,27 @@ private function runCli(string $appMode, ?array $jobClasses): void {
185189
break;
186190
}
187191

188-
$jobDetails = get_class($job) . ' (id: ' . $job->getId() . ', arguments: ' . json_encode($job->getArgument()) . ')';
192+
$jobClass = get_class($job);
193+
$jobDetails = $jobClass . ' (id: ' . $job->getId() . ', arguments: ' . json_encode($job->getArgument()) . ')';
189194
$this->logger->debug('CLI cron call has selected job ' . $jobDetails, ['app' => 'cron']);
190195

191196
$this->verboseOutput('Starting job ' . $jobDetails);
192197

193-
$startTime = microtime(true);
194198
$referenceMemory = memory_get_usage();
195199
memory_reset_peak_usage();
196200

201+
$jobClassId = $this->jobClassesRegistry->getId($jobClass);
202+
$jobRunId = $this->jobRuns->started($jobClassId);
203+
$startTime = microtime(true);
197204
$job->start($this->jobList);
198205

199206
$memoryIncrease = memory_get_usage() - $referenceMemory;
200207
$timeSpent = microtime(true) - $startTime;
201-
$jobMemoryPeak = memory_get_peak_usage() - $referenceMemory;
208+
$jobMemoryPeak = memory_get_peak_usage();
209+
// TODO Job failure will never be caught here because exceptions are caught within $job->start method
210+
// The error will only be visible in server logs.
211+
// It should be a temporary state until a proper job runner is implemented.
212+
$this->jobRuns->finished($jobRunId, (int)($timeSpent * 1000), (int)($jobMemoryPeak / 1024));
202213

203214
$cronInterval = 5 * 60;
204215
if ($timeSpent > $cronInterval) {

core/register_command.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use OC\Core\Command\Background\JobWorker;
2121
use OC\Core\Command\Background\ListCommand;
2222
use OC\Core\Command\Background\Mode;
23+
use OC\Core\Command\Background\RunningJobs;
2324
use OC\Core\Command\Broadcast\Test;
2425
use OC\Core\Command\Check;
2526
use OC\Core\Command\Config\App\DeleteConfig;
@@ -144,6 +145,7 @@
144145
$application->add(Server::get(ListCommand::class));
145146
$application->add(Server::get(Delete::class));
146147
$application->add(Server::get(JobWorker::class));
148+
$application->add(Server::get(RunningJobs::class));
147149

148150
$application->add(Server::get(Test::class));
149151

lib/composer/composer/autoload_classmap.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,11 @@
200200
'OCP\\AutoloadNotAllowedException' => $baseDir . '/lib/public/AutoloadNotAllowedException.php',
201201
'OCP\\BackgroundJob\\IJob' => $baseDir . '/lib/public/BackgroundJob/IJob.php',
202202
'OCP\\BackgroundJob\\IJobList' => $baseDir . '/lib/public/BackgroundJob/IJobList.php',
203+
'OCP\\BackgroundJob\\IJobRuns' => $baseDir . '/lib/public/BackgroundJob/IJobRuns.php',
203204
'OCP\\BackgroundJob\\IParallelAwareJob' => $baseDir . '/lib/public/BackgroundJob/IParallelAwareJob.php',
204205
'OCP\\BackgroundJob\\Job' => $baseDir . '/lib/public/BackgroundJob/Job.php',
206+
'OCP\\BackgroundJob\\JobRun' => $baseDir . '/lib/public/BackgroundJob/JobRun.php',
207+
'OCP\\BackgroundJob\\JobStatus' => $baseDir . '/lib/public/BackgroundJob/JobStatus.php',
205208
'OCP\\BackgroundJob\\QueuedJob' => $baseDir . '/lib/public/BackgroundJob/QueuedJob.php',
206209
'OCP\\BackgroundJob\\TimedJob' => $baseDir . '/lib/public/BackgroundJob/TimedJob.php',
207210
'OCP\\BeforeSabrePubliclyLoadedEvent' => $baseDir . '/lib/public/BeforeSabrePubliclyLoadedEvent.php',
@@ -1250,7 +1253,9 @@
12501253
'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php',
12511254
'OC\\Avatar\\PlaceholderAvatar' => $baseDir . '/lib/private/Avatar/PlaceholderAvatar.php',
12521255
'OC\\Avatar\\UserAvatar' => $baseDir . '/lib/private/Avatar/UserAvatar.php',
1256+
'OC\\BackgroundJob\\JobClassesRegistry' => $baseDir . '/lib/private/BackgroundJob/JobClassesRegistry.php',
12531257
'OC\\BackgroundJob\\JobList' => $baseDir . '/lib/private/BackgroundJob/JobList.php',
1258+
'OC\\BackgroundJob\\JobRuns' => $baseDir . '/lib/private/BackgroundJob/JobRuns.php',
12541259
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
12551260
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
12561261
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
@@ -1333,6 +1338,7 @@
13331338
'OC\\Core\\Command\\Background\\JobWorker' => $baseDir . '/core/Command/Background/JobWorker.php',
13341339
'OC\\Core\\Command\\Background\\ListCommand' => $baseDir . '/core/Command/Background/ListCommand.php',
13351340
'OC\\Core\\Command\\Background\\Mode' => $baseDir . '/core/Command/Background/Mode.php',
1341+
'OC\\Core\\Command\\Background\\RunningJobs' => $baseDir . '/core/Command/Background/RunningJobs.php',
13361342
'OC\\Core\\Command\\Base' => $baseDir . '/core/Command/Base.php',
13371343
'OC\\Core\\Command\\Broadcast\\Test' => $baseDir . '/core/Command/Broadcast/Test.php',
13381344
'OC\\Core\\Command\\Check' => $baseDir . '/core/Command/Check.php',
@@ -1607,6 +1613,8 @@
16071613
'OC\\Core\\Migrations\\Version33000Date20260126120000' => $baseDir . '/core/Migrations/Version33000Date20260126120000.php',
16081614
'OC\\Core\\Migrations\\Version34000Date20260318095645' => $baseDir . '/core/Migrations/Version34000Date20260318095645.php',
16091615
'OC\\Core\\Migrations\\Version34000Date20260415161745' => $baseDir . '/core/Migrations/Version34000Date20260415161745.php',
1616+
'OC\\Core\\Migrations\\Version34000Date20260518163022' => $baseDir . '/core/Migrations/Version34000Date20260518163022.php',
1617+
'OC\\Core\\Migrations\\Version34000Date20260521110333' => $baseDir . '/core/Migrations/Version34000Date20260521110333.php',
16101618
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
16111619
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
16121620
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',

lib/composer/composer/autoload_static.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
241241
'OCP\\AutoloadNotAllowedException' => __DIR__ . '/../../..' . '/lib/public/AutoloadNotAllowedException.php',
242242
'OCP\\BackgroundJob\\IJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJob.php',
243243
'OCP\\BackgroundJob\\IJobList' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJobList.php',
244+
'OCP\\BackgroundJob\\IJobRuns' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJobRuns.php',
244245
'OCP\\BackgroundJob\\IParallelAwareJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IParallelAwareJob.php',
245246
'OCP\\BackgroundJob\\Job' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/Job.php',
247+
'OCP\\BackgroundJob\\JobRun' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/JobRun.php',
248+
'OCP\\BackgroundJob\\JobStatus' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/JobStatus.php',
246249
'OCP\\BackgroundJob\\QueuedJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/QueuedJob.php',
247250
'OCP\\BackgroundJob\\TimedJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/TimedJob.php',
248251
'OCP\\BeforeSabrePubliclyLoadedEvent' => __DIR__ . '/../../..' . '/lib/public/BeforeSabrePubliclyLoadedEvent.php',
@@ -1291,7 +1294,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
12911294
'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php',
12921295
'OC\\Avatar\\PlaceholderAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/PlaceholderAvatar.php',
12931296
'OC\\Avatar\\UserAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/UserAvatar.php',
1297+
'OC\\BackgroundJob\\JobClassesRegistry' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobClassesRegistry.php',
12941298
'OC\\BackgroundJob\\JobList' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobList.php',
1299+
'OC\\BackgroundJob\\JobRuns' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobRuns.php',
12951300
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
12961301
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
12971302
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
@@ -1374,6 +1379,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13741379
'OC\\Core\\Command\\Background\\JobWorker' => __DIR__ . '/../../..' . '/core/Command/Background/JobWorker.php',
13751380
'OC\\Core\\Command\\Background\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/Background/ListCommand.php',
13761381
'OC\\Core\\Command\\Background\\Mode' => __DIR__ . '/../../..' . '/core/Command/Background/Mode.php',
1382+
'OC\\Core\\Command\\Background\\RunningJobs' => __DIR__ . '/../../..' . '/core/Command/Background/RunningJobs.php',
13771383
'OC\\Core\\Command\\Base' => __DIR__ . '/../../..' . '/core/Command/Base.php',
13781384
'OC\\Core\\Command\\Broadcast\\Test' => __DIR__ . '/../../..' . '/core/Command/Broadcast/Test.php',
13791385
'OC\\Core\\Command\\Check' => __DIR__ . '/../../..' . '/core/Command/Check.php',
@@ -1648,6 +1654,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
16481654
'OC\\Core\\Migrations\\Version33000Date20260126120000' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20260126120000.php',
16491655
'OC\\Core\\Migrations\\Version34000Date20260318095645' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260318095645.php',
16501656
'OC\\Core\\Migrations\\Version34000Date20260415161745' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260415161745.php',
1657+
'OC\\Core\\Migrations\\Version34000Date20260518163022' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260518163022.php',
1658+
'OC\\Core\\Migrations\\Version34000Date20260521110333' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260521110333.php',
16511659
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
16521660
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
16531661
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',

0 commit comments

Comments
 (0)