diff --git a/config/app.example.php b/config/app.example.php index 2b37398a..ae54705c 100644 --- a/config/app.example.php +++ b/config/app.example.php @@ -107,6 +107,15 @@ // - string: Uses specified layout 'adminLayout' => null, + // Maximum number of pending and scheduled job rows the admin + // dashboard materialises and renders. Aggregate tile counts on the + // dashboard are still computed via DB count() and reflect the + // unbounded totals; only the visible row list is capped. Raise this + // for more rows at once, lower it if the page is sluggish on a + // large backlog. The cap also keeps DebugKit's Variables panel + // from OOM-ing on huge queues. + 'adminDetailsLimit' => 200, + // Back-to-App link in the admin header (opt-in). When set, an outline // button appears in the top navbar so admins can escape the // plugin-isolated layout. Accepts anything Router::url() takes — Cake diff --git a/src/Controller/Admin/QueueController.php b/src/Controller/Admin/QueueController.php index 8b23beee..56035a10 100644 --- a/src/Controller/Admin/QueueController.php +++ b/src/Controller/Admin/QueueController.php @@ -47,16 +47,31 @@ public function index() { $status = $QueueProcesses->status(); $current = $this->QueuedJobs->getLength(); - $pendingDetails = $this->QueuedJobs->getPendingStats()->toArray(); - $new = 0; - foreach ($pendingDetails as $pendingDetail) { - if ($pendingDetail['fetched'] || $pendingDetail['attempts']) { - continue; - } - $new++; + + // Cap how many pending/scheduled rows we materialise and pass to the + // view. Without a cap a backlog of thousands of pending jobs makes + // the dashboard explode: heavy per-row HTML in the response, and + // DebugKit's Variables panel serialising every entity for its + // snapshot can OOM the request. Aggregate tile counts on the + // dashboard keep using DB-side count() queries so they reflect the + // true totals regardless of the visible-list cap. + $detailsLimit = (int)Configure::read('Queue.adminDetailsLimit', 200); + + // +1 row past the cap so we can detect truncation without a second + // count query in the small-backlog case. + $pendingDetails = $this->QueuedJobs->getPendingStats()->limit($detailsLimit + 1)->toArray(); + $pendingDetailsTruncated = count($pendingDetails) > $detailsLimit; + if ($pendingDetailsTruncated) { + array_pop($pendingDetails); } - $scheduledDetails = $this->QueuedJobs->getScheduledStats()->toArray(); + $new = $this->QueuedJobs->getNewCount(); + + $scheduledDetails = $this->QueuedJobs->getScheduledStats()->limit($detailsLimit + 1)->toArray(); + $scheduledDetailsTruncated = count($scheduledDetails) > $detailsLimit; + if ($scheduledDetailsTruncated) { + array_pop($scheduledDetails); + } $data = $this->QueuedJobs->getStats(); @@ -74,7 +89,17 @@ public function index() { $servers = $QueueProcesses->serverList(); $workers = $status ? $status['workers'] : 0; - $scheduledJobs = count($scheduledDetails); + // True totals from DB. When the visible list is truncated we need + // these for the "showing N of M" hint; when it isn't, they also + // happen to match $pendingDetails count (small extra query that + // avoids one branch in the derivation below). + $totalPending = $pendingDetailsTruncated + ? $this->QueuedJobs->getPendingCount() + : count($pendingDetails); + $scheduledJobs = $scheduledDetailsTruncated + ? $this->QueuedJobs->getScheduledCount() + : count($scheduledDetails); + $runningJobs = $this->QueuedJobs->find() ->where([ 'completed IS' => null, @@ -89,7 +114,7 @@ public function index() { ]) ->count(); // Pending = total pending minus running and failed (to avoid double counting) - $pendingJobs = max(0, count($pendingDetails) - $runningJobs - $failedJobs); + $pendingJobs = max(0, $totalPending - $runningJobs - $failedJobs); $configurations = (array)Configure::read('Queue'); @@ -99,6 +124,10 @@ public function index() { 'data', 'pendingDetails', 'scheduledDetails', + 'pendingDetailsTruncated', + 'scheduledDetailsTruncated', + 'detailsLimit', + 'totalPending', 'status', 'tasks', 'addableTasks', diff --git a/src/Model/Table/QueuedJobsTable.php b/src/Model/Table/QueuedJobsTable.php index aae551a8..0b08119b 100644 --- a/src/Model/Table/QueuedJobsTable.php +++ b/src/Model/Table/QueuedJobsTable.php @@ -1040,6 +1040,46 @@ public function getPendingStats(): SelectQuery { return $this->find('all', ...$findCond); } + /** + * Count of pending jobs (matches getPendingStats() conditions). + * + * Companion to getPendingStats() for callers that LIMIT the materialised + * details list and need the unbounded total to render "showing N of M". + * + * @return int + */ + public function getPendingCount(): int { + return $this->find() + ->where([ + 'completed IS' => null, + 'OR' => [ + 'notbefore <=' => new DateTime(), + 'notbefore IS' => null, + ], + ]) + ->count(); + } + + /** + * Count of pending jobs that have never been fetched or attempted + * (the "new" tile on the admin dashboard). + * + * @return int + */ + public function getNewCount(): int { + return $this->find() + ->where([ + 'completed IS' => null, + 'fetched IS' => null, + 'attempts' => 0, + 'OR' => [ + 'notbefore <=' => new DateTime(), + 'notbefore IS' => null, + ], + ]) + ->count(); + } + /** * @return \Cake\ORM\Query\SelectQuery */ @@ -1068,6 +1108,23 @@ public function getScheduledStats(): SelectQuery { return $this->find('all', ...$findCond); } + /** + * Count of scheduled jobs (matches getScheduledStats() conditions). + * + * Companion to getScheduledStats() for callers that LIMIT the + * materialised details list and need the unbounded total. + * + * @return int + */ + public function getScheduledCount(): int { + return $this->find() + ->where([ + 'completed IS' => null, + 'notbefore >' => new DateTime(), + ]) + ->count(); + } + /** * Cleanup/Delete Completed Jobs. * diff --git a/templates/Admin/Queue/index.php b/templates/Admin/Queue/index.php index 38508aa0..dc9ea613 100644 --- a/templates/Admin/Queue/index.php +++ b/templates/Admin/Queue/index.php @@ -1,8 +1,12 @@ $taskDescriptions @@ -124,6 +128,23 @@ ) ?>