Merge pull request #60972 from nextcloud/feat/clean_crashed_jobs

Job run history cleanup
This commit is contained in:
Benjamin Gaussorgues 2026-06-12 15:49:36 +02:00 committed by GitHub
commit e1eea9bee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 309 additions and 43 deletions

View file

@ -3002,4 +3002,12 @@ $CONFIG = [
* Defaults to ``0``.
*/
'preview_expiration_days' => 0,
/**
* Delete job runs older than a certain number of days.
* Less than one day is not allowed.
*
* Defaults to ``60``.
*/
'background_jobs_expiration_days' => 60,
];

View file

@ -0,0 +1,86 @@
<?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;
use Psr\Log\LoggerInterface;
use RuntimeException;
class CleanupBackgroundJobsJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly JobRuns $jobRuns,
private readonly IServerInfo $serverInfo,
private readonly IConfig $config,
private readonly LoggerInterface $logger,
) {
parent::__construct($time);
$this->setInterval(60 * 60);
$this->setTimeSensitivity(IJob::TIME_SENSITIVE);
}
#[Override]
protected function run($argument): void {
$this->reapCrashedJobs();
$this->cleanOldestRuns();
}
private function reapCrashedJobs(): void {
$currentServerId = $this->serverInfo->getServerId();
foreach ($this->jobRuns->runningJobs(1000) as $job) {
if ($job->serverId !== $currentServerId) {
continue;
}
$output = [];
$result = 0;
exec('ps -p ' . escapeshellarg((string)$job->pid) . ' -o cmd', $output, $result);
if (count($output) === 1 && current($output) === 'CMD' && $result === 1) {
// Process doesn't exists anymore
$maxDuration = (new DateTimeImmutable())->diff($job->startedAt);
$maxDuration
= ($maxDuration->days * 24 * 60 * 60 * 1000)
+ ($maxDuration->h * 60 * 60 * 1000)
+ ($maxDuration->i * 60 * 1000)
+ ($maxDuration->s * 1000)
+ (int)($maxDuration->f * 1000);
$this->jobRuns->finished($job->runId, $maxDuration, 0, JobStatus::CRASHED);
$this->logger->warning('No process matching PID {pid} found on server {serverId}. Job {runId} was marked as crashed', [
'pid' => $job->pid,
'serverId' => $job->serverId,
'runId' => $job->runId,
]);
}
}
}
private function cleanOldestRuns(): void {
$daysToKeep = $this->config->getSystemValueInt('background_jobs_expiration_days', 60);
if ($daysToKeep < 1) {
throw new RuntimeException('Invalid number of days');
}
$cleanBeforeTimestamp = time() - ($daysToKeep * 24 * 3600);
$cleanedJobs = $this->jobRuns->deleteBefore($cleanBeforeTimestamp);
if ($cleanedJobs > 0) {
$this->logger->info(
'Cleanup of old background jobs. Number of jobs removed: ' . $cleanedJobs . 'Reason: older than ' . $daysToKeep . ' days.',
);
}
}
}

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',
@ -1311,6 +1312,7 @@ return array(
'OC\\Core\\AppInfo\\ConfigLexicon' => $baseDir . '/core/AppInfo/ConfigLexicon.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupBackgroundJobsJob' => $baseDir . '/core/BackgroundJobs/CleanupBackgroundJobsJob.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\ExpirePreviewsJob' => $baseDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => $baseDir . '/core/BackgroundJobs/GenerateMetadataJob.php',
@ -2051,6 +2053,7 @@ return array(
'OC\\Repair' => $baseDir . '/lib/private/Repair.php',
'OC\\RepairException' => $baseDir . '/lib/private/RepairException.php',
'OC\\Repair\\AddBruteForceCleanupJob' => $baseDir . '/lib/private/Repair/AddBruteForceCleanupJob.php',
'OC\\Repair\\AddCleanupBackgroundJobsJob' => $baseDir . '/lib/private/Repair/AddCleanupBackgroundJobsJob.php',
'OC\\Repair\\AddCleanupDeletedUsersBackgroundJob' => $baseDir . '/lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php',
'OC\\Repair\\AddCleanupUpdaterBackupsJob' => $baseDir . '/lib/private/Repair/AddCleanupUpdaterBackupsJob.php',
'OC\\Repair\\AddMetadataGenerationJob' => $baseDir . '/lib/private/Repair/AddMetadataGenerationJob.php',
@ -2173,6 +2176,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',
@ -1352,6 +1353,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\AppInfo\\ConfigLexicon' => __DIR__ . '/../../..' . '/core/AppInfo/ConfigLexicon.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupBackgroundJobsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupBackgroundJobsJob.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\ExpirePreviewsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/ExpirePreviewsJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/GenerateMetadataJob.php',
@ -2092,6 +2094,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Repair' => __DIR__ . '/../../..' . '/lib/private/Repair.php',
'OC\\RepairException' => __DIR__ . '/../../..' . '/lib/private/RepairException.php',
'OC\\Repair\\AddBruteForceCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddBruteForceCleanupJob.php',
'OC\\Repair\\AddCleanupBackgroundJobsJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddCleanupBackgroundJobsJob.php',
'OC\\Repair\\AddCleanupDeletedUsersBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php',
'OC\\Repair\\AddCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddCleanupUpdaterBackupsJob.php',
'OC\\Repair\\AddMetadataGenerationJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddMetadataGenerationJob.php',
@ -2214,6 +2217,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

@ -62,6 +62,18 @@ final readonly class JobRuns implements IJobRuns {
return $result === 1;
}
public function deleteBefore(int $timestamp): int {
$beforeSnowflake = $this->snowflakeGenerator->minForTimeId($timestamp);
$beforeSnowflake = '91480652934574081';
$qb = $this->connection->getQueryBuilder();
$result = $qb
->delete(self::TABLE)
->where($qb->expr()->lt('run_id', $qb->createNamedParameter($beforeSnowflake)))
->executeStatement();
return $result;
}
#[Override]
public function runningJobs(int $limit = 200): \Generator {
$qb = $this->connection->getQueryBuilder();

View file

@ -9,6 +9,7 @@
namespace OC;
use OC\Repair\AddBruteForceCleanupJob;
use OC\Repair\AddCleanupBackgroundJobsJob;
use OC\Repair\AddCleanupDeletedUsersBackgroundJob;
use OC\Repair\AddCleanupUpdaterBackupsJob;
use OC\Repair\AddMetadataGenerationJob;
@ -134,14 +135,14 @@ class Repair implements IOutput {
}
}
if (!($s instanceof IRepairStep)) {
if (!$s instanceof IRepairStep) {
throw new \Exception("Repair step '$repairStep' is not of type \\OCP\\Migration\\IRepairStep");
}
$repairStep = $s;
}
if (($repairStep instanceof IRepairStepExpensive) && !$includeExpensive) {
if ($repairStep instanceof IRepairStepExpensive && !$includeExpensive) {
$this->debug("Skipping expensive repair step '" . $repairStep::class . "'");
} else {
$this->repairSteps[] = $repairStep;
@ -195,6 +196,7 @@ class Repair implements IOutput {
Server::get(SanitizeAccountProperties::class),
Server::get(AddMovePreviewJob::class),
Server::get(ConfigKeyMigration::class),
Server::get(AddCleanupBackgroundJobsJob::class),
];
if ($includeExpensive) {

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

@ -16,6 +16,7 @@ use InvalidArgumentException;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Authentication\Token\PublicKeyTokenProvider;
use OC\Authentication\Token\TokenCleanupJob;
use OC\Core\BackgroundJobs\CleanupBackgroundJobsJob;
use OC\Core\BackgroundJobs\ExpirePreviewsJob;
use OC\Core\BackgroundJobs\GenerateMetadataJob;
use OC\Core\BackgroundJobs\PreviewMigrationJob;
@ -532,6 +533,7 @@ class Setup {
$jobList->add(GenerateMetadataJob::class);
$jobList->add(PreviewMigrationJob::class);
$jobList->add(ExpirePreviewsJob::class);
$jobList->add(CleanupBackgroundJobsJob::class);
}
/**

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) {
@ -43,6 +43,23 @@ final readonly class SnowflakeGenerator implements ISnowflakeGenerator {
return $this->nextId();
}
return $this->packSnowflakeId($seconds, $milliseconds, $serverId, $isCli, $sequenceId);
}
/**
* Return minimal snowflake ID for a given timestamp
*
* Not a real snowflake ID!
* Only use it for comparisons. For example get all snowflake IDs generated before $timestamp
*
* @since 34.0.1
*/
#[Override]
public function minForTimeId(int $timestamp): string {
return $this->packSnowflakeId($timestamp - self::TS_OFFSET, 0, 0, 0, 0);
}
private function packSnowflakeId($seconds, $milliseconds, $serverId, $isCli, $sequenceId): string {
if (PHP_INT_SIZE === 8) {
$firstHalf = $seconds & 0x7FFFFFFF;
$secondHalf = (($milliseconds & 0x3FF) << 22) | ($serverId << 13) | ($isCli << 12) | $sequenceId;
@ -102,24 +119,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

@ -42,4 +42,18 @@ interface ISnowflakeGenerator {
* @since 33.0
*/
public function nextId(): string;
/**
* Return the smallest possible Snowflake ID for a given timestamp
*
* Not a real snowflake ID!
* Only use it for comparisons. Examples:
* - find all Snowflake IDs generated from a given $timestamp
* Look for `>= minForTimeId($timestamp)`
* - delete all Snowflake IDs generated before a given $timestamp
* Delete where `id < minForTimeId($timestamp)`
*
* @since 34.0.1
*/
public function minForTimeId(int $timestamp): 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,26 @@ class GeneratorTest extends TestCase {
$this->assertTrue($data->isCli());
// Check serverId
$this->assertEquals(42, $data->getServerId());
$this->assertEquals($this->serverInfo->getServerId(), $data->getServerId());
}
public function testMinForTime(): void {
$generator = new SnowflakeGenerator(new TimeFactory(), $this->sequence, $this->serverInfo);
$now = time();
$snowflakeId = $generator->minForTimeId($now);
$data = $this->decoder->decode($snowflakeId);
$this->assertIsString($snowflakeId);
// Check timestamp
$this->assertEquals($now - ISnowflakeGenerator::TS_OFFSET, $data->getSeconds());
// Check all other fields are at zero
$this->assertEquals(0, $data->getMilliseconds());
$this->assertEquals(0, $data->getServerId());
$this->assertEquals(0, $data->getSequenceId());
$this->assertFalse($data->isCli());
$this->assertEquals(0, $data->getServerId());
}
#[DataProvider('provideSnowflakeData')]
@ -72,12 +91,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 {