From 2dd0c54888243595eaa4c7e1d301cfbc7fee424b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Sun, 20 Sep 2026 23:32:47 +0200 Subject: [PATCH] perf(setupchecks): Don't load all tasks to calculate stats Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Joas Schilling --- .../SetupChecks/TaskProcessingPickupSpeed.php | 26 +++------ .../SetupChecks/TaskProcessingSuccessRate.php | 21 ++----- .../TaskProcessingWorkerIsRunning.php | 3 +- .../TaskProcessingPickupSpeedTest.php | 56 ++++++++++--------- .../TaskProcessingSuccessRateTest.php | 53 +++++++++--------- .../TaskProcessingWorkerIsRunningTest.php | 42 +++++++------- core/Listener/AddMissingIndicesListener.php | 6 ++ .../Version30000Date20240708160048.php | 6 ++ lib/private/TaskProcessing/Db/TaskMapper.php | 39 ++++++++++--- lib/private/TaskProcessing/Manager.php | 6 +- lib/public/TaskProcessing/IManager.php | 11 +++- .../lib/TaskProcessing/TaskProcessingTest.php | 49 ++++++++++++++++ 12 files changed, 195 insertions(+), 123 deletions(-) diff --git a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php index 872962276a573..be7d5a2221871 100644 --- a/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php +++ b/apps/settings/lib/SetupChecks/TaskProcessingPickupSpeed.php @@ -19,6 +19,7 @@ class TaskProcessingPickupSpeed implements ISetupCheck { public const MAX_SLOW_PERCENTAGE = 0.1; public const MAX_DAYS = 14; + public const MAX_PICKUP_DELAY = 60 * 4; public function __construct( private IL10N $l10n, @@ -39,14 +40,12 @@ public function getName(): string { #[\Override] public function run(): SetupResult { - $taskCount = 0; $lastNDays = 1; - while ($taskCount === 0 && $lastNDays < self::MAX_DAYS) { + do { $lastNDays++; - // userId: '' means no filter, whereas null would mean guest - $tasks = $this->taskProcessingManager->getTasks(userId: '', scheduleAfter: $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays)); - $taskCount = count($tasks); - } + $scheduleAfter = $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays); + $taskCount = $this->taskProcessingManager->countTasks(scheduleAfter: $scheduleAfter); + } while ($taskCount === 0 && $lastNDays < self::MAX_DAYS); if ($taskCount === 0) { return SetupResult::success( $this->l10n->n( @@ -56,19 +55,8 @@ public function run(): SetupResult { ) ); } - $slowCount = 0; - foreach ($tasks as $task) { - if ($task->getStartedAt() === null) { - continue; // task was not picked up yet - } - if ($task->getScheduledAt() === null) { - continue; // task was not scheduled yet -- should not happen, but the API specifies null as return value - } - $pickupDelay = $task->getScheduledAt() - $task->getStartedAt(); - if ($pickupDelay > 60 * 4) { - $slowCount++; // task pickup took longer than 4 minutes - } - } + // Tasks that have not been picked up yet are not counted as slow + $slowCount = $this->taskProcessingManager->countTasks(scheduleAfter: $scheduleAfter, minPickupDelay: self::MAX_PICKUP_DELAY); if (($slowCount / $taskCount) < self::MAX_SLOW_PERCENTAGE) { return SetupResult::success( diff --git a/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php b/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php index 73b4ad6ff5f18..bd837177b448d 100644 --- a/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php +++ b/apps/settings/lib/SetupChecks/TaskProcessingSuccessRate.php @@ -40,14 +40,12 @@ public function getName(): string { #[\Override] public function run(): SetupResult { - $taskCount = 0; $lastNDays = 0; - while ($taskCount === 0 && $lastNDays < self::MAX_DAYS) { + do { $lastNDays++; - // userId: '' means no filter, whereas null would mean guest - $tasks = $this->taskProcessingManager->getTasks(userId: '', scheduleAfter: $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays)); - $taskCount = count($tasks); - } + $scheduleAfter = $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays); + $taskCount = $this->taskProcessingManager->countTasks(scheduleAfter: $scheduleAfter); + } while ($taskCount === 0 && $lastNDays < self::MAX_DAYS); if ($taskCount === 0) { return SetupResult::success( $this->l10n->n( @@ -57,16 +55,7 @@ public function run(): SetupResult { ) ); } - $failedCount = 0; - foreach ($tasks as $task) { - if ($task->getEndedAt() === null) { - continue; // task was not picked up yet - } - $status = $task->getStatus(); - if ($status === Task::STATUS_FAILED) { - $failedCount++; - } - } + $failedCount = $this->taskProcessingManager->countTasks(status: Task::STATUS_FAILED, scheduleAfter: $scheduleAfter); if (($failedCount / $taskCount) < self::MAX_FAILURE_PERCENTAGE) { return SetupResult::success( diff --git a/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php b/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php index 34d842c2e2f07..829bf96a2e78a 100644 --- a/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php +++ b/apps/settings/lib/SetupChecks/TaskProcessingWorkerIsRunning.php @@ -44,8 +44,7 @@ public function getName(): string { #[\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); + $taskCount = $this->taskProcessingManager->countTasks(scheduleAfter: $this->timeFactory->now()->getTimestamp() - (60 * 60 * 24 * $lastNDays)); if ($taskCount === 0) { // In case taskprocessing is not used at all return SetupResult::success( diff --git a/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php b/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php index 67fd0d81e1cee..a137a98e912c9 100644 --- a/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php +++ b/apps/settings/tests/SetupChecks/TaskProcessingPickupSpeedTest.php @@ -14,7 +14,6 @@ use OCP\IL10N; use OCP\SetupCheck\SetupResult; use OCP\TaskProcessing\IManager; -use OCP\TaskProcessing\Task; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -39,37 +38,44 @@ protected function setUp(): void { ); } + /** + * @param int $taskCount Tasks scheduled in the window + * @param int $slowCount Tasks of those that were picked up too late + */ + private function mockCounts(int $taskCount, int $slowCount): void { + $this->taskProcessingManager->method('countTasks') + ->willReturnCallback(function (?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null) use ($taskCount, $slowCount): int { + if ($minPickupDelay === null) { + return $taskCount; + } + $this->assertSame(TaskProcessingPickupSpeed::MAX_PICKUP_DELAY, $minPickupDelay); + return $slowCount; + }); + } + public function testPass(): void { - $tasks = []; - for ($i = 0; $i < 100; $i++) { - $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); - $task->setStartedAt(0); - if ($i < 5) { - $task->setScheduledAt(60 * 5); // 5% get 5mins - } else { - $task->setScheduledAt(60); // the rest gets 1min - } - $tasks[] = $task; - } - $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + // 5% of the tasks were picked up too late + $this->mockCounts(100, 5); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); } public function testFail(): void { - $tasks = []; - for ($i = 0; $i < 100; $i++) { - $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); - $task->setStartedAt(0); - if ($i < 30) { - $task->setScheduledAt(60 * 5); // 30% get 5mins - } else { - $task->setScheduledAt(60); // the rest gets 1min - } - $tasks[] = $task; - } - $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + // 30% of the tasks were picked up too late + $this->mockCounts(100, 30); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); $this->assertEquals(SetupResult::WARNING, $this->check->run()->getSeverity()); } + + public function testWidensTheWindowWhileThereAreNoTasks(): void { + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); + $this->taskProcessingManager->expects($this->never())->method('getTasks'); + $this->taskProcessingManager->expects($this->exactly(TaskProcessingPickupSpeed::MAX_DAYS - 1)) + ->method('countTasks') + ->willReturn(0); + + $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); + } } diff --git a/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php b/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php index 4b3b3e2421550..24ded7940ded6 100644 --- a/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php +++ b/apps/settings/tests/SetupChecks/TaskProcessingSuccessRateTest.php @@ -39,39 +39,40 @@ protected function setUp(): void { ); } + /** + * @param int $taskCount Tasks scheduled in the window + * @param int $failedCount Tasks of those that failed + */ + private function mockCounts(int $taskCount, int $failedCount): void { + $this->taskProcessingManager->method('countTasks') + ->willReturnCallback(function (?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null) use ($taskCount, $failedCount): int { + return $status === Task::STATUS_FAILED ? $failedCount : $taskCount; + }); + } + public function testPass(): void { - $tasks = []; - for ($i = 0; $i < 100; $i++) { - $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); - $task->setStartedAt(0); - $task->setEndedAt(1); - if ($i < 5) { - $task->setStatus(Task::STATUS_FAILED); // 5% get status FAILED - } else { - $task->setStatus(Task::STATUS_SUCCESSFUL); - } - $tasks[] = $task; - } - $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + // 5% of the tasks failed + $this->mockCounts(100, 5); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); } public function testFail(): void { - $tasks = []; - for ($i = 0; $i < 100; $i++) { - $task = new Task('test', ['test' => 'test'], 'settings', 'user' . $i); - $task->setStartedAt(0); - $task->setEndedAt(1); - if ($i < 30) { - $task->setStatus(Task::STATUS_FAILED); // 30% get status FAILED - } else { - $task->setStatus(Task::STATUS_SUCCESSFUL); - } - $tasks[] = $task; - } - $this->taskProcessingManager->method('getTasks')->willReturn($tasks); + // 30% of the tasks failed + $this->mockCounts(100, 30); + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); $this->assertEquals(SetupResult::WARNING, $this->check->run()->getSeverity()); } + + public function testWidensTheWindowWhileThereAreNoTasks(): void { + $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); + $this->taskProcessingManager->expects($this->never())->method('getTasks'); + $this->taskProcessingManager->expects($this->exactly(TaskProcessingSuccessRate::MAX_DAYS)) + ->method('countTasks') + ->willReturn(0); + + $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); + } } diff --git a/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php b/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php index 430cd3316bf6a..107decd86d20f 100644 --- a/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php +++ b/apps/settings/tests/SetupChecks/TaskProcessingWorkerIsRunningTest.php @@ -16,7 +16,6 @@ use OCP\IURLGenerator; use OCP\SetupCheck\SetupResult; use OCP\TaskProcessing\IManager; -use OCP\TaskProcessing\Task; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -48,16 +47,7 @@ protected function setUp(): void { } 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->taskProcessingManager->method('countTasks')->willReturn(10); $this->timeFactory->method('now')->willReturn(new \DateTimeImmutable()); $this->appConfig->method('getValueString')->willReturn((string)$this->timeFactory->now()->getTimestamp()); @@ -65,19 +55,29 @@ public function testPass(): void { } 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->taskProcessingManager->method('countTasks')->willReturn(10); $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()); } + + public function testTasksAreOnlyCounted(): void { + $now = new \DateTimeImmutable(); + $this->timeFactory->method('now')->willReturn($now); + // The tasks themselves are never needed, only whether there are any + $this->taskProcessingManager->expects($this->never())->method('getTasks'); + $this->taskProcessingManager->expects($this->once()) + ->method('countTasks') + ->willReturnCallback(function (?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null) use ($now): int { + $this->assertNull($status); + $this->assertSame([], $taskTypeIds); + $this->assertSame($now->getTimestamp() - 60 * 60 * 24 * TaskProcessingWorkerIsRunning::HAS_TASKS_IN_LAST_X_DAYS, $scheduleAfter); + $this->assertNull($minPickupDelay); + return 0; + }); + $this->appConfig->expects($this->never())->method('getValueString'); + + $this->assertEquals(SetupResult::SUCCESS, $this->check->run()->getSeverity()); + } } diff --git a/core/Listener/AddMissingIndicesListener.php b/core/Listener/AddMissingIndicesListener.php index e35126ab16ded..22cf7d76735c6 100644 --- a/core/Listener/AddMissingIndicesListener.php +++ b/core/Listener/AddMissingIndicesListener.php @@ -229,5 +229,11 @@ public function handle(Event $event): void { 'taskp_status_type_upd', ['status', 'type', 'last_updated'] ); + + $event->addMissingIndex( + 'taskprocessing_tasks', + 'taskp_tasks_scheduled', + ['scheduled_at'] + ); } } diff --git a/core/Migrations/Version30000Date20240708160048.php b/core/Migrations/Version30000Date20240708160048.php index 587cf451ff73e..8dc9cad12d035 100644 --- a/core/Migrations/Version30000Date20240708160048.php +++ b/core/Migrations/Version30000Date20240708160048.php @@ -13,7 +13,9 @@ use OCP\DB\ISchemaWrapper; use OCP\DB\Types; use OCP\Migration\Attributes\AddColumn; +use OCP\Migration\Attributes\AddIndex; use OCP\Migration\Attributes\ColumnType; +use OCP\Migration\Attributes\IndexType; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; @@ -23,6 +25,7 @@ #[AddColumn(table: 'taskprocessing_tasks', name: 'scheduled_at', type: ColumnType::INTEGER)] #[AddColumn(table: 'taskprocessing_tasks', name: 'started_at', type: ColumnType::INTEGER)] #[AddColumn(table: 'taskprocessing_tasks', name: 'ended_at', type: ColumnType::INTEGER)] +#[AddIndex(table: 'taskprocessing_tasks', type: IndexType::INDEX)] class Version30000Date20240708160048 extends SimpleMigrationStep { /** @@ -60,6 +63,9 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'unsigned' => true, ]); } + if (!$table->hasIndex('taskp_tasks_scheduled')) { + $table->addIndex(['scheduled_at'], 'taskp_tasks_scheduled'); + } return $schema; } diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index 0f36f19fcd0c1..d7870dd95e2d7 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -435,32 +435,53 @@ public function findNOldestScheduledByType(array $taskTypes, array $taskIdsToIgn } /** - * @param list $taskTypeIds - * @param int $status - * @return int + * Count the tasks matching the given filters, without loading them. + * + * @param ?int $status Only count tasks in this status + * @param list $taskTypeIds Only count tasks of these types + * @param ?int $scheduleAfter Only count tasks scheduled after this timestamp + * @param ?int $minPickupDelay Only count tasks that took more than this many seconds to be picked up * @throws Exception */ - public function countByStatus(array $taskTypeIds, int $status): int { + public function countTasks( + ?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null, + ): int { if ($taskTypeIds === []) { - return $this->countByStatusQuery($status); + return $this->countTasksQuery($status, null, $scheduleAfter, $minPickupDelay); } $count = 0; foreach (array_chunk($taskTypeIds, 900) as $chunk) { - $count += $this->countByStatusQuery($status, $chunk); + $count += $this->countTasksQuery($status, $chunk, $scheduleAfter, $minPickupDelay); } return $count; } - private function countByStatusQuery(int $status, ?array $taskTypeIds = null): int { + private function countTasksQuery( + ?int $status, ?array $taskTypeIds, ?int $scheduleAfter, ?int $minPickupDelay, + ): int { $qb = $this->db->getQueryBuilder(); $qb->select($qb->func()->count('id')) - ->from($this->tableName) - ->where($qb->expr()->eq('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_INT))); + ->from($this->tableName); + if ($status !== null) { + $qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_INT))); + } if ($taskTypeIds !== null) { $qb->andWhere($qb->expr()->in('type', $qb->createNamedParameter($taskTypeIds, IQueryBuilder::PARAM_STR_ARRAY))); } + if ($scheduleAfter !== null) { + $qb->andWhere($qb->expr()->isNotNull('scheduled_at')); + $qb->andWhere($qb->expr()->gt('scheduled_at', $qb->createNamedParameter($scheduleAfter, IQueryBuilder::PARAM_INT))); + } + if ($minPickupDelay !== null) { + $qb->andWhere($qb->expr()->isNotNull('scheduled_at')); + $qb->andWhere($qb->expr()->isNotNull('started_at')); + $qb->andWhere($qb->expr()->gt( + $qb->createFunction($qb->getColumnName('started_at') . ' - ' . $qb->getColumnName('scheduled_at')), + $qb->createNamedParameter($minPickupDelay, IQueryBuilder::PARAM_INT) + )); + } $result = $qb->executeQuery(); $count = (int)$result->fetchOne(); diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index e301bcab1faf6..604292f2e5164 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -1588,9 +1588,11 @@ public function getTasks( } #[\Override] - public function countTasks(int $status, array $taskTypeIds = []): int { + public function countTasks( + ?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null, + ): int { try { - return $this->taskMapper->countByStatus($taskTypeIds, $status); + return $this->taskMapper->countTasks($status, $taskTypeIds, $scheduleAfter, $minPickupDelay); } catch (\OCP\DB\Exception $e) { throw new \OCP\TaskProcessing\Exception\Exception('There was a problem counting the tasks', 0, $e); } diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index bc71ae4cce82e..cc6da31a1a2ce 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -298,15 +298,20 @@ public function lockTask(Task $task): bool; public function setTaskStatus(Task $task, int $status): void; /** - * Get the count of tasks filtered by status and optionally by task type(s) + * Get the count of tasks matching the given filters, without loading them * - * @param int $status The task status to filter by + * @param ?int $status The task status to filter by, or null to count tasks in any status * @param list $taskTypeIds Optional list of task type IDs to filter by + * @param ?int $scheduleAfter Only count tasks that were scheduled after this timestamp + * @param ?int $minPickupDelay Only count tasks that took more than this many seconds to be picked up by a worker * @return int The count of matching tasks * @throws Exception If the query failed * @since 34.0.0 + * @since 36.0.0 - parameter $status became optional, parameters $scheduleAfter and $minPickupDelay were added */ - public function countTasks(int $status, array $taskTypeIds = []): int; + public function countTasks( + ?int $status = null, array $taskTypeIds = [], ?int $scheduleAfter = null, ?int $minPickupDelay = null, + ): int; /** * Extract all input and output file IDs from a task diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php index e2cdaebeef8cd..f39370d2ea496 100644 --- a/tests/lib/TaskProcessing/TaskProcessingTest.php +++ b/tests/lib/TaskProcessing/TaskProcessingTest.php @@ -10,6 +10,7 @@ use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\RegistrationContext; use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\TaskProcessing\Db\Task as DbTask; use OC\TaskProcessing\Db\TaskMapper; use OC\TaskProcessing\Manager; use OC\TaskProcessing\RemoveOldTasksBackgroundJob; @@ -1383,6 +1384,54 @@ public function testNonexistentTask(): void { $this->manager->getTask(2147483646); } + /** + * Insert a task without going through a provider, to control its timestamps and status. + */ + private function insertTask(int $status, ?int $scheduledAt, ?int $startedAt): DbTask { + $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null); + $task->setStatus($status); + $task->setScheduledAt($scheduledAt); + $task->setStartedAt($startedAt); + /** @var DbTask $entity */ + $entity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); + return $entity; + } + + public function testCountTasks(): void { + // Far in the future, so that tasks of other tests are outside of the window + $now = time() + 365 * 24 * 3600; + $window = $now - 7200; + $totalBefore = $this->manager->countTasks(); + $entities = []; + + try { + // Scheduled within the window, picked up after 1 minute + $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 3600, $now - 3540); + // Scheduled within the window, picked up after 10 minutes + $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 3600, $now - 3000); + // Scheduled within the window, picked up after 10 minutes, but failed + $entities[] = $this->insertTask(Task::STATUS_FAILED, $now - 3600, $now - 3000); + // Scheduled within the window, never picked up + $entities[] = $this->insertTask(Task::STATUS_CANCELLED, $now - 3600, null); + // Scheduled before the window, picked up after 10 minutes + $entities[] = $this->insertTask(Task::STATUS_SUCCESSFUL, $now - 90000, $now - 89400); + + self::assertEquals($totalBefore + 5, $this->manager->countTasks()); + self::assertEquals(4, $this->manager->countTasks(scheduleAfter: $window)); + self::assertEquals(1, $this->manager->countTasks(status: Task::STATUS_FAILED, scheduleAfter: $window)); + // Tasks that were never picked up are not counted as slow + self::assertEquals(2, $this->manager->countTasks(scheduleAfter: $window, minPickupDelay: 60 * 4)); + self::assertEquals(1, $this->manager->countTasks(status: Task::STATUS_SUCCESSFUL, scheduleAfter: $window, minPickupDelay: 60 * 4)); + self::assertEquals(0, $this->manager->countTasks(scheduleAfter: $window, minPickupDelay: 60 * 20)); + self::assertEquals(4, $this->manager->countTasks(taskTypeIds: [TextToText::ID], scheduleAfter: $window)); + self::assertEquals(0, $this->manager->countTasks(taskTypeIds: [TextToImage::ID], scheduleAfter: $window)); + } finally { + foreach ($entities as $entity) { + $this->taskMapper->delete($entity); + } + } + } + public function testOldTasksShouldBeCleanedUp(): void { $currentTime = new \DateTime('now'); $timeFactory = $this->createMock(ITimeFactory::class);