feat(utils): add getter for serverid with proper default

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
This commit is contained in:
Benjamin Gaussorgues 2026-06-03 18:26:46 +02:00
parent deefec1a59
commit 60ce92a697
No known key found for this signature in database
12 changed files with 205 additions and 41 deletions

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\BackgroundJobs;
use DateTimeImmutable;
use OC\BackgroundJob\JobRuns;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\JobStatus;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
use OCP\IServerInfo;
use Override;
class CleanupBackgroundJobsJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly JobRuns $jobRuns,
private readonly IServerInfo $serverInfo,
private readonly LoggerInterface $logger,
) {
parent::__construct($time);
$this->setInterval(60 * 60);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
#[Override]
protected function run($argument): void {
$this->reapCrashedJobs();
// TODO Clean oldest jobs
}
private function reapCrashedJobs(): void {
$currentServerId = $this->serverInfo->getServerId();
foreach ($this->jobRuns->runningJobs(1000) as $job) {
if ($job->serverId !== $currentServerId) {
continue;
}
exec('ps -p ' . escapeshellarg((string)$job->pid) . ' -o cmd', $output, $result);
if (count($output) === 1 && $output[0] === 'CMD' && $result === 1) {
// Process doesn't exists anymore
$maxDuration = (new DateTimeImmutable())->diff($job->startedAt);
$maxDuration =
($maxDuration->format('%a') * 24 * 60 * 60 * 1000)
+ ($maxDuration->format('%h') * 60 * 60 * 1000)
+ ($maxDuration->format('%m') * 60 * 1000)
+ ($maxDuration->format('%s') * 1000)
+ (int)($maxDuration->format('%f') / 1000);
$this->jobRuns->finished($job->runId, $maxDuration, 0, JobStatus::CRASHED);
}
}
}
}

View file

