mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 00:32:29 -04:00
Merge pull request #60779 from nextcloud/feat/completed_jobs_list
feat(jobs): add command to list executed background jobs
This commit is contained in:
commit
1b1c74e848
7 changed files with 192 additions and 10 deletions
101
core/Command/Background/JobsHistory.php
Normal file
101
core/Command/Background/JobsHistory.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Core\Command\Background;
|
||||
|
||||
use OC\BackgroundJob\JobRuns;
|
||||
use OC\Core\Command\Base;
|
||||
use OCP\BackgroundJob\JobStatus;
|
||||
use OCP\IConfig;
|
||||
use OCP\Util;
|
||||
use Override;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ValueError;
|
||||
|
||||
final class JobsHistory extends Base {
|
||||
public function __construct(
|
||||
private readonly JobRuns $jobRuns,
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function configure(): void {
|
||||
parent::configure();
|
||||
|
||||
$help = <<<EOF
|
||||
Display all currently running background jobs.
|
||||
|
||||
You can find the following informations:
|
||||
- <info>Run ID:</info> job identifier as found in database (Snowflake ID)
|
||||
- <info>Class:</info> class of the job
|
||||
- <info>Started at:</info> start time of the job
|
||||
- <info>Server ID:</info> server ID as defined in <options=bold>config.php</> (see `serverid`). Highlighted if it’s running on current server.
|
||||
- <info>PID:</info> PID of process executing the job
|
||||
- <info>Duration:</info> human readable duration
|
||||
- <info>Memory usage:</info> human readable memory usage peak
|
||||
|
||||
EOF;
|
||||
|
||||
$this
|
||||
->setName('background-job:history')
|
||||
->setDescription('Show currently running jobs')
|
||||
->setHelp($help)
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Maximum number of results returned by the command', 200)
|
||||
->addOption('class', 'c', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Filter by class name', [])
|
||||
->addOption('status', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Filter by status', []);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$limit = (int)$input->getOption('limit');
|
||||
$classesId = $input->getOption('class');
|
||||
try {
|
||||
$statuses = array_map(fn (string $value) => JobStatus::from((int)$value), $input->getOption('status'));
|
||||
} catch (ValueError $e) {
|
||||
$output->writeln('<error>Invalid status provided</error>');
|
||||
$output->writeln($e->getMessage());
|
||||
return Base::FAILURE;
|
||||
}
|
||||
$jobs = $this->jobRuns->completedJobs($statuses, $classesId, $limit);
|
||||
$this->writeStreamingTableInOutputFormat($input, $output, $this->formatLine($jobs), 20);
|
||||
|
||||
return Base::SUCCESS;
|
||||
}
|
||||
|
||||
private function formatLine(iterable $jobs): \Generator {
|
||||
$jobsInfo = [];
|
||||
$now = time();
|
||||
$currentServerId = $this->config->getSystemValueInt('serverid', -1);
|
||||
foreach ($jobs as $job) {
|
||||
$status = match ($job->status) {
|
||||
JobStatus::RUNNING => 'Running',
|
||||
JobStatus::SUCCEEDED => '<info>Succeeded</info>',
|
||||
JobStatus::FAILED => '<question>Failed</question>',
|
||||
JobStatus::CRASHED => '<error>Crashed</error>',
|
||||
default => 'Unknown',
|
||||
};
|
||||
yield [
|
||||
'Run ID' => $job->runId,
|
||||
'Status' => $status,
|
||||
'Class' => $job->className,
|
||||
'Started at' => $job->startedAt->format('Y-m-d H:i:s'),
|
||||
'Server ID' => $job->serverId === $currentServerId ? '<info>' . $job->serverId . '</info>' : $job->serverId,
|
||||
'PID' => $job->pid,
|
||||
'Duration' => $job->duration . ' ms',
|
||||
'Memory usage' => Util::humanFileSize($job->memoryPeak * 1024),
|
||||
];
|
||||
}
|
||||
|
||||
return $jobsInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ use OC\Core\Command\App\Remove;
|
|||
use OC\Core\Command\App\Update;
|
||||
use OC\Core\Command\Background\Delete;
|
||||
use OC\Core\Command\Background\Job;
|
||||
use OC\Core\Command\Background\JobsHistory;
|
||||
use OC\Core\Command\Background\JobWorker;
|
||||
use OC\Core\Command\Background\ListCommand;
|
||||
use OC\Core\Command\Background\Mode;
|
||||
|
|
@ -150,6 +151,7 @@ if ($config->getSystemValueBool('installed', false)) {
|
|||
$application->add(Server::get(Delete::class));
|
||||
$application->add(Server::get(JobWorker::class));
|
||||
$application->add(Server::get(RunningJobs::class));
|
||||
$application->add(Server::get(JobsHistory::class));
|
||||
|
||||
$application->add(Server::get(Test::class));
|
||||
|
||||
|
|
|
|||
|
|
@ -1336,6 +1336,7 @@ return array(
|
|||
'OC\\Core\\Command\\Background\\Job' => $baseDir . '/core/Command/Background/Job.php',
|
||||
'OC\\Core\\Command\\Background\\JobBase' => $baseDir . '/core/Command/Background/JobBase.php',
|
||||
'OC\\Core\\Command\\Background\\JobWorker' => $baseDir . '/core/Command/Background/JobWorker.php',
|
||||
'OC\\Core\\Command\\Background\\JobsHistory' => $baseDir . '/core/Command/Background/JobsHistory.php',
|
||||
'OC\\Core\\Command\\Background\\ListCommand' => $baseDir . '/core/Command/Background/ListCommand.php',
|
||||
'OC\\Core\\Command\\Background\\Mode' => $baseDir . '/core/Command/Background/Mode.php',
|
||||
'OC\\Core\\Command\\Background\\RunningJobs' => $baseDir . '/core/Command/Background/RunningJobs.php',
|
||||
|
|
|
|||
|
|
@ -1377,6 +1377,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Core\\Command\\Background\\Job' => __DIR__ . '/../../..' . '/core/Command/Background/Job.php',
|
||||
'OC\\Core\\Command\\Background\\JobBase' => __DIR__ . '/../../..' . '/core/Command/Background/JobBase.php',
|
||||
'OC\\Core\\Command\\Background\\JobWorker' => __DIR__ . '/../../..' . '/core/Command/Background/JobWorker.php',
|
||||
'OC\\Core\\Command\\Background\\JobsHistory' => __DIR__ . '/../../..' . '/core/Command/Background/JobsHistory.php',
|
||||
'OC\\Core\\Command\\Background\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/Background/ListCommand.php',
|
||||
'OC\\Core\\Command\\Background\\Mode' => __DIR__ . '/../../..' . '/core/Command/Background/Mode.php',
|
||||
'OC\\Core\\Command\\Background\\RunningJobs' => __DIR__ . '/../../..' . '/core/Command/Background/RunningJobs.php',
|
||||
|
|
|
|||
|
|
@ -8,13 +8,17 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OC\BackgroundJob;
|
||||
|
||||
use Exception;
|
||||
use OCP\BackgroundJob\IJobRuns;
|
||||
use OCP\BackgroundJob\JobRun;
|
||||
use OCP\BackgroundJob\JobStatus;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Snowflake\ISnowflakeDecoder;
|
||||
use OCP\Snowflake\ISnowflakeGenerator;
|
||||
use Override;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
final readonly class JobRuns implements IJobRuns {
|
||||
private const TABLE = 'job_runs';
|
||||
|
|
@ -23,7 +27,8 @@ final readonly class JobRuns implements IJobRuns {
|
|||
private IDBConnection $connection,
|
||||
private ISnowflakeGenerator $snowflakeGenerator,
|
||||
private ISnowflakeDecoder $snowflakeDecoder,
|
||||
private JobClassesRegistry $classesRegistry,
|
||||
private JobClassesRegistry $jobClassesRegistry,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -67,15 +72,57 @@ final readonly class JobRuns implements IJobRuns {
|
|||
->executeQuery();
|
||||
|
||||
foreach ($result->iterateAssociative() as $row) {
|
||||
$snowflakeInfo = $this->snowflakeDecoder->decode((string)$row['run_id']);
|
||||
yield new JobRun(
|
||||
$row['run_id'],
|
||||
$this->classesRegistry->getName($row['class_id']),
|
||||
$snowflakeInfo->getServerId(),
|
||||
(int)$row['pid'],
|
||||
$snowflakeInfo->getCreatedAt(),
|
||||
JobStatus::from((int)$row['status']),
|
||||
);
|
||||
yield $this->rowToJobRun($row);
|
||||
}
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function completedJobs(array $statuses = [], array $classes = [], int $limit = 200): \Generator {
|
||||
if ($statuses === []) {
|
||||
// By default, list only completed jobs
|
||||
$statuses = [JobStatus::SUCCEEDED, JobStatus::FAILED, JobStatus::CRASHED];
|
||||
}
|
||||
$dbStatuses = array_map(static fn (JobStatus $status) => $status->value, $statuses);
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb
|
||||
->select('run_id', 'class_id', 'pid', 'status', 'duration', 'ram_peak_usage')
|
||||
->from(self::TABLE)
|
||||
->where($qb->expr()->in('status', $qb->createNamedParameter($dbStatuses, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->setMaxResults($limit)
|
||||
->orderBy('run_id', 'DESC');
|
||||
|
||||
if ($classes !== []) {
|
||||
$classIds = [];
|
||||
foreach ($classes as $class) {
|
||||
try {
|
||||
$classIds[] = $this->jobClassesRegistry->getId($class);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->warning('Fail to resolve background job class {class}', ['class' => $class, 'exception' => $e]);
|
||||
}
|
||||
}
|
||||
if ($classIds === []) {
|
||||
throw new RuntimeException('No class ID found for filtering');
|
||||
}
|
||||
$qb->andWhere($qb->expr()->in('class_id', $qb->createNamedParameter($classIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
}
|
||||
|
||||
foreach ($qb->executeQuery()->iterateAssociative() as $row) {
|
||||
yield $this->rowToJobRun($row);
|
||||
}
|
||||
}
|
||||
|
||||
private function rowToJobRun(array $dbRow): JobRun {
|
||||
$snowflakeInfo = $this->snowflakeDecoder->decode((string)$dbRow['run_id']);
|
||||
return new JobRun(
|
||||
$dbRow['run_id'],
|
||||
$this->jobClassesRegistry->getName($dbRow['class_id']),
|
||||
$snowflakeInfo->getServerId(),
|
||||
(int)$dbRow['pid'],
|
||||
$snowflakeInfo->getCreatedAt(),
|
||||
JobStatus::from((int)$dbRow['status']),
|
||||
isset($dbRow['duration']) ? (int)$dbRow['duration'] : null,
|
||||
isset($dbRow['ram_peak_usage']) ? (int)$dbRow['ram_peak_usage'] : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,13 @@ interface IJobRuns {
|
|||
* @since 34.0.0
|
||||
*/
|
||||
public function runningJobs(int $limit = 200): \Generator;
|
||||
|
||||
/**
|
||||
* List of completed jobs
|
||||
*
|
||||
* @param list<JobStatus> $statuses
|
||||
* @param list<class-string<IJob>> $classes
|
||||
* @since 34.0.0
|
||||
*/
|
||||
public function completedJobs(array $statuses = [], array $classes = [], int $limit = 200): \Generator;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use OCP\Server;
|
|||
use OCP\Snowflake\ISnowflakeDecoder;
|
||||
use OCP\Snowflake\ISnowflakeGenerator;
|
||||
use Override;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -40,6 +41,7 @@ class JobRunsTest extends TestCase {
|
|||
Server::get(ISnowflakeGenerator::class),
|
||||
Server::get(ISnowflakeDecoder::class),
|
||||
$this->registry,
|
||||
$this->createMock(LoggerInterface::class),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -87,4 +89,23 @@ class JobRunsTest extends TestCase {
|
|||
}
|
||||
$this->assertGreaterThan(0, $runningJobs);
|
||||
}
|
||||
|
||||
public function testCompletedJobs(): void {
|
||||
$myPid = 1337;
|
||||
$myClass = DummyJob::class;
|
||||
|
||||
$runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid);
|
||||
$this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::FAILED);
|
||||
$runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid);
|
||||
$this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::SUCCEEDED);
|
||||
$runId = $this->runs->started($this->registry->getId(DummyJob::class), $myPid);
|
||||
$this->runs->finished($runId, 12345, 67890 * 1024, JobStatus::CRASHED);
|
||||
|
||||
$completedJobs = 0;
|
||||
foreach ($this->runs->runningJobs() as $job) {
|
||||
$this->assertInstanceOf(JobRun::class, $job);
|
||||
++$completedJobs;
|
||||
}
|
||||
$this->assertGreaterThan(0, $completedJobs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue