Skip to content

Commit 70c735e

Browse files
committed
feat(jobs): add command to list executed background jobs
Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
1 parent 66ed05a commit 70c735e

6 files changed

Lines changed: 149 additions & 9 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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\JobClassesRegistry;
13+
use OC\BackgroundJob\JobRuns;
14+
use OC\Core\Command\Base;
15+
use OCP\BackgroundJob\JobStatus;
16+
use OCP\IConfig;
17+
use OCP\Util;
18+
use Override;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Input\InputOption;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
23+
final class JobsHistory extends Base {
24+
public function __construct(
25+
private readonly JobRuns $jobRuns,
26+
private readonly JobClassesRegistry $jobClassesRegistry,
27+
private IConfig $config,
28+
) {
29+
parent::__construct();
30+
}
31+
32+
#[Override]
33+
protected function configure(): void {
34+
parent::configure();
35+
36+
$help = <<<EOF
37+
Display all currently running background jobs.
38+
39+
You can find the following informations:
40+
- <info>Run ID:</info> job identifier a found in database (Snowflake ID)
41+
- <info>Class:</info> class of the job
42+
- <info>Started at:</info> start time of the job
43+
- <info>Server ID:</info> server ID as defined in <options=bold>config.php</> (see `serverid`). Highlighted if it’s running on current server.
44+
- <info>PID:</info> PID of process executing the job
45+
- <info>Duration:</info> human readable duration
46+
- <info>Memory usage:</info> human readable memory usage peak
47+
48+
EOF;
49+
50+
$this
51+
->setName('background-job:history')
52+
->setDescription('Show currently running jobs')
53+
->setHelp($help)
54+
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Maximum number of results returned by the command', 200)
55+
->addOption('class', 'c', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Filter by class name', [])
56+
->addOption('status', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Filter by status', []);
57+
}
58+
59+
#[Override]
60+
protected function execute(InputInterface $input, OutputInterface $output): int {
61+
$limit = (int)$input->getOption('limit');
62+
$classesId = array_map(fn (string $value) => $this->jobClassesRegistry->getId($value), $input->getOption('class'));
63+
$statuses = array_map(fn (string $value) => JobStatus::tryFrom((int)$value), $input->getOption('status'));
64+
$jobs = $this->jobRuns->completedJobs($statuses, $classesId, $limit);
65+
$this->writeStreamingTableInOutputFormat($input, $output, $this->formatLine($jobs), 20);
66+
67+
return Base::SUCCESS;
68+
}
69+
70+
private function formatLine(iterable $jobs): \Generator {
71+
$jobsInfo = [];
72+
$now = time();
73+
$currentServerId = $this->config->getSystemValueInt('serverid', -1);
74+
foreach ($jobs as $job) {
75+
$status = match ($job->status) {
76+
JobStatus::RUNNING => 'Running',
77+
JobStatus::SUCCEEDED => '<info>Succeeded</info>',
78+
JobStatus::FAILED => '<question>Failed</question>',
79+
JobStatus::CRASHED => '<error>Crashed</error>',
80+
default => 'Unknown',
81+
};
82+
yield [
83+
'Run ID' => $job->runId,
84+
'Status' => $status,
85+
'Class' => $job->className,
86+
'Started at' => $job->startedAt->format('Y-m-d H:i:s'),
87+
'Server ID' => $job->serverId === $currentServerId ? '<info>' . $job->serverId . '</info>' : $job->serverId,
88+
'PID' => $job->pid,
89+
'Duration' => $job->duration . ' ms',
90+
'Memory usage' => Util::humanFileSize($job->memoryPeak * 1024),
91+
];
92+
}
93+
94+
return $jobsInfo;
95+
}
96+
}

core/register_command.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use OC\Core\Command\App\Update;
1818
use OC\Core\Command\Background\Delete;
1919
use OC\Core\Command\Background\Job;
20+
use OC\Core\Command\Background\JobsHistory;
2021
use OC\Core\Command\Background\JobWorker;
2122
use OC\Core\Command\Background\ListCommand;
2223
use OC\Core\Command\Background\Mode;
@@ -150,6 +151,7 @@
150151
$application->add(Server::get(Delete::class));
151152
$application->add(Server::get(JobWorker::class));
152153
$application->add(Server::get(RunningJobs::class));
154+
$application->add(Server::get(JobsHistory::class));
153155

154156
$application->add(Server::get(Test::class));
155157

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,7 @@
13361336
'OC\\Core\\Command\\Background\\Delete' => $baseDir . '/core/Command/Background/Delete.php',
13371337
'OC\\Core\\Command\\Background\\Job' => $baseDir . '/core/Command/Background/Job.php',
13381338
'OC\\Core\\Command\\Background\\JobBase' => $baseDir . '/core/Command/Background/JobBase.php',
1339+
'OC\\Core\\Command\\Background\\JobHistory' => $baseDir . '/core/Command/Background/JobHistory.php',
13391340
'OC\\Core\\Command\\Background\\JobWorker' => $baseDir . '/core/Command/Background/JobWorker.php',
13401341
'OC\\Core\\Command\\Background\\ListCommand' => $baseDir . '/core/Command/Background/ListCommand.php',
13411342
'OC\\Core\\Command\\Background\\Mode' => $baseDir . '/core/Command/Background/Mode.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13771377
'OC\\Core\\Command\\Background\\Delete' => __DIR__ . '/../../..' . '/core/Command/Background/Delete.php',
13781378
'OC\\Core\\Command\\Background\\Job' => __DIR__ . '/../../..' . '/core/Command/Background/Job.php',
13791379
'OC\\Core\\Command\\Background\\JobBase' => __DIR__ . '/../../..' . '/core/Command/Background/JobBase.php',
1380+
'OC\\Core\\Command\\Background\\JobHistory' => __DIR__ . '/../../..' . '/core/Command/Background/JobHistory.php',
13801381
'OC\\Core\\Command\\Background\\JobWorker' => __DIR__ . '/../../..' . '/core/Command/Background/JobWorker.php',
13811382
'OC\\Core\\Command\\Background\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/Background/ListCommand.php',
13821383
'OC\\Core\\Command\\Background\\Mode' => __DIR__ . '/../../..' . '/core/Command/Background/Mode.php',

lib/private/BackgroundJob/JobRuns.php

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use OCP\BackgroundJob\IJobRuns;
1212
use OCP\BackgroundJob\JobRun;
1313
use OCP\BackgroundJob\JobStatus;
14+
use OCP\DB\QueryBuilder\IQueryBuilder;
1415
use OCP\IDBConnection;
1516
use OCP\Snowflake\ISnowflakeDecoder;
1617
use OCP\Snowflake\ISnowflakeGenerator;
@@ -67,15 +68,45 @@ public function runningJobs(int $limit = 200): \Generator {
6768
->executeQuery();
6869

6970
foreach ($result->iterateAssociative() as $row) {
70-
$snowflakeInfo = $this->snowflakeDecoder->decode((string)$row['run_id']);
71-
yield new JobRun(
72-
$row['run_id'],
73-
$this->classesRegistry->getName($row['class_id']),
74-
$snowflakeInfo->getServerId(),
75-
$row['pid'],
76-
$snowflakeInfo->getCreatedAt(),
77-
JobStatus::from($row['status']),
78-
);
71+
yield $this->rowToJobRun($row);
7972
}
8073
}
74+
75+
#[Override]
76+
public function completedJobs(array $statuses = [], array $classIds = [], int $limit = 200): \Generator {
77+
if ($statuses === []) {
78+
$statuses = [JobStatus::SUCCEEDED, JobStatus::FAILED, JobStatus::CRASHED];
79+
}
80+
81+
$dbStatuses = array_map(static fn (JobStatus $status) => $status->value, $statuses);
82+
$qb = $this->connection->getQueryBuilder();
83+
$qb
84+
->select('run_id', 'class_id', 'pid', 'status', 'duration', 'ram_peak_usage')
85+
->from(self::TABLE)
86+
->where($qb->expr()->in('status', $qb->createNamedParameter($dbStatuses, IQueryBuilder::PARAM_INT_ARRAY)))
87+
->setMaxResults($limit)
88+
->orderBy('run_id', 'DESC');
89+
90+
if ($classIds !== []) {
91+
$qb->andWhere($qb->expr()->in('class_id', $qb->createNamedParameter($classIds, IQueryBuilder::PARAM_INT_ARRAY)));
92+
}
93+
94+
foreach ($qb->executeQuery()->iterateAssociative() as $row) {
95+
yield $this->rowToJobRun($row);
96+
}
97+
}
98+
99+
private function rowToJobRun(array $dbRow): JobRun {
100+
$snowflakeInfo = $this->snowflakeDecoder->decode((string)$dbRow['run_id']);
101+
return new JobRun(
102+
$dbRow['run_id'],
103+
$this->classesRegistry->getName($dbRow['class_id']),
104+
$snowflakeInfo->getServerId(),
105+
$dbRow['pid'],
106+
$snowflakeInfo->getCreatedAt(),
107+
JobStatus::from($dbRow['status']),
108+
$dbRow['duration'] ?? null,
109+
$dbRow['ram_peak_usage'] ?? null,
110+
);
111+
}
81112
}

lib/public/BackgroundJob/IJobRuns.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,13 @@ public function finished(int $runId, int $duration, int $memoryPeakUsage, JobSta
4242
* @since 34.0.0
4343
*/
4444
public function runningJobs(int $limit = 200): \Generator;
45+
46+
/**
47+
* List of completed jobs
48+
*
49+
* @param JobStatus[] $statuses
50+
* @param array<array-key, int|string> $classIds
51+
* @since 34.0.0
52+
*/
53+
public function completedJobs(array $statuses = [], array $classIds = [], int $limit = 200): \Generator;
4554
}

0 commit comments

Comments
 (0)