diff --git a/core/Command/TaskProcessing/Cleanup.php b/core/Command/TaskProcessing/Cleanup.php new file mode 100644 index 0000000000000..2ed2cbdec94a2 --- /dev/null +++ b/core/Command/TaskProcessing/Cleanup.php @@ -0,0 +1,93 @@ +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('Cleanup up tasks older than ' . $maxAgeSeconds . ' seconds and the related output files'); + + $taskIdsToCleanup = []; + try { + $fileCleanupGenerator = $this->taskProcessingManager->cleanupTaskProcessingTaskFiles($maxAgeSeconds); + foreach ($fileCleanupGenerator as $cleanedUpEntry) { + $output->writeln( + "\t - " . 'Deleted appData/core/TaskProcessing/' . $cleanedUpEntry['file_name'] + . ' (fileId: ' . $cleanedUpEntry['file_id'] . ', taskId: ' . $cleanedUpEntry['task_id'] . ')' + ); + } + $taskIdsToCleanup = $fileCleanupGenerator->getReturn(); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); + $output->writeln('Failed to delete stale task processing tasks files'); + } + try { + $deletedTaskCount = $this->taskMapper->deleteOlderThan($maxAgeSeconds); + foreach ($taskIdsToCleanup as $taskId) { + $output->writeln("\t - " . 'Deleted task ' . $taskId . ' from the database'); + } + $output->writeln("\t - " . 'Deleted ' . $deletedTaskCount . ' tasks from the database'); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + $output->writeln('Failed to delete stale task processing tasks'); + } + try { + $textToImageDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'), $maxAgeSeconds); + foreach ($textToImageDeletedFileNames as $entry) { + $output->writeln("\t - " . 'Deleted appData/core/text2image/' . $entry . ''); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + try { + $audioToTextDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'), $maxAgeSeconds); + foreach ($audioToTextDeletedFileNames as $entry) { + $output->writeln("\t - " . 'Deleted appData/core/audio2text/' . $entry . ''); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + + return 0; + } +} diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php index 0d4b831812ce7..a99e3e001b9e6 100644 --- a/core/Command/TaskProcessing/EnabledCommand.php +++ b/core/Command/TaskProcessing/EnabledCommand.php @@ -1,5 +1,7 @@ |DataResponse */ private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse { - $ids = $this->extractFileIdsFromTask($task); + $ids = $this->taskProcessingManager->extractFileIdsFromTask($task); if (!in_array($fileId, $ids)) { return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); } @@ -428,45 +427,6 @@ private function getFileContentsInternal(Task $task, int $fileId): StreamRespons return $response; } - /** - * @param Task $task - * @return list - * @throws NotFoundException - */ - private function extractFileIdsFromTask(Task $task): array { - $ids = []; - $taskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); - if (!isset($taskTypes[$task->getTaskTypeId()])) { - throw new NotFoundException('Could not find task type'); - } - $taskType = $taskTypes[$task->getTaskTypeId()]; - foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) { - if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { - /** @var int|list $inputSlot */ - $inputSlot = $task->getInput()[$key]; - if (is_array($inputSlot)) { - $ids = array_merge($inputSlot, $ids); - } else { - $ids[] = $inputSlot; - } - } - } - if ($task->getOutput() !== null) { - foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) { - if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { - /** @var int|list $outputSlot */ - $outputSlot = $task->getOutput()[$key]; - if (is_array($outputSlot)) { - $ids = array_merge($outputSlot, $ids); - } else { - $ids[] = $outputSlot; - } - } - } - } - return $ids; - } - /** * Sets the task progress * diff --git a/core/Migrations/Version32000Date20250806110519.php b/core/Migrations/Version32000Date20250806110519.php new file mode 100644 index 0000000000000..c498a1cc82062 --- /dev/null +++ b/core/Migrations/Version32000Date20250806110519.php @@ -0,0 +1,49 @@ +hasTable('taskprocessing_tasks')) { + $table = $schema->getTable('taskprocessing_tasks'); + if (!$table->hasColumn('allow_cleanup')) { + $table->addColumn('allow_cleanup', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 1, + 'unsigned' => true, + ]); + return $schema; + } + } + + return null; + } +} diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index 5fb2502c3885c..1d3992369c630 100644 --- a/core/ResponseDefinitions.php +++ b/core/ResponseDefinitions.php @@ -200,6 +200,7 @@ * scheduledAt: ?int, * startedAt: ?int, * endedAt: ?int, + * allowCleanup: bool, * } * * @psalm-type CoreProfileAction = array{ diff --git a/core/openapi-ex_app.json b/core/openapi-ex_app.json index 4dad268c1b3f5..21e349ffbd08b 100644 --- a/core/openapi-ex_app.json +++ b/core/openapi-ex_app.json @@ -145,7 +145,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -216,6 +217,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } } diff --git a/core/openapi-full.json b/core/openapi-full.json index d4f69abf5354f..fd606e01966a3 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -639,7 +639,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -710,6 +711,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/core/openapi.json b/core/openapi.json index 1a7ddc55c92a5..c5b9263e3939f 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -639,7 +639,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -710,6 +711,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/core/register_command.php b/core/register_command.php index ed64983e76225..9fd5b9b611e0e 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -253,6 +253,7 @@ $application->add(Server::get(EnabledCommand::class)); $application->add(Server::get(Command\TaskProcessing\ListCommand::class)); $application->add(Server::get(Statistics::class)); + $application->add(Server::get(Command\TaskProcessing\Cleanup::class)); $application->add(Server::get(RedisCommand::class)); $application->add(Server::get(DistributedClear::class)); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fda8798fe43c2..f406f24dfe628 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1346,6 +1346,7 @@ 'OC\\Core\\Command\\SystemTag\\Delete' => $baseDir . '/core/Command/SystemTag/Delete.php', 'OC\\Core\\Command\\SystemTag\\Edit' => $baseDir . '/core/Command/SystemTag/Edit.php', 'OC\\Core\\Command\\SystemTag\\ListCommand' => $baseDir . '/core/Command/SystemTag/ListCommand.php', + 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => $baseDir . '/core/Command/TaskProcessing/Cleanup.php', 'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => $baseDir . '/core/Command/TaskProcessing/EnabledCommand.php', 'OC\\Core\\Command\\TaskProcessing\\GetCommand' => $baseDir . '/core/Command/TaskProcessing/GetCommand.php', 'OC\\Core\\Command\\TaskProcessing\\ListCommand' => $baseDir . '/core/Command/TaskProcessing/ListCommand.php', @@ -1512,6 +1513,7 @@ 'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php', + 'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 69e3c41284b51..70e61075aa78e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1387,6 +1387,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Command\\SystemTag\\Delete' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Delete.php', 'OC\\Core\\Command\\SystemTag\\Edit' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Edit.php', 'OC\\Core\\Command\\SystemTag\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/SystemTag/ListCommand.php', + 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/Cleanup.php', 'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/EnabledCommand.php', 'OC\\Core\\Command\\TaskProcessing\\GetCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/GetCommand.php', 'OC\\Core\\Command\\TaskProcessing\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/ListCommand.php', @@ -1553,6 +1554,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php', + 'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php index 4d919deaf94a3..05c0ae9ac742d 100644 --- a/lib/private/TaskProcessing/Db/Task.php +++ b/lib/private/TaskProcessing/Db/Task.php @@ -45,6 +45,8 @@ * @method int getStartedAt() * @method setEndedAt(int $endedAt) * @method int getEndedAt() + * @method setAllowCleanup(int $allowCleanup) + * @method int getAllowCleanup() */ class Task extends Entity { protected $lastUpdated; @@ -63,16 +65,17 @@ class Task extends Entity { protected $scheduledAt; protected $startedAt; protected $endedAt; + protected $allowCleanup; /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at']; + public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt']; + public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup']; public function __construct() { @@ -94,6 +97,7 @@ public function __construct() { $this->addType('scheduledAt', 'integer'); $this->addType('startedAt', 'integer'); $this->addType('endedAt', 'integer'); + $this->addType('allowCleanup', 'integer'); } public function toRow(): array { @@ -122,6 +126,7 @@ public static function fromPublicTask(OCPTask $task): self { 'scheduledAt' => $task->getScheduledAt(), 'startedAt' => $task->getStartedAt(), 'endedAt' => $task->getEndedAt(), + 'allowCleanup' => $task->getAllowCleanup() ? 1 : 0, ]); return $taskEntity; } @@ -144,6 +149,7 @@ public function toPublicTask(): OCPTask { $task->setScheduledAt($this->getScheduledAt()); $task->setStartedAt($this->getStartedAt()); $task->setEndedAt($this->getEndedAt()); + $task->setAllowCleanup($this->getAllowCleanup() !== 0); return $task; } } diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index 91fd68820ae56..fee9653463319 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -183,16 +183,39 @@ public function findTasks( /** * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag * @return int the number of deleted tasks * @throws Exception */ - public function deleteOlderThan(int $timeout): int { + public function deleteOlderThan(int $timeout, bool $force = false): int { $qb = $this->db->getQueryBuilder(); $qb->delete($this->tableName) ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } return $qb->executeStatement(); } + /** + * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag + * @return \Generator + * @throws Exception + */ + public function getTasksToCleanup(int $timeout, bool $force = false): \Generator { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } + foreach ($this->yieldEntities($qb) as $entity) { + yield $entity; + }; + } + public function update(Entity $entity): Entity { $entity->setLastUpdated($this->timeFactory->now()->getTimestamp()); return parent::update($entity); diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index 11fb2bed559e2..e288f2981a817 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -30,6 +30,7 @@ use OCP\Files\Node; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; use OCP\Http\Client\IClientService; use OCP\IAppConfig; use OCP\ICache; @@ -78,6 +79,8 @@ class Manager implements IManager { 'ai.taskprocessing_provider_preferences', ]; + public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months + /** @var list|null */ private ?array $providers = null; @@ -1448,6 +1451,97 @@ private function validateUserAccessToFile(mixed $fileId, ?string $userId): void } } + /** + * @param Task $task + * @return list + * @throws NotFoundException + */ + public function extractFileIdsFromTask(Task $task): array { + $ids = []; + $taskTypes = $this->getAvailableTaskTypes(); + if (!isset($taskTypes[$task->getTaskTypeId()])) { + throw new NotFoundException('Could not find task type'); + } + $taskType = $taskTypes[$task->getTaskTypeId()]; + foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list $inputSlot */ + $inputSlot = $task->getInput()[$key]; + if (is_array($inputSlot)) { + $ids = array_merge($inputSlot, $ids); + } else { + $ids[] = $inputSlot; + } + } + } + if ($task->getOutput() !== null) { + foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list $outputSlot */ + $outputSlot = $task->getOutput()[$key]; + if (is_array($outputSlot)) { + $ids = array_merge($outputSlot, $ids); + } else { + $ids[] = $outputSlot; + } + } + } + } + return $ids; + } + + /** + * @param ISimpleFolder $folder + * @param int $ageInSeconds + * @return \Generator + */ + public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + foreach ($folder->getDirectoryListing() as $file) { + if ($file->getMTime() < time() - $ageInSeconds) { + try { + $fileName = $file->getName(); + $file->delete(); + yield $fileName; + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); + } + } + } + } + + /** + * @param int $ageInSeconds + * @return \Generator + * @throws Exception + * @throws InvalidPathException + * @throws NotFoundException + * @throws \JsonException + * @throws \OCP\Files\NotFoundException + */ + public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + $taskIdsToCleanup = []; + foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) { + $taskIdsToCleanup[] = $task->getId(); + $ocpTask = $task->toPublicTask(); + $fileIds = $this->extractFileIdsFromTask($ocpTask); + foreach ($fileIds as $fileId) { + // only look for output files stored in appData/TaskProcessing/ + $file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/'); + if ($file instanceof File) { + try { + $fileId = $file->getId(); + $fileName = $file->getName(); + $file->delete(); + yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName]; + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); + } + } + } + } + return $taskIdsToCleanup; + } + /** * Make a request to the task's webhookUri if necessary * diff --git a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php index 42d073a024d31..52fc204b7525d 100644 --- a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php +++ b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php @@ -10,17 +10,14 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; use OCP\Files\AppData\IAppDataFactory; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\Files\SimpleFS\ISimpleFolder; use Psr\Log\LoggerInterface; class RemoveOldTasksBackgroundJob extends TimedJob { - public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months private \OCP\Files\IAppData $appData; public function __construct( ITimeFactory $timeFactory, + private Manager $taskProcessingManager, private TaskMapper $taskMapper, private LoggerInterface $logger, IAppDataFactory $appDataFactory, @@ -32,48 +29,29 @@ public function __construct( $this->appData = $appDataFactory->get('core'); } - /** * @inheritDoc */ protected function run($argument): void { try { - $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); - } catch (\OCP\DB\Exception $e) { - $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + iterator_to_array($this->taskProcessingManager->cleanupTaskProcessingTaskFiles()); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('text2image'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { - // noop + $this->taskMapper->deleteOlderThan(Manager::MAX_TASK_AGE_SECONDS); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('audio2text'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } try { - $this->clearFilesOlderThan($this->appData->getFolder('TaskProcessing'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } } - - /** - * @param ISimpleFolder $folder - * @param int $ageInSeconds - * @return void - */ - private function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds): void { - foreach ($folder->getDirectoryListing() as $file) { - if ($file->getMTime() < time() - $ageInSeconds) { - try { - $file->delete(); - } catch (NotPermittedException $e) { - $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); - } - } - } - } - } diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index 723eca8f61561..731250d7aa1c2 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -234,4 +234,14 @@ public function lockTask(Task $task): bool; * @since 30.0.0 */ public function setTaskStatus(Task $task, int $status): void; + + /** + * Extract all input and output file IDs from a task + * + * @param Task $task + * @return list + * @throws NotFoundException + * @since 32.0.0 + */ + public function extractFileIdsFromTask(Task $task): array; } diff --git a/lib/public/TaskProcessing/Task.php b/lib/public/TaskProcessing/Task.php index 71c271cd43d52..06dc84d59ff4a 100644 --- a/lib/public/TaskProcessing/Task.php +++ b/lib/public/TaskProcessing/Task.php @@ -66,6 +66,7 @@ final class Task implements \JsonSerializable { protected ?int $scheduledAt = null; protected ?int $startedAt = null; protected ?int $endedAt = null; + protected bool $allowCleanup = true; /** * @param string $taskTypeId @@ -253,7 +254,23 @@ final public function setEndedAt(?int $endedAt): void { } /** - * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array|numeric|string>, output: ?array|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int} + * @return bool + * @since 32.0.0 + */ + final public function getAllowCleanup(): bool { + return $this->allowCleanup; + } + + /** + * @param bool $allowCleanup + * @since 32.0.0 + */ + final public function setAllowCleanup(bool $allowCleanup): void { + $this->allowCleanup = $allowCleanup; + } + + /** + * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array|numeric|string>, output: ?array|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int, allowCleanup: bool} * @since 30.0.0 */ final public function jsonSerialize(): array { @@ -272,6 +289,7 @@ final public function jsonSerialize(): array { 'scheduledAt' => $this->getScheduledAt(), 'startedAt' => $this->getStartedAt(), 'endedAt' => $this->getEndedAt(), + 'allowCleanup' => $this->getAllowCleanup(), ]; } diff --git a/openapi.json b/openapi.json index cf391e80eea31..af0c627d39f93 100644 --- a/openapi.json +++ b/openapi.json @@ -677,7 +677,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -748,6 +749,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php index fee4e9ba3bad8..d2f619da3495d 100644 --- a/tests/lib/TaskProcessing/TaskProcessingTest.php +++ b/tests/lib/TaskProcessing/TaskProcessingTest.php @@ -972,6 +972,7 @@ public function testOldTasksShouldBeCleanedUp(): void { // run background job $bgJob = new RemoveOldTasksBackgroundJob( $timeFactory, + $this->manager, $this->taskMapper, Server::get(LoggerInterface::class), Server::get(IAppDataFactory::class), diff --git a/version.php b/version.php index c13cc6eebc28b..97d570fc3dbce 100644 --- a/version.php +++ b/version.php @@ -9,7 +9,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // when updating major/minor version number. -$OC_Version = [32, 0, 0, 2]; +$OC_Version = [32, 0, 0, 3]; // The human-readable string $OC_VersionString = '32.0.0 dev';