diff --git a/src/Model/Filter/QueuedJobsCollection.php b/src/Model/Filter/QueuedJobsCollection.php index dd96904b..c0bcbde2 100644 --- a/src/Model/Filter/QueuedJobsCollection.php +++ b/src/Model/Filter/QueuedJobsCollection.php @@ -54,6 +54,55 @@ public function initialize(): void { return true; } + if ($status === 'pending') { + // Waiting to be picked up: never fetched, no failure, due, + // and not aborted. Mirrors the dashboard "Pending" card + // (totalPending minus running and retriable-failed). + $query->where([ + 'completed IS' => null, + 'fetched IS' => null, + 'failure_message IS' => null, + 'AND' => [ + [ + 'OR' => [ + 'notbefore <=' => new DateTime(), + 'notbefore IS' => null, + ], + ], + [ + 'OR' => [ + 'status IS' => null, + 'status !=' => QueuedJobsTable::STATUS_ABORTED, + ], + ], + ], + ]); + + return true; + } + if ($status === 'running') { + // Picked up by a worker and not yet completed or failed. + $query->where([ + 'completed IS' => null, + 'fetched IS NOT' => null, + 'failure_message IS' => null, + ]); + + return true; + } + if ($status === 'failed') { + // Unfinished jobs with a recorded failure: still-retrying + // ones and terminally aborted ones alike. + $query->where(['completed IS' => null, 'failure_message IS NOT' => null]); + + return true; + } + if ($status === 'aborted') { + // Terminally failed: retries exhausted, will never run again. + $query->where(['completed IS' => null, 'status' => QueuedJobsTable::STATUS_ABORTED]); + + return true; + } throw new NotImplementedException('Invalid status type'); }, diff --git a/src/Model/Table/QueuedJobsTable.php b/src/Model/Table/QueuedJobsTable.php index 51b29c4b..e31df74a 100644 --- a/src/Model/Table/QueuedJobsTable.php +++ b/src/Model/Table/QueuedJobsTable.php @@ -327,20 +327,12 @@ public function isQueued(string $reference, ?string $jobTask = null): bool { * @return int */ public function getLength(?string $type = null): int { - $findConf = [ - 'conditions' => [ - 'completed IS' => null, - 'OR' => [ - 'notbefore <=' => new DateTime(), - 'notbefore IS' => null, - ], - ], - ]; + $conditions = $this->pendingConditions(); if ($type !== null) { - $findConf['conditions']['job_task'] = $type; + $conditions['job_task'] = $type; } - return $this->find('all', ...$findConf)->count(); + return $this->find('all', conditions: $conditions)->count(); } /** diff --git a/templates/Admin/Queue/index.php b/templates/Admin/Queue/index.php index 177c971d..5ba9d6fe 100644 --- a/templates/Admin/Queue/index.php +++ b/templates/Admin/Queue/index.php @@ -196,6 +196,7 @@ 'count' => $scheduledJobs, 'icon' => 'calendar', 'color' => 'info', + 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'scheduled']]), ]) ?>
@@ -204,6 +205,7 @@ 'count' => $pendingJobs, 'icon' => 'clock', 'color' => 'warning', + 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'pending']]), ]) ?>
@@ -212,6 +214,7 @@ 'count' => $runningJobs, 'icon' => 'spinner', 'color' => 'primary', + 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'running']]), ]) ?>
@@ -220,6 +223,7 @@ 'count' => $failedJobs, 'icon' => 'times-circle', 'color' => 'danger', + 'link' => $this->Url->build(['action' => 'index', 'controller' => 'QueuedJobs', '?' => ['status' => 'failed']]), ]) ?>
diff --git a/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php b/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php index 5c933724..6d13cc26 100644 --- a/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php +++ b/tests/TestCase/Controller/Admin/QueuedJobsControllerTest.php @@ -262,6 +262,39 @@ public function testIndexSearch() { $this->assertNotContains('bar', $jobTasks); } + /** + * The status filter supports running, failed and aborted in addition to + * completed/in_progress/scheduled, so the dashboard stat cards can link to + * the matching job list. + * + * @return void + */ + public function testIndexSearchByRunningFailedAborted() { + $this->createJob(['job_task' => 'waiting']); + $this->createJob(['job_task' => 'running', 'fetched' => new DateTime('-1 minute')]); + $this->createJob(['job_task' => 'failing', 'failure_message' => 'boom', 'attempts' => 1]); + $this->createJob(['job_task' => 'dead', 'failure_message' => 'boom', 'attempts' => 3, 'status' => 'aborted']); + + $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'pending']]); + $this->assertResponseCode(200); + $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList(); + $this->assertSame(['waiting'], $tasks); + + $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'running']]); + $this->assertResponseCode(200); + $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList(); + $this->assertSame(['running'], $tasks); + + $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'failed']]); + $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList(); + sort($tasks); + $this->assertSame(['dead', 'failing'], $tasks); + + $this->get(['prefix' => 'Admin', 'plugin' => 'Queue', 'controller' => 'QueuedJobs', 'action' => 'index', '?' => ['status' => 'aborted']]); + $tasks = collection($this->viewVariable('queuedJobs'))->extract('job_task')->toList(); + $this->assertSame(['dead'], $tasks); + } + /** * @return void */ diff --git a/tests/TestCase/Model/Table/QueuedJobsTableTest.php b/tests/TestCase/Model/Table/QueuedJobsTableTest.php index 57412c85..fe78a588 100644 --- a/tests/TestCase/Model/Table/QueuedJobsTableTest.php +++ b/tests/TestCase/Model/Table/QueuedJobsTableTest.php @@ -903,6 +903,26 @@ public function testGetPendingCountExcludesAborted() { $this->assertNotContains('aborted-one', $statsRefs); } + /** + * getLength() backs the dashboard "Pending Jobs (new/current)" card; it must + * exclude aborted jobs so the card total stays consistent with the pending + * list (getPendingStats()), which already excludes them. + * + * @return void + */ + public function testGetLengthExcludesAborted() { + $job = $this->QueuedJobs->newEntity([ + 'key' => 'key', + 'job_task' => 'FooBar', + 'reference' => 'len-one', + ]); + $this->QueuedJobs->saveOrFail($job); + $this->assertSame(1, $this->QueuedJobs->getLength()); + + $this->QueuedJobs->markJobAborted($job); + $this->assertSame(0, $this->QueuedJobs->getLength()); + } + /** * Resetting an aborted job for rerun must clear its terminal status so it * counts as pending again and gets picked up.