diff options
Diffstat (limited to 'core/Command/TaskProcessing')
-rw-r--r-- | core/Command/TaskProcessing/Cleanup.php | 93 | ||||
-rw-r--r-- | core/Command/TaskProcessing/EnabledCommand.php | 64 | ||||
-rw-r--r-- | core/Command/TaskProcessing/GetCommand.php | 44 | ||||
-rw-r--r-- | core/Command/TaskProcessing/ListCommand.php | 98 | ||||
-rw-r--r-- | core/Command/TaskProcessing/Statistics.php | 196 |
5 files changed, 495 insertions, 0 deletions
diff --git a/core/Command/TaskProcessing/Cleanup.php b/core/Command/TaskProcessing/Cleanup.php new file mode 100644 index 00000000000..2ed2cbdec94 --- /dev/null +++ b/core/Command/TaskProcessing/Cleanup.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OC\TaskProcessing\Db\TaskMapper; +use OC\TaskProcessing\Manager; +use OCP\Files\AppData\IAppDataFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Cleanup extends Base { + private \OCP\Files\IAppData $appData; + + public function __construct( + protected Manager $taskProcessingManager, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + IAppDataFactory $appDataFactory, + ) { + parent::__construct(); + $this->appData = $appDataFactory->get('core'); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:cleanup') + ->setDescription('cleanup old tasks') + ->addArgument( + 'maxAgeSeconds', + InputArgument::OPTIONAL, + // default is not defined as an argument default value because we want to show a nice "4 months" value + 'delete tasks that are older than this number of seconds, defaults to ' . Manager::MAX_TASK_AGE_SECONDS . ' (4 months)', + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $maxAgeSeconds = $input->getArgument('maxAgeSeconds') ?? Manager::MAX_TASK_AGE_SECONDS; + $output->writeln('<comment>Cleanup up tasks older than ' . $maxAgeSeconds . ' seconds and the related output files</comment>'); + + $taskIdsToCleanup = []; + try { + $fileCleanupGenerator = $this->taskProcessingManager->cleanupTaskProcessingTaskFiles($maxAgeSeconds); + foreach ($fileCleanupGenerator as $cleanedUpEntry) { + $output->writeln( + "<info>\t - " . 'Deleted appData/core/TaskProcessing/' . $cleanedUpEntry['file_name'] + . ' (fileId: ' . $cleanedUpEntry['file_id'] . ', taskId: ' . $cleanedUpEntry['task_id'] . ')</info>' + ); + } + $taskIdsToCleanup = $fileCleanupGenerator->getReturn(); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks files</warning>'); + } + try { + $deletedTaskCount = $this->taskMapper->deleteOlderThan($maxAgeSeconds); + foreach ($taskIdsToCleanup as $taskId) { + $output->writeln("<info>\t - " . 'Deleted task ' . $taskId . ' from the database</info>'); + } + $output->writeln("<comment>\t - " . 'Deleted ' . $deletedTaskCount . ' tasks from the database</comment>'); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks</warning>'); + } + try { + $textToImageDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'), $maxAgeSeconds); + foreach ($textToImageDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/text2image/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + try { + $audioToTextDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'), $maxAgeSeconds); + foreach ($audioToTextDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/audio2text/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + + return 0; + } +} diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php new file mode 100644 index 00000000000..a99e3e001b9 --- /dev/null +++ b/core/Command/TaskProcessing/EnabledCommand.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\IAppConfig; +use OCP\TaskProcessing\IManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class EnabledCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + private IAppConfig $appConfig, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task-type:set-enabled') + ->setDescription('Enable or disable a task type') + ->addArgument( + 'task-type-id', + InputArgument::REQUIRED, + 'ID of the task type to configure' + ) + ->addArgument( + 'enabled', + InputArgument::REQUIRED, + 'status of the task type availability. Set 1 to enable and 0 to disable.' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $enabled = (bool)$input->getArgument('enabled'); + $taskType = $input->getArgument('task-type-id'); + $json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', lazy: true); + try { + if ($json === '') { + $taskTypeSettings = []; + } else { + $taskTypeSettings = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + } + + $taskTypeSettings[$taskType] = $enabled; + + $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskTypeSettings), lazy: true); + $this->writeArrayInOutputFormat($input, $output, $taskTypeSettings); + return 0; + } catch (\JsonException $e) { + throw new \JsonException('Error in TaskType DB entry'); + } + + } +} diff --git a/core/Command/TaskProcessing/GetCommand.php b/core/Command/TaskProcessing/GetCommand.php new file mode 100644 index 00000000000..f97556281a1 --- /dev/null +++ b/core/Command/TaskProcessing/GetCommand.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GetCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:get') + ->setDescription('Display all information for a specific task') + ->addArgument( + 'task-id', + InputArgument::REQUIRED, + 'ID of the task to display' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $taskId = (int)$input->getArgument('task-id'); + $task = $this->taskProcessingManager->getTask($taskId); + $jsonTask = $task->jsonSerialize(); + $jsonTask['error_message'] = $task->getErrorMessage(); + $this->writeArrayInOutputFormat($input, $output, $jsonTask); + return 0; + } +} diff --git a/core/Command/TaskProcessing/ListCommand.php b/core/Command/TaskProcessing/ListCommand.php new file mode 100644 index 00000000000..50a694c1a6e --- /dev/null +++ b/core/Command/TaskProcessing/ListCommand.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use OCP\TaskProcessing\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:list') + ->setDescription('list tasks') + ->addOption( + 'userIdFilter', + 'u', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one user ID' + ) + ->addOption( + 'type', + 't', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one task type' + ) + ->addOption( + 'appId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one app ID' + ) + ->addOption( + 'customId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one custom ID' + ) + ->addOption( + 'status', + 's', + InputOption::VALUE_OPTIONAL, + 'only get the tasks that have a specific status (STATUS_UNKNOWN=0, STATUS_SCHEDULED=1, STATUS_RUNNING=2, STATUS_SUCCESSFUL=3, STATUS_FAILED=4, STATUS_CANCELLED=5)' + ) + ->addOption( + 'scheduledAfter', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that were scheduled after a specific date (Unix timestamp)' + ) + ->addOption( + 'endedBefore', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that ended before a specific date (Unix timestamp)' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userIdFilter = $input->getOption('userIdFilter'); + if ($userIdFilter === null) { + $userIdFilter = ''; + } elseif ($userIdFilter === '') { + $userIdFilter = null; + } + $type = $input->getOption('type'); + $appId = $input->getOption('appId'); + $customId = $input->getOption('customId'); + $status = $input->getOption('status'); + $scheduledAfter = $input->getOption('scheduledAfter'); + $endedBefore = $input->getOption('endedBefore'); + + $tasks = $this->taskProcessingManager->getTasks($userIdFilter, $type, $appId, $customId, $status, $scheduledAfter, $endedBefore); + $arrayTasks = array_map(static function (Task $task) { + $jsonTask = $task->jsonSerialize(); + $jsonTask['error_message'] = $task->getErrorMessage(); + return $jsonTask; + }, $tasks); + + $this->writeArrayInOutputFormat($input, $output, $arrayTasks); + return 0; + } +} diff --git a/core/Command/TaskProcessing/Statistics.php b/core/Command/TaskProcessing/Statistics.php new file mode 100644 index 00000000000..6aa4cd5bf52 --- /dev/null +++ b/core/Command/TaskProcessing/Statistics.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use OCP\TaskProcessing\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Statistics extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:stats') + ->setDescription('get statistics for tasks') + ->addOption( + 'userIdFilter', + 'u', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one user ID' + ) + ->addOption( + 'type', + 't', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one task type' + ) + ->addOption( + 'appId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one app ID' + ) + ->addOption( + 'customId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one custom ID' + ) + ->addOption( + 'status', + 's', + InputOption::VALUE_OPTIONAL, + 'only get the tasks that have a specific status (STATUS_UNKNOWN=0, STATUS_SCHEDULED=1, STATUS_RUNNING=2, STATUS_SUCCESSFUL=3, STATUS_FAILED=4, STATUS_CANCELLED=5)' + ) + ->addOption( + 'scheduledAfter', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that were scheduled after a specific date (Unix timestamp)' + ) + ->addOption( + 'endedBefore', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that ended before a specific date (Unix timestamp)' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userIdFilter = $input->getOption('userIdFilter'); + if ($userIdFilter === null) { + $userIdFilter = ''; + } elseif ($userIdFilter === '') { + $userIdFilter = null; + } + $type = $input->getOption('type'); + $appId = $input->getOption('appId'); + $customId = $input->getOption('customId'); + $status = $input->getOption('status'); + $scheduledAfter = $input->getOption('scheduledAfter'); + $endedBefore = $input->getOption('endedBefore'); + + $tasks = $this->taskProcessingManager->getTasks($userIdFilter, $type, $appId, $customId, $status, $scheduledAfter, $endedBefore); + + $stats = ['Number of tasks' => count($tasks)]; + + $maxRunningTime = 0; + $totalRunningTime = 0; + $runningTimeCount = 0; + + $maxQueuingTime = 0; + $totalQueuingTime = 0; + $queuingTimeCount = 0; + + $maxUserWaitingTime = 0; + $totalUserWaitingTime = 0; + $userWaitingTimeCount = 0; + + $maxInputSize = 0; + $maxOutputSize = 0; + $inputCount = 0; + $inputSum = 0; + $outputCount = 0; + $outputSum = 0; + + foreach ($tasks as $task) { + // running time + if ($task->getStartedAt() !== null && $task->getEndedAt() !== null) { + $taskRunningTime = $task->getEndedAt() - $task->getStartedAt(); + $totalRunningTime += $taskRunningTime; + $runningTimeCount++; + if ($taskRunningTime >= $maxRunningTime) { + $maxRunningTime = $taskRunningTime; + } + } + // queuing time + if ($task->getScheduledAt() !== null && $task->getStartedAt() !== null) { + $taskQueuingTime = $task->getStartedAt() - $task->getScheduledAt(); + $totalQueuingTime += $taskQueuingTime; + $queuingTimeCount++; + if ($taskQueuingTime >= $maxQueuingTime) { + $maxQueuingTime = $taskQueuingTime; + } + } + // user waiting time + if ($task->getScheduledAt() !== null && $task->getEndedAt() !== null) { + $taskUserWaitingTime = $task->getEndedAt() - $task->getScheduledAt(); + $totalUserWaitingTime += $taskUserWaitingTime; + $userWaitingTimeCount++; + if ($taskUserWaitingTime >= $maxUserWaitingTime) { + $maxUserWaitingTime = $taskUserWaitingTime; + } + } + // input/output sizes + if ($task->getStatus() === Task::STATUS_SUCCESSFUL) { + $outputString = json_encode($task->getOutput()); + if ($outputString !== false) { + $outputCount++; + $outputLength = strlen($outputString); + $outputSum += $outputLength; + if ($outputLength > $maxOutputSize) { + $maxOutputSize = $outputLength; + } + } + } + $inputString = json_encode($task->getInput()); + if ($inputString !== false) { + $inputCount++; + $inputLength = strlen($inputString); + $inputSum += $inputLength; + if ($inputLength > $maxInputSize) { + $maxInputSize = $inputLength; + } + } + } + + if ($runningTimeCount > 0) { + $stats['Max running time'] = $maxRunningTime; + $averageRunningTime = $totalRunningTime / $runningTimeCount; + $stats['Average running time'] = (int)$averageRunningTime; + $stats['Running time count'] = $runningTimeCount; + } + if ($queuingTimeCount > 0) { + $stats['Max queuing time'] = $maxQueuingTime; + $averageQueuingTime = $totalQueuingTime / $queuingTimeCount; + $stats['Average queuing time'] = (int)$averageQueuingTime; + $stats['Queuing time count'] = $queuingTimeCount; + } + if ($userWaitingTimeCount > 0) { + $stats['Max user waiting time'] = $maxUserWaitingTime; + $averageUserWaitingTime = $totalUserWaitingTime / $userWaitingTimeCount; + $stats['Average user waiting time'] = (int)$averageUserWaitingTime; + $stats['User waiting time count'] = $userWaitingTimeCount; + } + if ($outputCount > 0) { + $stats['Max output size (bytes)'] = $maxOutputSize; + $averageOutputSize = $outputSum / $outputCount; + $stats['Average output size (bytes)'] = (int)$averageOutputSize; + $stats['Number of tasks with output'] = $outputCount; + } + if ($inputCount > 0) { + $stats['Max input size (bytes)'] = $maxInputSize; + $averageInputSize = $inputSum / $inputCount; + $stats['Average input size (bytes)'] = (int)$averageInputSize; + $stats['Number of tasks with input'] = $inputCount; + } + + $this->writeArrayInOutputFormat($input, $output, $stats); + return 0; + } +} |