From e0735d7164ece220c127d4bacc8f5d54379f6cda Mon Sep 17 00:00:00 2001
From: Mojmir Fendek
Date: Fri, 24 Apr 2020 11:12:58 +1200
Subject: [PATCH] Feature review for various features.
---
src/Queue/Admin/DataExtension.php | 70 ++++++++
src/Queue/Admin/Extension.php | 222 ++++++++++++++++++++++++
src/Queue/Cleanup/Task.php | 63 +++++++
src/Queue/Dev/Job.php | 62 +++++++
src/Queue/Dev/Task.php | 57 ++++++
src/Queue/ExecutionTime.php | 46 +++++
src/Queue/Extension.php | 37 ++++
src/Queue/Factory/Job.php | 46 +++++
src/Queue/Factory/Task.php | 146 ++++++++++++++++
src/Queue/Job.php | 64 +++++++
src/Queue/Logger.php | 83 +++++++++
src/Queue/Management/Report.php | 279 ++++++++++++++++++++++++++++++
src/Queue/Management/Task.php | 176 +++++++++++++++++++
src/Queue/Manager.php | 36 ++++
src/Queue/batch-state-update.png | Bin 0 -> 34644 bytes
src/Queue/job-admin.png | Bin 0 -> 74651 bytes
src/Queue/job-edit-form.png | Bin 0 -> 30419 bytes
src/Queue/job-messages.png | Bin 0 -> 181430 bytes
src/Queue/overview-report.png | Bin 0 -> 108098 bytes
19 files changed, 1387 insertions(+)
create mode 100644 src/Queue/Admin/DataExtension.php
create mode 100644 src/Queue/Admin/Extension.php
create mode 100644 src/Queue/Cleanup/Task.php
create mode 100644 src/Queue/Dev/Job.php
create mode 100644 src/Queue/Dev/Task.php
create mode 100644 src/Queue/ExecutionTime.php
create mode 100644 src/Queue/Extension.php
create mode 100644 src/Queue/Factory/Job.php
create mode 100644 src/Queue/Factory/Task.php
create mode 100644 src/Queue/Job.php
create mode 100644 src/Queue/Logger.php
create mode 100644 src/Queue/Management/Report.php
create mode 100644 src/Queue/Management/Task.php
create mode 100644 src/Queue/Manager.php
create mode 100644 src/Queue/batch-state-update.png
create mode 100644 src/Queue/job-admin.png
create mode 100644 src/Queue/job-edit-form.png
create mode 100644 src/Queue/job-messages.png
create mode 100644 src/Queue/overview-report.png
diff --git a/src/Queue/Admin/DataExtension.php b/src/Queue/Admin/DataExtension.php
new file mode 100644
index 00000000..ef16da95
--- /dev/null
+++ b/src/Queue/Admin/DataExtension.php
@@ -0,0 +1,70 @@
+owner;
+
+ $fields->addFieldsToTab('Root.JobData', [
+ $jobDataPreview = TextareaField::create('SavedJobDataPreview', 'Job Data'),
+ ]);
+
+ if (strlen($owner->getMessagesRaw()) > 0) {
+ $fields->addFieldToTab(
+ 'Root.MessagesRaw',
+ $messagesRaw = LiteralField::create('MessagesRaw', $owner->getMessagesRaw())
+ );
+ }
+
+ $jobDataPreview->setReadonly(true);
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSavedJobDataPreview(): ?string
+ {
+ return $this->owner->SavedJobData;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getMessagesRaw(): ?string
+ {
+ return $this->owner->SavedJobMessages;
+ }
+
+ /**
+ * @return string
+ */
+ public function getImplementationSummary(): string
+ {
+ $segments = explode('\\', $this->owner->Implementation);
+
+ while (count($segments) > 2) {
+ array_shift($segments);
+ }
+
+ return implode('\\', $segments);
+ }
+}
diff --git a/src/Queue/Admin/Extension.php b/src/Queue/Admin/Extension.php
new file mode 100644
index 00000000..1db3c5d8
--- /dev/null
+++ b/src/Queue/Admin/Extension.php
@@ -0,0 +1,222 @@
+Fields();
+
+ // there are multiple fields that need to be updated
+ $fieldNames = [
+ 'QueuedJobDescriptor',
+ $this->encodeClassName(QueuedJobDescriptor::class),
+ ];
+
+ foreach ($fieldNames as $fieldName) {
+ /** @var GridField $gridField */
+ $gridField = $fields->fieldByName($fieldName);
+
+ if (!$gridField) {
+ continue;
+ }
+
+ $config = $gridField->getConfig();
+
+ // apply custom filters
+ $this->customiseFilters($config);
+ }
+ }
+
+ /**
+ * Customise queued jobs filters UI
+ *
+ * @param GridFieldConfig $config
+ */
+ private function customiseFilters(GridFieldConfig $config): void
+ {
+ /** @var GridFieldDataColumns $gridFieldColumns */
+ $gridFieldColumns = $config->getComponentByType(GridFieldDataColumns::class);
+
+ $gridFieldColumns->setDisplayFields([
+ 'getImplementationSummary' => 'Type',
+ 'JobTypeString' => 'Queue',
+ 'JobStatus' => 'Status',
+ 'JobTitle' => 'Description',
+ 'Created' => 'Added',
+ 'StartAfter' => 'Scheduled',
+ 'JobFinished' => 'Finished',
+ ]);
+
+ $config->removeComponentsByType(GridFieldFilterHeader::class);
+
+ $filter = new RichFilterHeader();
+ $filter
+ ->setFilterConfig([
+ 'getImplementationSummary' => 'Implementation',
+ 'Description' => 'JobTitle',
+ 'Status' => [
+ 'title' => 'JobStatus',
+ 'filter' => 'ExactMatchFilter',
+ ],
+ 'JobTypeString' => [
+ 'title' => 'JobType',
+ 'filter' => 'ExactMatchFilter',
+ ],
+ 'Created' => 'Added',
+ 'StartAfter' => 'Scheduled',
+ ])
+ ->setFilterFields([
+ 'JobType' => $queueType = DropdownField::create(
+ '',
+ '',
+ $this->getQueueTypes()
+ ),
+ 'JobStatus' => $jobStatus = DropdownField::create(
+ '',
+ '',
+ $this->getJobStatuses()
+ ),
+ 'Added' => $added = DropdownField::create(
+ '',
+ '',
+ $this->getAddedDates()
+ ),
+ 'Scheduled' => $scheduled = DropdownField::create(
+ '',
+ '',
+ [
+ self::SCHEDULED_FILTER_FUTURE => self::SCHEDULED_FILTER_FUTURE,
+ self::SCHEDULED_FILTER_PAST => self::SCHEDULED_FILTER_PAST,
+ ]
+ ),
+ ])
+ ->setFilterMethods([
+ 'Added' => static function (DataList $list, $name, $value): DataList {
+ if ($value) {
+ $added = DBDatetime::now()->modify($value);
+
+ return $list->filter(['Created:LessThanOrEqual' => $added->Rfc2822()]);
+ }
+
+ return $list;
+ },
+ 'Scheduled' => static function (DataList $list, $name, $value): DataList {
+ if ($value === static::SCHEDULED_FILTER_FUTURE) {
+ return $list->filter([
+ 'StartAfter:GreaterThan' => DBDatetime::now()->Rfc2822(),
+ ]);
+ }
+
+ if ($value === static::SCHEDULED_FILTER_PAST) {
+ return $list->filter([
+ 'StartAfter:LessThanOrEqual' => DBDatetime::now()->Rfc2822(),
+ ]);
+ }
+
+ return $list;
+ },
+ ]);
+
+ foreach ([$jobStatus, $queueType, $added, $scheduled] as $dropDownField) {
+ /** @var DropdownField $dropDownField */
+ $dropDownField->setEmptyString('-- select --');
+ }
+
+ $config->addComponent($filter, GridFieldPaginator::class);
+ }
+
+ /**
+ * Queue types options for drop down field
+ *
+ * @return array
+ */
+ private function getQueueTypes(): array
+ {
+ /** @var QueuedJobDescriptor $job */
+ $job = QueuedJobDescriptor::singleton();
+ $map = $job->getJobTypeValues();
+ $values = array_values($map);
+ $keys = [];
+
+ foreach (array_keys($map) as $key) {
+ $keys[] = (int) $key;
+ }
+
+ return array_combine($keys, $values);
+ }
+
+ /**
+ * All possible job statuses (this list is not exposed by the module)
+ * intended to be used in a drop down field
+ *
+ * @return array
+ */
+ private function getJobStatuses(): array
+ {
+ /** @var QueuedJobDescriptor $job */
+ $job = QueuedJobDescriptor::singleton();
+ $statuses = $job->getJobStatusValues();
+
+ sort($statuses, SORT_STRING);
+
+ $statuses = array_combine($statuses, $statuses);
+
+ return $statuses;
+ }
+
+ /**
+ * Encode class name to match the matching CMS field name
+ *
+ * @param string $className
+ * @return string
+ */
+ private function encodeClassName(string $className): string
+ {
+ return str_replace('\\', '-', $className);
+ }
+
+ /**
+ * Date options for added dates drop down field
+ *
+ * @return array
+ */
+ private function getAddedDates(): array
+ {
+ return [
+ '-1 day' => '1 day or older',
+ '-3 day' => '3 days or older',
+ '-7 day' => '7 days or older',
+ '-14 day' => '14 days or older',
+ '-1 month' => '1 month or older',
+ ];
+ }
+}
diff --git a/src/Queue/Cleanup/Task.php b/src/Queue/Cleanup/Task.php
new file mode 100644
index 00000000..0981392f
--- /dev/null
+++ b/src/Queue/Cleanup/Task.php
@@ -0,0 +1,63 @@
+isMaintenanceLockActive()) {
+ return;
+ }
+
+ $table = QueuedJobDescriptor::config()->get('table_name');
+
+ // determine expiry
+ $expired = DBDatetime::now()->modify(sprintf('-%s hours', self::EXPIRY_HOURS))->Rfc2822();
+
+ // Format query
+ $query = sprintf(
+ "DELETE FROM `%s` WHERE `JobStatus` = '%s' AND (`JobFinished` <= '%s' OR `JobFinished` IS NULL) LIMIT %d",
+ $table,
+ QueuedJob::STATUS_COMPLETE,
+ $expired,
+ self::EXPIRY_LIMIT
+ );
+
+ DB::query($query);
+
+ echo sprintf('%d job descriptors deleted.', (int) DB::affected_rows());
+ }
+}
diff --git a/src/Queue/Dev/Job.php b/src/Queue/Dev/Job.php
new file mode 100644
index 00000000..b15883f5
--- /dev/null
+++ b/src/Queue/Dev/Job.php
@@ -0,0 +1,62 @@
+type = $type;
+ $this->randomID = $randomID;
+ $this->items = [1, 2, 3, 4, 5];
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Test job';
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getJobType(): int
+ {
+ return (int) $this->type;
+ }
+
+ public function getRunAsMemberID(): ?int
+ {
+ return 0;
+ }
+
+ /**
+ * @param mixed $item
+ */
+ public function processItem($item): void
+ {
+ $this->addMessage(sprintf('Step %d at %s', $item, DBDatetime::now()->Rfc2822()));
+ sleep(1);
+ }
+}
diff --git a/src/Queue/Dev/Task.php b/src/Queue/Dev/Task.php
new file mode 100644
index 00000000..11b234d4
--- /dev/null
+++ b/src/Queue/Dev/Task.php
@@ -0,0 +1,57 @@
+Pass GET param ?total=x to create x jobs.
';
+ echo 'Pass GET param ?type=(2|3) to create jobs in medium|large queues respectively'
+ . ' (defaults to large).
';
+
+ $total = $request->getVar('total') ?: 0;
+ $type = $request->getVar('type') ?: QueuedJob::LARGE;
+ $service = QueuedJobService::singleton();
+
+ for ($i = 1; $i <= $total; $i += 1) {
+ $randomId = $i . DBDatetime::now()->getTimestamp();
+ $job = new Job();
+ $job->hydrate((int) $type, (int) $randomId);
+ $service->queueJob($job);
+ }
+ }
+}
diff --git a/src/Queue/ExecutionTime.php b/src/Queue/ExecutionTime.php
new file mode 100644
index 00000000..b3807b12
--- /dev/null
+++ b/src/Queue/ExecutionTime.php
@@ -0,0 +1,46 @@
+getMaxExecution();
+
+ try {
+ $this->setMaxExecution($executionTime);
+
+ return $callback();
+ } finally {
+ $this->setMaxExecution($originalTime);
+ }
+ }
+}
diff --git a/src/Queue/Extension.php b/src/Queue/Extension.php
new file mode 100644
index 00000000..80e8fd59
--- /dev/null
+++ b/src/Queue/Extension.php
@@ -0,0 +1,37 @@
+addMessage(sprintf('%s : %s', ClassInfo::shortName($e), $e->getMessage()));
+ }
+}
diff --git a/src/Queue/Factory/Job.php b/src/Queue/Factory/Job.php
new file mode 100644
index 00000000..0d4d9628
--- /dev/null
+++ b/src/Queue/Factory/Job.php
@@ -0,0 +1,46 @@
+jobClass = $jobClass;
+ $this->items = $items;
+ }
+
+ public function getTitle(): string
+ {
+ return 'Factory job';
+ }
+
+ /**
+ * @param mixed $item
+ * @throws ValidationException
+ */
+ protected function processItem($item): void
+ {
+ if (!is_array($item) || count($item) === 0) {
+ return;
+ }
+
+ $job = Injector::inst()->create($this->jobClass);
+ $job->hydrate(array_values($item));
+ QueuedJobService::singleton()->queueJob($job);
+ }
+}
diff --git a/src/Queue/Factory/Task.php b/src/Queue/Factory/Task.php
new file mode 100644
index 00000000..1b9a728f
--- /dev/null
+++ b/src/Queue/Factory/Task.php
@@ -0,0 +1,146 @@
+getVar('limit');
+ $offset = (int) $request->getVar('offset');
+
+ if ($limit > 0) {
+ $list = $list->limit($limit, $offset);
+ }
+
+ $ids = $list->columnUnique('ID');
+ $this->queueJobsFromIds($request, $ids, $jobClass, $size);
+ }
+
+ /**
+ * @param HTTPRequest $request
+ * @param array $ids
+ * @param string $jobClass
+ * @param int $size
+ * @throws ValidationException
+ */
+ protected function queueJobsFromIds(HTTPRequest $request, array $ids, string $jobClass, int $size): void
+ {
+ $ids = $this->formatIds($ids);
+ $this->queueJobsFromData($request, $ids, $jobClass, $size);
+ }
+
+ /**
+ * @param HTTPRequest $request
+ * @param array $data
+ * @param string $jobClass
+ * @param int $size
+ * @throws ValidationException
+ */
+ protected function queueJobsFromData(HTTPRequest $request, array $data, string $jobClass, int $size): void
+ {
+ if (count($data) === 0) {
+ return;
+ }
+
+ $jobs = $request->getVar('jobs') ?? self::FACTORY_JOBS_BATCH_SIZE;
+ $jobs = (int) $jobs;
+
+ $chunkSize = (int) $request->getVar('size');
+ $chunkSize = $chunkSize > 0
+ ? $chunkSize
+ : $size;
+
+ $chunks = array_chunk($data, $chunkSize);
+
+ if ($jobs > 0) {
+ $this->createFactoryJobs($chunks, $jobClass, $jobs);
+
+ return;
+ }
+
+ $this->createSpecifiedJobs($chunks, $jobClass);
+ }
+
+ /**
+ * @param array $chunks
+ * @param string $jobClass
+ * @throws ValidationException
+ */
+ private function createSpecifiedJobs(array $chunks, string $jobClass): void
+ {
+ $service = QueuedJobService::singleton();
+
+ foreach ($chunks as $chunk) {
+ $job = Injector::inst()->create($jobClass);
+ $job->hydrate(array_values($chunk));
+ $service->queueJob($job);
+ }
+ }
+
+ /**
+ * @param array $chunks
+ * @param string $jobClass
+ * @param int $chunkSize
+ * @throws ValidationException
+ */
+ private function createFactoryJobs(array $chunks, string $jobClass, int $chunkSize): void
+ {
+ $service = QueuedJobService::singleton();
+ $chunks = array_chunk($chunks, $chunkSize);
+
+ foreach ($chunks as $chunk) {
+ $job = new Job();
+ $job->hydrate($jobClass, array_values($chunk));
+ $service->queueJob($job);
+ }
+ }
+
+ /**
+ * Cast all IDs to int so we don't end up with type errors
+ *
+ * @param array $ids
+ * @return array
+ */
+ private function formatIds(array $ids): array
+ {
+ $formatted = [];
+
+ foreach ($ids as $id) {
+ $formatted[] = (int) $id;
+ }
+
+ return $formatted;
+ }
+}
diff --git a/src/Queue/Job.php b/src/Queue/Job.php
new file mode 100644
index 00000000..f943fea1
--- /dev/null
+++ b/src/Queue/Job.php
@@ -0,0 +1,64 @@
+remaining = $this->items;
+ $this->totalSteps = count($this->items);
+ }
+
+ public function process(): void
+ {
+ $remaining = $this->remaining;
+
+ // check for trivial case
+ if (count($remaining) === 0) {
+ $this->isComplete = true;
+
+ return;
+ }
+
+ $item = array_shift($remaining);
+
+ $this->processItem($item);
+
+ // update job progress
+ $this->remaining = $remaining;
+ $this->currentStep += 1;
+
+ // check for job completion
+ if (count($remaining) > 0) {
+ return;
+ }
+
+ $this->isComplete = true;
+ }
+
+ /**
+ * @param mixed $item
+ */
+ abstract protected function processItem($item): void;
+}
diff --git a/src/Queue/Logger.php b/src/Queue/Logger.php
new file mode 100644
index 00000000..d0649f46
--- /dev/null
+++ b/src/Queue/Logger.php
@@ -0,0 +1,83 @@
+job = $job;
+ }
+
+ public function debug($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function critical($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function alert($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function log($level, $message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function emergency($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function warning($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function error($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function notice($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ public function info($message, array $context = []): void // phpcs:ignore SlevomatCodingStandard.TypeHints
+ {
+ $this->logJobMessage($message);
+ }
+
+ private function logJobMessage(string $message): void
+ {
+ $job = $this->job;
+
+ if (!$job instanceof QueuedJob) {
+ return;
+ }
+
+ $job->addMessage($message);
+ }
+}
diff --git a/src/Queue/Management/Report.php b/src/Queue/Management/Report.php
new file mode 100644
index 00000000..213ad440
--- /dev/null
+++ b/src/Queue/Management/Report.php
@@ -0,0 +1,279 @@
+Rfc2822();
+ $service = QueuedJobService::singleton();
+ $queueState = [];
+
+ if ($service->isMaintenanceLockActive()) {
+ $queueState[] = 'Paused';
+ }
+
+ if ($service->isAtMaxJobs()) {
+ $queueState[] = 'Maximum init jobs';
+ }
+
+ $queueState = $queueState
+ ? implode(' ', $queueState)
+ : 'Running';
+
+ // job states
+ $query = SQLSelect::create(
+ '`JobStatus`, COUNT(`JobStatus`) as `count`',
+ 'QueuedJobDescriptor',
+ ['StartAfter IS NULL OR StartAfter <= ?' => $now],
+ ['count' => 'DESC'],
+ ['JobStatus']
+ );
+
+ $results = $query->execute();
+ $totalJobs = 0;
+
+ $jobsData = [];
+
+ while ($result = $results->next()) {
+ $status = $result['JobStatus'];
+ $count = $result['count'];
+ $jobsData[$status] = $count;
+ $totalJobs+= $count;
+ }
+
+ $brokenJobs = array_key_exists(QueuedJob::STATUS_BROKEN, $jobsData)
+ ? $jobsData[QueuedJob::STATUS_BROKEN]
+ : 0;
+ $newsJobs = array_key_exists(QueuedJob::STATUS_NEW, $jobsData)
+ ? $jobsData[QueuedJob::STATUS_NEW]
+ : 0;
+ $initJobs = array_key_exists(QueuedJob::STATUS_INIT, $jobsData)
+ ? $jobsData[QueuedJob::STATUS_INIT]
+ : 0;
+ $runningJobs = array_key_exists(QueuedJob::STATUS_RUN, $jobsData)
+ ? $jobsData[QueuedJob::STATUS_RUN]
+ : 0;
+ $completedJobs = array_key_exists(QueuedJob::STATUS_COMPLETE, $jobsData)
+ ? $jobsData[QueuedJob::STATUS_COMPLETE]
+ : 0;
+ $jobsInProgress = $newsJobs + $initJobs + $runningJobs;
+
+ $queueState = $queueState === 'Running' && $jobsInProgress === 0
+ ? 'Idle'
+ : $queueState;
+
+ // progress bar
+ echo sprintf(
+ '[%s] Job progress %0.2f%%
',
+ $queueState,
+ $totalJobs > 0 ? (($totalJobs - $jobsInProgress) / $totalJobs) * 100 : 0
+ );
+
+ $barWidth = 1000;
+ echo sprintf(
+ '',
+ $barWidth
+ );
+
+ foreach (['lime' => $completedJobs, 'red' => $brokenJobs] as $color => $count) {
+ echo sprintf(
+ '
'
+ . '
',
+ $count,
+ $color,
+ $totalJobs > 0 ? ($count / $totalJobs) * $barWidth : 0
+ );
+ }
+
+ echo '
';
+
+ echo 'Job status breakdown
';
+
+ foreach ($jobsData as $status => $count) {
+ if (!$count) {
+ continue;
+ }
+
+ echo sprintf('%d - %s
', $count, $status);
+ }
+
+ echo sprintf('%d - Total
', $totalJobs);
+
+ // first and last completed job
+ $query = SQLSelect::create(
+ 'MAX(`JobFinished`) as `last_job`, MIN(`JobStarted`) as `first_job`',
+ 'QueuedJobDescriptor',
+ [['JobStatus' => QueuedJob::STATUS_COMPLETE]]
+ );
+
+ $results = $query->execute();
+ $result = $results->first();
+ $firstJob = $result['first_job'] ?? '';
+ $lastJob = $result['last_job'] ?? '';
+
+ // total job duration
+ $query = SQLSelect::create(
+ sprintf(
+ '`JobTitle`, SUM(UNIX_TIMESTAMP(`JobFinished`) - UNIX_TIMESTAMP(%s) as `duration`, COUNT(*) as `count`',
+ 'COALESCE(`JobRestarted`, `JobStarted`))'
+ ),
+ 'QueuedJobDescriptor',
+ [['JobStatus' => QueuedJob::STATUS_COMPLETE]],
+ ['duration' => 'DESC'],
+ ['JobTitle']
+ );
+
+ $results = $query->execute();
+
+ $totalDuration = 0;
+ $jobDurations = [];
+ $jobTypesCompleted = [];
+ $jobQueueTypeCompleted = [];
+
+ while ($result = $results->next()) {
+ $jobType = $result['JobTitle'];
+ $duration = $result['duration'];
+ $totalDuration += $duration;
+
+ $jobDurations[$jobType] = $duration;
+
+ $count = $result['count'];
+ $jobTypesCompleted[$jobType] = $count;
+ }
+
+ // total job duration
+ $query = SQLSelect::create(
+ 'JobType, COUNT(*) as `count`',
+ 'QueuedJobDescriptor',
+ [['JobStatus' => QueuedJob::STATUS_COMPLETE]],
+ [],
+ ['JobType']
+ );
+
+ $results = $query->execute();
+
+ while ($result = $results->next()) {
+ $jobType = $result['JobType'];
+ $count = $result['count'];
+
+ $jobQueueTypeCompleted[$jobType] = $count;
+ }
+
+ $elapsed = 0;
+
+ if ($totalDuration > 0) {
+ echo sprintf('%d s - total job duration
', $totalDuration);
+ echo sprintf('%0.4f s - average job duration
', $totalDuration / $completedJobs);
+ echo sprintf('%s - first job
', $firstJob);
+ echo sprintf('%s - last job
', $lastJob);
+
+ $elapsed = strtotime($lastJob) - strtotime($firstJob);
+ echo sprintf('%s - elapsed time (s)
', $elapsed);
+ } else {
+ echo 'No completed jobs found
';
+ }
+
+ echo 'Durations by job type
';
+
+ foreach ($jobDurations as $jobType => $duration) {
+ $jobType = $jobType ?: 'Unknown';
+
+ echo sprintf('%d s - %s
', $duration, $jobType);
+ }
+
+ echo 'Completed jobs by job type
';
+
+ foreach ($jobTypesCompleted as $jobType => $completed) {
+ $jobType = $jobType ?: 'Unknown';
+
+ echo sprintf('%d jobs - %s
', $completed, $jobType);
+ }
+
+ echo 'Completed jobs by queue type
';
+
+ $queueTypes = QueuedJobDescriptor::singleton()->getJobTypeValues();
+
+ foreach ($jobQueueTypeCompleted as $jobType => $completed) {
+ $jobType = $jobType ?: QueuedJob::QUEUED;
+
+ echo sprintf('%d - %s
', $completed, $queueTypes[(string) $jobType]);
+ }
+
+ echo 'Seconds per completed job by job type
';
+
+ foreach ($jobDurations as $jobType => $duration) {
+ $completed = (int) $jobTypesCompleted[$jobType];
+ $jobType = $jobType ?: 'Unknown';
+
+ echo sprintf('%f s/job - %s
', ($duration / $completed), $jobType);
+ }
+
+ echo 'Completed jobs per elapsed second by job type
';
+
+ if ($elapsed) {
+ foreach ($jobTypesCompleted as $jobType => $completed) {
+ $jobType = $jobType ?: 'Unknown';
+
+ echo sprintf('%f jobs/elapsed second - %s
', ($completed / $elapsed), $jobType);
+ }
+ }
+
+ // job type breakdown
+ $query = SQLSelect::create(
+ '`JobTitle`, COUNT(`JobTitle`) as `count`',
+ 'QueuedJobDescriptor',
+ ['StartAfter IS NULL OR StartAfter <= ?' => $now],
+ ['count' => 'DESC'],
+ ['JobTitle']
+ );
+
+ $results = $query->execute();
+ echo 'Job type breakdown
';
+
+ while ($result = $results->next()) {
+ $count = $result['count'];
+
+ if (!$count) {
+ continue;
+ }
+
+ echo sprintf('%d - %s
', $count, $result['JobTitle']);
+ }
+ }
+}
diff --git a/src/Queue/Management/Task.php b/src/Queue/Management/Task.php
new file mode 100644
index 00000000..da678ce9
--- /dev/null
+++ b/src/Queue/Management/Task.php
@@ -0,0 +1,176 @@
+getJobStatusValues();
+ sort($statuses, SORT_STRING);
+
+ $currentStatuses = array_diff($statuses, [
+ QueuedJob::STATUS_COMPLETE,
+ ]);
+
+ // job implementations
+ $query = SQLSelect::create(
+ 'DISTINCT `Implementation`',
+ 'QueuedJobDescriptor',
+ ['`JobStatus` != ?' => QueuedJob::STATUS_COMPLETE],
+ ['Implementation' => 'ASC']
+ );
+
+ $results = $query->execute();
+
+ $implementations = [];
+
+ // Add job types
+ while ($result = $results->next()) {
+ $implementation = $result['Implementation'];
+
+ if (!$implementation) {
+ continue;
+ }
+
+ $implementations[] = $result['Implementation'];
+ }
+
+ if (count($implementations) === 0) {
+ echo 'No job implementations found.';
+
+ return;
+ }
+
+ $implementation = $request->postVar('implementation');
+ $currentStatus = $request->postVar('currentStatus');
+ $status = $request->postVar('status');
+
+ if ($implementation
+ && $status
+ && ($implementation === 'all' || in_array($implementation, $implementations))
+ && ($currentStatus === 'any' || in_array($currentStatus, $currentStatuses))
+ && in_array($status, $statuses)
+ ) {
+ $where = [
+ ['`JobStatus` != ?' => QueuedJob::STATUS_COMPLETE],
+ ];
+
+ // Filter by implementation
+ $where[] = $implementation === 'all'
+ ? '`Implementation` IN ' . sprintf(
+ "('%s')",
+ str_replace('\\', '\\\\', implode("','", $implementations))
+ )
+ : ['`Implementation`' => $implementation];
+
+ // Filter by status
+ if ($currentStatus !== 'any') {
+ $where[] = ['`JobStatus`' => $currentStatus];
+ }
+
+ // Assemble query
+ $query = SQLUpdate::create(
+ 'QueuedJobDescriptor',
+ [
+ 'JobStatus' => $status,
+ // make sure to reset all data which is related to job management
+ // job lock
+ 'Worker' => null,
+ 'Expiry' => null,
+ // resume / pause
+ 'ResumeCounts' => 0,
+ // broken job notification
+ 'NotifiedBroken' => 0,
+ ],
+ $where
+ );
+
+ $query->execute();
+
+ echo sprintf('Job status updated (%d rows affected).', DB::affected_rows());
+
+ return;
+ }
+
+ echo '';
+ }
+}
diff --git a/src/Queue/Manager.php b/src/Queue/Manager.php
new file mode 100644
index 00000000..5753321c
--- /dev/null
+++ b/src/Queue/Manager.php
@@ -0,0 +1,36 @@
+k<;BZAfuda?R
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
z
zjHD5Q1`lF|d67CeR>HFN!6`hsRcKc11M==J|Ijfjf#j@kCDii@r-qAkb|tcxP!OKj
zx3#=L-=Zl;fL_v4(DuFX{W&L{vy^mziPj9C{l~qsiL%ViuBhnp6~^acUYuu$Ht8Bb
z5}Lf|;yPs5zW@DGW!p^}_m%2}pDW7gWd0-H{sZ0G3`k=)a4H69p
z`Y3|0ez;;4QFme{5iP(TLWrzA0c_Xqc)*WZ3MSx<0U@e92oh$H{6+)L@rc4>1Y7u0
zCuU;kx_S3Aj92z^4M<;AyxqtU$yScf5;UO-P8h(vCxAw*OE>o|D^VLcw2iV$fYKC^
z-JTUVZk1*`2pY-`IM_$Jo{-k1*ZbhH
z{kX`y73%L^ec+GKpAI{|h)&(ddk{M@aJr1`!#7|>$wqx}zer9ukr-@cH(Vy?f*8LC
z?U>y^%?8Ej+!067)8r)av2k&TW>>$>_MPDF^e2ycN26Z6KN&w*lgKkh;Hx~5_5=t
z%1a708^xiUKgRlPtnP;s;Q;>4z9DEyK4FGe
z3<0ZqfT&MqT)h28XdnLV?Z8u`>+RAYuqf$y_V5DdTLIUn2QUnR-|6p62C*r#=V9k{
zKlc5hj;~Eap#Ai`e{FzBw7%rA>lc*iF#|;nC5E2QkiO_QfO%B-F>UqG!bjI2H6H@|
z@khI{GJBts+d#r6eS6}&9;OiD5@SL^G`)3Tmy+(ic#h0{wPxjLJ
z|6DLQra7(A4x%YXOrr?)Ohz|sFAge!`}r(nlJNXbKTfD;^EK9Fgx?jvSrWzg8Ae{v
zYC)_)ri1_9TpLhLKmjAHY4J2BGN5*)lHeX84QXy4S?-kml+(iu9w5*f*2d~OOMpzo
z9(8wEH+Ff=%Emyi!LU6=f4SCe>fWbdkCAC>!NQ*#(Q%xOvRkVYDqdoMAlJKOD1@i4
zr2*Qvb^0f``>4G9&RT@kX$-06p?|uP4xL>bOe7$`iB0JC06a&W>zjHGK7L5M
zKYHQ&QF;^O4M~zj>CX7I1Oeu(c!6@>OFJzw;&K@E%QjcUj7vLSr5jF>g>aU#1c4
z-B_+)s{OE4<+Hq->bH8)bHX*T>Pw#Mrhm@&g1Pcdsa?;(ugd`kKJDVCUyxjf?tqqWi-|H+NTdH@h|9Z%?!SNM8c?=HU3B&g_kSut1Fk$
zEI9O@h)r)u#dlJJH<*`{ah&^FhCGu?zltakF%56;@^OEN)_!;oHzIo;-E_d=JSvwO
zn88^!+&;x*iYbh;*RkM
zPO29+RNFh&vR@-tyxb$r#Z6`^*Sr8{u|&3&r3L7+tiaC8$`n3>n}>JR)%MQFlYmFM
z(abv=4BdraX2fN!vhGN)MU&fX{}jNuh<&AlRmDKOrwg%WH$hN?FAH{K^d6ZO<$Ce<
zHBOYDHZtf*TUZtz-)BiuTa%d4S;(O>>-SwVy9CjR!%|%50!|WpSg;%mbC4pnh)cpH
z4J(q+jMU3ad2~gTGK{ydUU=|Fy`LV{T;z0;n+%50Va6F+IMiTcAI+e14W-bLuW`?|
zTiQ)Hmf7k)J6U9xQ=8E0QZyBB;G;<_hC$Od
zM$!$!`heO`ahZ&BQMz5zPJNTMI3ib+!8zL7f>J!pL0@x#tGr)9{D?{oMQykGP1FyN
zho!jKIxhtCNJPR$Z0(V4n@C>Fp1z?UHpJDH#Ua{zPU~}vOMJ2c>!g6$UARdIp91x=WhA#>X91&NPpic)9>Emv5THRmt$hWh^BJfh$hkQp&h_vFM{83uZ+Z
zsoOtjTy1A>Z;k5Q0#=<{jmB19W=O>XN4MOZ=1ZE!+-iFMZfan989%Wz=s5Te#v{@7
ztEEymeyu%GtXDk#gs8d8^(j-wl2Bm|wCOy<6C!)5DQ_ORP>p@fDN|k=zCy~lcwUR)
zhR*R`*J$D61N}d!WWVap0ZEL<&Q{)fHW4Cs&bYhepc$FIS_DgBv9cv|Mz_)JGaFu@
zw(r_>u+`WKT&pbS
z$Y2O9~SWel!-?v3%x(3tOgahGGHV})afY=2l_4p{>PUP66ih1?p)@S>16sHU%&{k7*S&}=Ml2_}U{n6zccS;3qV^`-SkK3tN
zy2$hv!$;xcWaLGEqeemCC@$l0#kPCDlb5;9ZMUI5&CB6SUl_VJs{JKx^dh{?ui6?O
z>zzKf`%q$)qsc>5m+s|744GHk&c@kC>+#7i2zaYa3--s(OgzDEijR;9)a$9B?88y^
zlM}sVcU+4|ukbcO!uy3
zHS43~hKk22%Hu~0
zz3L2-#~W3Z_e>?0XtaBozVCg~dl)Ud^0})(naz$rByI-eovz2rIV`@4&2%=sG2P@j
zw(n}HaXeM%b)bbyjE40)!DElCdb+Z(tkuf8Ny_ije0?G<(H
zwRMZHd%3My3BLKUO}z1}o*_gi*CmYdj9HG2vY;s`9+}=0I{9pDNM+O7+S!jR&y}T~
z+9~UrPT}^t2CB0eI?)exDo@e-qLK^5?}WE>eU2!tbS3%}XRvMk+0@*LUacg2q$6PL
zt==T{qQo+s4M}J$Dt?ltJ2Lx2lJCjyA&$hBn5Dou`V_~rMvIYi%KN6x_`S*Yszz?n
zR@S2KLup|QbJ4Gvl{G(|r(UX|#PFy7SPUEa7%M7t!^=-_C#0?#u)$m%8o7fobDvY;6c`dh;*4=jrGzd>JuvRiIxcP+sGcjA=
zSM(>prph}3U(>_u(5Si_$pr#?A3o7q9IV!OtwegO2-e)Ac{6cke~osjXsZSbpChBO
zjv^_#e@=Q`h*keqj&ic5zR4{Gi$*Wy`uSC5B^(|;H}Sr{ccQ2$6d7B1>i{@#MQ4`klnUuLLw-i1DpQ%=pW
z+w1O8i8xcd#&C|kkZ&ghy{y=^kQu?NpYD1fs$XCI@$}NhiLW2a=p1)HdaQC!V*I|(
zmoeHJ>fPKj@1QQ@u<#tqovSc*_WLyas#2rl9cL%CbUSn3?e3*m9sbpe3i;Gz67qJ>
zjGdDbmZ;r(YD%Mq)azH&lim76dE+QAw{zZ8itJzF>f-3C-|D$Dw<>Xh=fLe?dPF5Y
zddt^SP|35l_g7e9Gs*CDInl;6e`f9JE1il5Rxp|8Qy~3)_{%bh!
zVFuOk@mo)dZmCVo(ro7tV{x)w`dn1Ep1KwyIk(AJ$S(DOW~e<2r~S@5!2y+N`!B0q
zuKFG3nz&U|H0Nu*wufIEOMykbqAd^4fO7lO_mPc!B&^h;x1KDQ?f3U~Y^$*0tQmjW
zty6*Z6Nq&{7$i{ZeL&OpL?SzD_i=HWxyToUAE+i7AHQ9b?}g*mFW`e3tdsY$8=91I
znYNF%xW|}$OSbCV7K`0akJH#b%7U5hBK11<+unxhu0c19y|U}BZI8Zk=-Nn~vm^d>
z6IZRqMn{Lm$5qIhg%H5;Hb&;liXkoM6FChe`XQSUdG1-$$s2fmcK`hs48tG};hugm
zbJug^PauCxo0R`+KDLd>?~7!}EMXn@FC2=Q(fV@{PCV-E>%qcwzb~nt;ePa3!=P>F
z`me}?TqFUTaF0{=an#=@K?eqzESy}YtWAS&|GT*
z##lf5^IR3YrjtY7M0!$xZ~F)?$|_&}_vQ4d_`lk|y2t(G&u!He;G(2iBmWHtJXawB
z_4Z5S%P6%!x6M6UB4Z*2k|lX4CK&vH?4S=s%}M?sq-wp;---j1R9^wEWzV6SBgs?R
zN^0{olv1%k=J*Y@UN6^*j&s>R{UuEG};M;`wj
zwAZBbr>Cz?UW#Y)vJTx(DP)o`J>57YH&wGqQN!xJN
zpD|R$r;V2+yiPydP^wcp9XV)n0kKlwLC)J<01dkmVn~LD4#PeeIg~0qY1tBVQYHZn
z6IjOR<;;}=qjWJ1P9$1@qhNXFo`$YM*+T`~TnsIrRJViaNgg+HGMf$TrW>
z_P+xmNtt9D#8?&LwfN$BAINw5^lqkI_s8wQUG0IxK;5AKh83&Pp^HxTJ*{m492IjB
znq4qb6_G~Zy=emqe^uo4+zVi)K{}1~dK%&YXV3`D*Lw+7Sde_m63Zo*y`gQT4<<1U
zlv_K(P?B;3)loKRo-66qWUd8zA;*!{Nr7DA8{{<4fn1PAdJt@2MXDiP@~~I~@YZqh
z&p1cXt5vN54VStK?3Wu1CTg;T?_|`p@2{NgS1u8XY$mrU?oy(!>*!_gy~SJ&Y_%IK
ztQ^g&$aH4F634nsDm#eJRS<*J<-WnSvXI`2aY^r3Im&Zm(^N-iK!TNM4EJAeA&Lb8
z?Wv3Ja?7-Jian|b$hyAZYe%YQAKQ9}CeC5IReZh(jTEetd(+X<);_5$JEW1M-7e{f
zbdINbGFT|Bi{kG%*O4cwN0^KUtRg*P@jsztsfI|=^l!h&KE8x*BKfE-YfHmpHX-xy
zdu&k+FiuxU^!M8pvGwB3;6-_*p>gcSD6mF1M}LA8P6^>%@K{LTEJrt()t(9xuTur@
z66*RCxe@|ha?&nv;SwrTd|NAI%d$f0JrHpqO}jyfta{gFMpb
zJ_>`^wOx;!XHNIg<5Zjkat8GJ8y2??Qp>iXuD1HMJCOJ$%EXuYM$#Y!+AcP%UR?d;
zMp<)4eR>)y>ad^NIYhydeQ++amOhSNJ)ALVQRQ;kTK^mfZW{ailsyH6g8vInFpx4z
zSY-Hn)koTU-@R9?;xWqW_P@B+N_G=n306A7r(HUkt&|yp$a#X7uLWLHqBE+EC6?a%
z>IRH+7PtXretz>~TgqT_t{Rk{aQ+idFBE92np#g`1X2f$eO3ff(F)*19OE34mF6+}
zqGDg5#}UMwDv13Qp8V_R8TpFI*v=HnI;e|3kEBAZ1C%DKfOkq8l~
zYkk|o>yS~aav58--k^JYU-9lL-5X6BP+dm!v0N3k!M36A>>fu-mDo0s&kRh1
z8c5i)x)OihA9CfMD4tj~9gNMKiP|&ws$QzLqyF(e@gpey5|YG7z2vr>w`f`Dg6kc`
zch@aU2K(KA76(Kzs`Wl}j&J#e8<4x3s$%amME&Gr4=#dYiew=i>oh$(VPCqmS_kPJ
zs*$(-m`BxGoy5N59Rol`Qm3-o_PI%lBkU(S>i6!2l-K*QL_56gK$mWww)Oi=7Cqdv
zSnnJe{dYyD;(rrS5m53_?Z|Z|Ah-VT*qXAD(jxB9%HmJPdKJW2u5B|jMaO$9P4NAx
zMp7+10g-<}>D*h1_UKZ_amQ#na@Pn->lHz^9%1LBXjC%Mo4YX}HIAojs9(EFCLMB(
z^A}Wre>}tSKGq<1_ee|_k-#aI!CkRkENn0AfQn6;uW#y~T>BkE8&e4)3HN3teO!OR
ziC_PKiSUdx;R-FM|MLccSl~2LRFIA_juLfM|dW0MShw)xXcx4^OUg
zBRcdiAX)@K^uAn}{qJ+xz?0|XcQpUK?f*B!ah?6ht0PCSNfoZhYX9%h{O?Nn-w)^4
hF82TTeI)4M$fc0PPuol1w2#1F3UaDfiY}Q3|6d`)ZbJY7
literal 0
HcmV?d00001
diff --git a/src/Queue/job-admin.png b/src/Queue/job-admin.png
new file mode 100644
index 0000000000000000000000000000000000000000..685a07ca93bbd0c89de318a9be23929d88059ec1
GIT binary patch
literal 74651
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