diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index f856bd7aa8c42..e8557c195a87b 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -136,6 +136,7 @@ 'OCA\\Settings\\SetupChecks\\SystemIs64bit' => $baseDir . '/../lib/SetupChecks/SystemIs64bit.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => $baseDir . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingSuccessRate' => $baseDir . '/../lib/SetupChecks/TaskProcessingSuccessRate.php', + 'OCA\\Settings\\SetupChecks\\TaskProcessingWorkerIsRunning' => $baseDir . '/../lib/SetupChecks/TaskProcessingWorkerIsRunning.php', 'OCA\\Settings\\SetupChecks\\TempSpaceAvailable' => $baseDir . '/../lib/SetupChecks/TempSpaceAvailable.php', 'OCA\\Settings\\SetupChecks\\TransactionIsolation' => $baseDir . '/../lib/SetupChecks/TransactionIsolation.php', 'OCA\\Settings\\SetupChecks\\TwoFactorConfiguration' => $baseDir . '/../lib/SetupChecks/TwoFactorConfiguration.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index d1c8d9b9eae77..027328f3f3672 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -151,6 +151,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\SetupChecks\\SystemIs64bit' => __DIR__ . '/..' . '/../lib/SetupChecks/SystemIs64bit.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => __DIR__ . '/..' . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingSuccessRate' => __DIR__ . '/..' . '/../lib/SetupChecks/TaskProcessingSuccessRate.php', + 'OCA\\Settings\\SetupChecks\\TaskProcessingWorkerIsRunning' => __DIR__ . '/..' . '/../lib/SetupChecks/TaskProcessingWorkerIsRunning.php', 'OCA\\Settings\\SetupChecks\\TempSpaceAvailable' => __DIR__ . '/..' . '/../lib/SetupChecks/TempSpaceAvailable.php', 'OCA\\Settings\\SetupChecks\\TransactionIsolation' => __DIR__ . '/..' . '/../lib/SetupChecks/TransactionIsolation.php', 'OCA\\Settings\\SetupChecks\\TwoFactorConfiguration' => __DIR__ . '/..' . '/../lib/SetupChecks/TwoFactorConfiguration.php', diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index 7f837bfd0ae1a..41a50cb870fbf 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -72,6 +72,8 @@ use OCA\Settings\SetupChecks\SupportedDatabase; use OCA\Settings\SetupChecks\SystemIs64bit; use OCA\Settings\SetupChecks\TaskProcessingPickupSpeed; +use OCA\Settings\SetupChecks\TaskProcessingSuccessRate; +use OCA\Settings\SetupChecks\TaskProcessingWorkerIsRunning; use OCA\Settings\SetupChecks\TempSpaceAvailable; use OCA\Settings\SetupChecks\TransactionIsolation; use OCA\Settings\SetupChecks\TwoFactorConfiguration; @@ -212,6 +214,8 @@ public function register(IRegistrationContext $context): void { $context->registerSetupCheck(SupportedDatabase::class); $context->registerSetupCheck(SystemIs64bit::class); $context->registerSetupCheck(TaskProcessingPickupSpeed::class); + $context->registerSetupCheck(TaskProcessingSuccessRate::class); + $context->registerSetupCheck(TaskProcessingWorkerIsRunning::class); $context->registerSetupCheck(TempSpaceAvailable::class); $context->registerSetupCheck(TransactionIsolation::class); $context->registerSetupCheck(TwoFactorConfiguration::class); diff --git a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php index 4897d0cf2de0f..4b7c3a39f9d25 100644 --- a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php +++ b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php @@ -16,7 +16,7 @@ use OCP\TaskProcessing\IManager; class TaskProcessingPickupSpeed implements ISetupCheck { - public const MAX_SLOW_PERCENTAGE = 0.2; + public const MAX_SLOW_PERCENTAGE = 0.1; public const MAX_DAYS = 14; diff --git a/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php b/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php index 4fc801ad8b7ef..cde34bcf543e4 100644 --- a/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php +++ b/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php @@ -17,7 +17,7 @@ use OCP\TaskProcessing\Task; class TaskProcessingSuccessRate implements ISetupCheck { - public const MAX_FAILURE_PERCENTAGE = 0.2; + public const MAX_FAILURE_PERCENTAGE = 0.1; public const MAX_DAYS = 14; @@ -33,7 +33,7 @@ public function getCategory(): string { } public function getName(): string { - return $this->l10n->t('Task Processing pickup speed'); + return $this->l10n->t('Task Processing success rate'); } public function run(): SetupResult { diff --git a/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php b/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php new file mode 100644 index 0000000000000..e480d6765c955 --- /dev/null +++ b/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php @@ -0,0 +1,74 @@ +l10n->t('Task Processing worker status'); + } + + #[\Override] + public function run(): SetupResult { + $lastNDays = self::HAS_TASKS_IN_LAST_X_DAYS; + $tasks = $this->taskProcessingManager->getTasks(userId: '', scheduleAfter: $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays)); + $taskCount = count($tasks); + if ($taskCount === 0) { + // In case taskprocessing is not used at all + return SetupResult::success( + $this->l10n->n( + 'No scheduled tasks in the last day.', + 'No scheduled tasks in the last %n days.', + $lastNDays + ) + ); + } + $lastIteration = (int)$this->appConfig->getValueString('core', 'ai.taskprocessing_worker_last_iteration', lazy: true); + if ($lastIteration > $this->timeFactory->now()->getTimestamp() - (60 * self::IS_RUNNING_IN_LAST_X_MINUTES)) { + return SetupResult::success( + $this->l10n->n('The Task Processing worker has run in the last minute.', 'The Task Processing worker has run in the last %n minutes.', self::IS_RUNNING_IN_LAST_X_MINUTES) + ); + } + + if ($lastIteration > 0) { + return SetupResult::warning( + $this->l10n->t('The Task Processing worker does not seem to be running. The last run was at %s.', [date('Y-m-d H:i:s', $lastIteration)]) + ); + } + + return SetupResult::warning( + $this->l10n->t('The Task Processing worker does not seem to be running. It seems it has never run so far.') + ); + } +} diff --git a/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php b/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php index 6375d9f6e7fa2..0bb3b0d05878b 100644 --- a/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php +++ b/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php @@ -42,8 +42,8 @@ public function testPass(): void { for ($i = 0; $i < 100; $i++) { $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); $task->setStartedAt(0); - if ($i < 15) { - $task->setScheduledAt(60 * 5); // 15% get 5mins + if ($i < 5) { + $task->setScheduledAt(60 * 5); // 5% get 5mins } else { $task->setScheduledAt(60); // the rest gets 1min } diff --git a/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php b/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php index 2c8b850ff6ae8..56733a9a95810 100644 --- a/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php +++ b/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php @@ -43,8 +43,8 @@ public function testPass(): void { $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); $task->setStartedAt(0); $task->setEndedAt(1); - if ($i < 15) { - $task->setStatus(Task::STATUS_FAILED); // 15% get status FAILED + if ($i < 5) { + $task->setStatus(Task::STATUS_FAILED); // 5% get status FAILED } else { $task->setStatus(Task::STATUS_SUCCESSFUL); } diff --git a/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php b/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php new file mode 100644 index 0000000000000..43df9e599b686 --- /dev/null +++ b/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php @@ -0,0 +1,78 @@ +l10n = $this->getMockBuilder(IL10N::class)->getMock(); + $this->timeFactory = $this->getMockBuilder(ITimeFactory::class)->getMock(); + $this->taskProcessingManager = $this->getMockBuilder(IManager::class)->getMock(); + $this->appConfig = $this->getMockBuilder(IAppConfig::class)->getMock(); + + $this->check = new TaskProcessingWorkerIsRunning( + $this->l10n, + $this->taskProcessingManager, + $this->timeFactory, + $this->appConfig + ); + } + + public function testPass(): void { + $tasks = []; + for ($i = 0; $i < 10; $i++) { + $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); + $task->setStartedAt($this->timeFactory->now()->getTimestamp()); + $task->setScheduledAt($this->timeFactory->now()->getTimestamp()); + $task->setEndedAt($this->timeFactory->now()->getTimestamp()); + $task->setStatus(Task::STATUS_SUCCESSFUL); + $tasks[] = $task; + } + $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); + $this->appConfig->method('getValueString')->willReturn((string)$this->timeFactory->now()->getTimestamp()); + + $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); + } + + public function testFail(): void { + $tasks = []; + for ($i = 0; $i < 10; $i++) { + $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); + $task->setStartedAt($this->timeFactory->now()->getTimestamp()); + $task->setScheduledAt($this->timeFactory->now()->getTimestamp()); + $task->setEndedAt($this->timeFactory->now()->getTimestamp()); + $task->setStatus(Task::STATUS_SUCCESSFUL); + $tasks[] = $task; + } + $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); + $this->appConfig->method('getValueString')->willReturn((string)($this->timeFactory->now()->getTimestamp() - 60 * 10)); + + $this->assertEquals(SetupResult::WARNING, $this->check->run()->getSeverity()); + } +} diff --git a/core/Command/TaskProcessing/WorkerCommand.php b/core/Command/TaskProcessing/WorkerCommand.php index 09f3f3573d23c..b8df7aea5c25e 100644 --- a/core/Command/TaskProcessing/WorkerCommand.php +++ b/core/Command/TaskProcessing/WorkerCommand.php @@ -10,6 +10,8 @@ use OC\Core\Command\Base; use OC\Core\Command\InterruptedException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IAppConfig; use OCP\TaskProcessing\Exception\Exception; use OCP\TaskProcessing\Exception\NotFoundException; use OCP\TaskProcessing\IManager; @@ -23,6 +25,8 @@ class WorkerCommand extends Base { public function __construct( private readonly IManager $taskProcessingManager, private readonly LoggerInterface $logger, + private readonly IAppConfig $appConfig, + private readonly ITimeFactory $timeFactory, ) { parent::__construct(); } @@ -61,12 +65,13 @@ protected function configure(): void { } protected function execute(InputInterface $input, OutputInterface $output): int { - $startTime = time(); + $startTime = $this->timeFactory->now()->getTimestamp(); $timeout = (int)$input->getOption('timeout'); $interval = (int)$input->getOption('interval'); $once = $input->getOption('once') === true; /** @var list $taskTypes */ $taskTypes = $input->getOption('taskTypes'); + $lastConfigStorageTime = 0; if ($timeout > 0) { $output->writeln('Task processing worker will stop after ' . $timeout . ' seconds'); @@ -74,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int while (true) { // Stop if timeout exceeded - if ($timeout > 0 && ($startTime + $timeout) < time()) { + if ($timeout > 0 && ($startTime + $timeout) < $this->timeFactory->now()->getTimestamp()) { $output->writeln('Timeout reached, exiting...', OutputInterface::VERBOSITY_VERBOSE); break; } @@ -82,11 +87,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Handle SIGTERM/SIGINT gracefully try { $this->abortIfInterrupted(); - } catch (InterruptedException $e) { + } catch (InterruptedException) { $output->writeln('Task processing worker stopped'); break; } + if ($lastConfigStorageTime < $this->timeFactory->now()->getTimestamp() - 60) { + $this->appConfig->setValueString('core', 'ai.taskprocessing_worker_last_iteration', (string)$this->timeFactory->now()->getTimestamp(), lazy: true); + $lastConfigStorageTime = $this->timeFactory->now()->getTimestamp(); + } $processedTask = $this->processNextTask($output, $taskTypes); if ($once) { diff --git a/tests/Core/Command/TaskProcessing/WorkerCommandTest.php b/tests/Core/Command/TaskProcessing/WorkerCommandTest.php index 3f472548cef49..a83418338d12c 100644 --- a/tests/Core/Command/TaskProcessing/WorkerCommandTest.php +++ b/tests/Core/Command/TaskProcessing/WorkerCommandTest.php @@ -10,6 +10,8 @@ namespace Tests\Core\Command\TaskProcessing; use OC\Core\Command\TaskProcessing\WorkerCommand; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IAppConfig; use OCP\TaskProcessing\Exception\Exception; use OCP\TaskProcessing\Exception\NotFoundException; use OCP\TaskProcessing\IManager; @@ -24,6 +26,8 @@ class WorkerCommandTest extends TestCase { private IManager&MockObject $manager; private LoggerInterface&MockObject $logger; + private IAppConfig&MockObject $appConfig; + private ITimeFactory&MockObject $timeFactory; private WorkerCommand $command; protected function setUp(): void { @@ -31,7 +35,10 @@ protected function setUp(): void { $this->manager = $this->createMock(IManager::class); $this->logger = $this->createMock(LoggerInterface::class); - $this->command = new WorkerCommand($this->manager, $this->logger); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->method('now')->willReturnCallback(fn () => new \DateTimeImmutable()); + $this->command = new WorkerCommand($this->manager, $this->logger, $this->appConfig, $this->timeFactory); } /**