@ -12,7 +12,7 @@ namespace OC\Core\Command\Background;
use OC\BackgroundJob\JobRuns;
use OC\Core\Command\Base;
use OCP\BackgroundJob\JobStatus;
use OCP\IConfig;
use OCP\IServerInfo;
use OCP\Util;
use Override;
use Symfony\Component\Console\Input\InputInterface;
@ -23,7 +23,7 @@ use ValueError;
final class JobsHistory extends Base {
public function __construct(
private readonly JobRuns $jobRuns,
private IConfig $config,
private readonly IServerInfo $serverInfo,
) {
parent::__construct();
}
@ -75,7 +75,7 @@ final class JobsHistory extends Base {
private function formatLine(iterable $jobs): \Generator {
$jobsInfo = [];
$now = time();
$currentServerId = $this->config->getSystemValueInt('serverid', -1);
$currentServerId = $this->serverInfo->getServerId();
foreach ($jobs as $job) {
$status = match ($job->status) {
JobStatus::RUNNING => 'Running',

View file

@ -11,7 +11,7 @@ namespace OC\Core\Command\Background;
use OC\BackgroundJob\JobRuns;
use OC\Core\Command\Base;
use OCP\IConfig;
use OCP\IServerInfo;
use Override;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -20,7 +20,7 @@ use Symfony\Component\Console\Output\OutputInterface;
final class RunningJobs extends Base {
public function __construct(
private readonly JobRuns $jobRuns,
private IConfig $config,
private readonly IServerInfo $serverInfo,
) {
parent::__construct();
}
@ -60,7 +60,7 @@ final class RunningJobs extends Base {
private function formatLine(iterable $jobs): \Generator {
$now = time();
$currentServerId = $this->config->getSystemValueInt('serverid', -1);
$currentServerId = $this->serverInfo->getServerId();
foreach ($jobs as $job) {
yield [
'Run ID' => $job->runId,

View file

@ -651,6 +651,7 @@ return array(
'OCP\\IRequest' => $baseDir . '/lib/public/IRequest.php',
'OCP\\IRequestId' => $baseDir . '/lib/public/IRequestId.php',
'OCP\\IServerContainer' => $baseDir . '/lib/public/IServerContainer.php',
'OCP\\IServerInfo' => $baseDir . '/lib/public/IServerInfo.php',
'OCP\\ISession' => $baseDir . '/lib/public/ISession.php',
'OCP\\IStreamImage' => $baseDir . '/lib/public/IStreamImage.php',
'OCP\\ITagManager' => $baseDir . '/lib/public/ITagManager.php',
@ -2173,6 +2174,7 @@ return array(
'OC\\Security\\VerificationToken\\VerificationToken' => $baseDir . '/lib/private/Security/VerificationToken/VerificationToken.php',
'OC\\Server' => $baseDir . '/lib/private/Server.php',
'OC\\ServerContainer' => $baseDir . '/lib/private/ServerContainer.php',
'OC\\ServerInfo' => $baseDir . '/lib/private/ServerInfo.php',
'OC\\ServerNotAvailableException' => $baseDir . '/lib/private/ServerNotAvailableException.php',
'OC\\ServiceUnavailableException' => $baseDir . '/lib/private/ServiceUnavailableException.php',
'OC\\Session\\CryptoSessionData' => $baseDir . '/lib/private/Session/CryptoSessionData.php',

View file

@ -692,6 +692,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\IRequest' => __DIR__ . '/../../..' . '/lib/public/IRequest.php',
'OCP\\IRequestId' => __DIR__ . '/../../..' . '/lib/public/IRequestId.php',
'OCP\\IServerContainer' => __DIR__ . '/../../..' . '/lib/public/IServerContainer.php',
'OCP\\IServerInfo' => __DIR__ . '/../../..' . '/lib/public/IServerInfo.php',
'OCP\\ISession' => __DIR__ . '/../../..' . '/lib/public/ISession.php',
'OCP\\IStreamImage' => __DIR__ . '/../../..' . '/lib/public/IStreamImage.php',
'OCP\\ITagManager' => __DIR__ . '/../../..' . '/lib/public/ITagManager.php',
@ -2214,6 +2215,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\VerificationToken\\VerificationToken' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/VerificationToken.php',
'OC\\Server' => __DIR__ . '/../../..' . '/lib/private/Server.php',
'OC\\ServerContainer' => __DIR__ . '/../../..' . '/lib/private/ServerContainer.php',
'OC\\ServerInfo' => __DIR__ . '/../../..' . '/lib/private/ServerInfo.php',
'OC\\ServerNotAvailableException' => __DIR__ . '/../../..' . '/lib/private/ServerNotAvailableException.php',
'OC\\ServiceUnavailableException' => __DIR__ . '/../../..' . '/lib/private/ServiceUnavailableException.php',
'OC\\Session\\CryptoSessionData' => __DIR__ . '/../../..' . '/lib/private/Session/CryptoSessionData.php',

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Repair;
use OC\Core\BackgroundJobs\CleanupBackgroundJobsJob;
use OCP\BackgroundJob\IJobList;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use Override;
class AddCleanupBackgroundJobsJob implements IRepairStep {
public function __construct(
private readonly IJobList $jobList,
) {
}
#[\Override]
public function getName(): string {
return 'Cleanup completed background jobs';
}
#[Override]
public function run(IOutput $output): void {
$this->jobList->add(CleanupBackgroundJobsJob::class);
}
}

View file

@ -227,6 +227,7 @@ use OCP\IPreview;
use OCP\IRequest;
use OCP\IRequestId;
use OCP\IServerContainer;
use OCP\IServerInfo;
use OCP\ISession;
use OCP\ITagManager;
use OCP\ITempManager;
@ -1146,6 +1147,9 @@ class Server extends ServerContainer implements IServerContainer {
return $c->get(FileSequence::class);
}, false);
$this->registerAlias(ISnowflakeDecoder::class, SnowflakeDecoder::class);
$this->registerAlias(IJobRuns::class, JobRuns::class);
$this->registerAlias(IServerInfo::class, ServerInfo::class);
$this->connectDispatcher();
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC;
use OCP\IConfig;
use OCP\IServerInfo;
use Override;
readonly class ServerInfo implements IServerInfo {
public function __construct(
private IConfig $config,
) {
}
#[Override]
public function getServerId(): int {
$serverid = $this->config->getSystemValueInt('serverid', -1);
if ($serverid < 1) {
// Fallback: generates a server ID based on hostname
/** @var int<0,max> */
$serverid = PHP_INT_SIZE === 4
? hexdec(hash('xxh32', $this->getHostname()))
// Makes sure it doesn't overflow 32 bits int
: hexdec(substr(hash('xxh32', $this->getHostname()), -3));
}
/** @var int<0,511> */
return $serverid & 0x1FF;
}
#[Override]
public function getHostname(): string {
$hostname = gethostname();
if ($hostname === false) {
// Use a random hostname
return bin2hex(random_bytes(8));
}
return $hostname;
}
}

View file

@ -10,7 +10,7 @@ declare(strict_types=1);
namespace OC\Snowflake;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IServerInfo;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
@ -24,8 +24,8 @@ use Override;
final readonly class SnowflakeGenerator implements ISnowflakeGenerator {
public function __construct(
private ITimeFactory $timeFactory,
private IConfig $config,
private ISequence $sequenceGenerator,
private IServerInfo $serverInfo,
) {
}
@ -34,7 +34,7 @@ final readonly class SnowflakeGenerator implements ISnowflakeGenerator {
// Relative time
[$seconds, $milliseconds] = $this->getCurrentTime();
$serverId = $this->getServerId(); // Already 9 bits
$serverId = $this->serverInfo->getServerId();
$isCli = (int)$this->isCli(); // 1 bit
$sequenceId = $this->sequenceGenerator->nextId($seconds, $milliseconds, $serverId); // 12 bits
if ($sequenceId > 0xFFF || $sequenceId === false) {
@ -102,24 +102,6 @@ final readonly class SnowflakeGenerator implements ISnowflakeGenerator {
];
}
/**
* Return configured serverid or generate one if not set
*
* @return int<0,511>
*/
private function getServerId(): int {
$serverid = $this->config->getSystemValueInt('serverid', -1);
if ($serverid < 1) {
// Fallback: generates a server ID based on hostname
// or random bytes if hostname isn't available
/** @var int<0,max> */
$serverid = hexdec(hash('xxh32', gethostname() ?: random_bytes(8)));
}
/** @var int<0,511> */
return $serverid & 0x1FF;
}
private function isCli(): bool {
return PHP_SAPI === 'cli';
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP;
use OCP\AppFramework\Attribute\Consumable;
/**
* @since 34.0.1
*/
#[Consumable(since: '34.0.1')]
interface IServerInfo {
/**
* Returns configured Server ID or use default fallback
*
* @return int<0,511>
* @since 34.0.1
*/
public function getServerId(): int;
/**
* Returns current server hostname
*
* @since 34.0.1
*/
public function getHostname(): string;
}

View file

@ -246,9 +246,7 @@ class Util {
*/
public static function linkToAbsolute($app, $file, $args = []) {
$urlGenerator = Server::get(IURLGenerator::class);
return $urlGenerator->getAbsoluteURL(
$urlGenerator->linkTo($app, $file, $args)
);
return $urlGenerator->getAbsoluteURL($urlGenerator->linkTo($app, $file, $args));
}
/**
@ -483,7 +481,7 @@ class Util {
* @since 4.5.0
*/
public static function mb_array_change_key_case($input, $case = MB_CASE_LOWER, $encoding = 'UTF-8') {
$case = ($case !== MB_CASE_UPPER) ? MB_CASE_LOWER : MB_CASE_UPPER;
$case = $case !== MB_CASE_UPPER ? MB_CASE_LOWER : MB_CASE_UPPER;
$ret = [];
foreach ($input as $k => $v) {
$ret[mb_convert_case($k, $case, $encoding)] = $v;
@ -518,7 +516,7 @@ class Util {
$freeSpace = max($freeSpace, 0);
return $freeSpace;
} else {
return (INF > 0)? INF: PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188
return INF > 0 ? INF : PHP_INT_MAX; // work around https://bugs.php.net/bug.php?id=69188
}
}

View file

@ -14,7 +14,8 @@ use OC\Snowflake\ISequence;
use OC\Snowflake\SnowflakeDecoder;
use OC\Snowflake\SnowflakeGenerator;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IServerInfo;
use OCP\Server;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
@ -25,8 +26,8 @@ use Test\TestCase;
*/
class GeneratorTest extends TestCase {
private SnowflakeDecoder $decoder;
private IConfig&MockObject $config;
private ISequence&MockObject $sequence;
private IServerInfo $serverInfo;
#[\Override]
public function setUp(): void {
@ -34,16 +35,15 @@ class GeneratorTest extends TestCase {
$this->decoder = new SnowflakeDecoder();
$this->config = $this->createMock(IConfig::class);
$this->config->method('getSystemValueInt')->with('serverid')->willReturn(42);
$this->sequence = $this->createMock(ISequence::class);
$this->sequence->method('isAvailable')->willReturn(true);
$this->sequence->method('nextId')->willReturn(421);
$this->serverInfo = Server::get(IServerInfo::class);
}
public function testGenerator(): void {
$generator = new SnowflakeGenerator(new TimeFactory(), $this->config, $this->sequence);
$generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo);
$snowflakeId = $generator->nextId();
$data = $this->decoder->decode($generator->nextId());
@ -63,7 +63,7 @@ class GeneratorTest extends TestCase {
$this->assertTrue($data->isCli());
// Check serverId
$this->assertEquals(42, $data->getServerId());
$this->assertEquals($this->serverInfo->getServerId(), $data->getServerId());
}
#[DataProvider('provideSnowflakeData')]
@ -72,12 +72,12 @@ class GeneratorTest extends TestCase {
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->method('now')->willReturn($dt);
$generator = new SnowflakeGenerator($timeFactory, $this->config, $this->sequence);
$generator = new SnowflakeGenerator($timeFactory, $this->sequence, $this->serverInfo);
$data = $this->decoder->decode($generator->nextId());
$this->assertEquals($expectedSeconds, $data->getCreatedAt()->format('U') - ISnowflakeGenerator::TS_OFFSET);
$this->assertEquals($expectedMilliseconds, (int)$data->getCreatedAt()->format('v'));
$this->assertEquals(42, $data->getServerId());
$this->assertEquals($this->serverInfo->getServerId(), $data->getServerId());
}
public static function provideSnowflakeData(): array {