Browse Source

Merge pull request #54272 from nextcloud/enh/noid/taskprocessing-task-add-cleanup-flag

feat(taskprocessing): add cleanup flag to tasks
refresh-ldap-user-on-login
Daniel 4 months ago
committed by GitHub
parent
commit
64c52006dd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 93
      core/Command/TaskProcessing/Cleanup.php
  2. 2
      core/Command/TaskProcessing/EnabledCommand.php
  3. 2
      core/Command/TaskProcessing/GetCommand.php
  4. 2
      core/Command/TaskProcessing/ListCommand.php
  5. 2
      core/Command/TaskProcessing/Statistics.php
  6. 42
      core/Controller/TaskProcessingApiController.php
  7. 49
      core/Migrations/Version32000Date20250806110519.php
  8. 1
      core/ResponseDefinitions.php
  9. 6
      core/openapi-ex_app.json
  10. 6
      core/openapi-full.json
  11. 6
      core/openapi.json
  12. 1
      core/register_command.php
  13. 2
      lib/composer/composer/autoload_classmap.php
  14. 2
      lib/composer/composer/autoload_static.php
  15. 10
      lib/private/TaskProcessing/Db/Task.php
  16. 25
      lib/private/TaskProcessing/Db/TaskMapper.php
  17. 94
      lib/private/TaskProcessing/Manager.php
  18. 44
      lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php
  19. 10
      lib/public/TaskProcessing/IManager.php
  20. 20
      lib/public/TaskProcessing/Task.php
  21. 6
      openapi.json
  22. 1
      tests/lib/TaskProcessing/TaskProcessingTest.php
  23. 2
      version.php

93
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;
}
}

2
core/Command/TaskProcessing/EnabledCommand.php

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later

2
core/Command/TaskProcessing/GetCommand.php

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later

2
core/Command/TaskProcessing/ListCommand.php

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later

2
core/Command/TaskProcessing/Statistics.php

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later

42
core/Controller/TaskProcessingApiController.php

@ -31,7 +31,6 @@ use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\Lock\LockedException;
use OCP\TaskProcessing\EShapeType;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\NotFoundException;
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
@ -391,7 +390,7 @@ class TaskProcessingApiController extends OCSController {
* @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*/
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 @@ class TaskProcessingApiController extends OCSController {
return $response;
}
/**
* @param Task $task
* @return list<int>
* @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<int> $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<int> $outputSlot */
$outputSlot = $task->getOutput()[$key];
if (is_array($outputSlot)) {
$ids = array_merge($outputSlot, $ids);
} else {
$ids[] = $outputSlot;
}
}
}
}
return $ids;
}
/**
* Sets the task progress
*

49
core/Migrations/Version32000Date20250806110519.php

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
*
*/
#[AddColumn(table: 'taskprocessing_tasks', name: 'allow_cleanup', type: ColumnType::SMALLINT)]
class Version32000Date20250806110519 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->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;
}
}

1
core/ResponseDefinitions.php

@ -201,6 +201,7 @@ namespace OC\Core;
* scheduledAt: ?int,
* startedAt: ?int,
* endedAt: ?int,
* allowCleanup: bool,
* }
*
* @psalm-type CoreProfileAction = array{

6
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"
}
}
}

6
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"
}
}
},

6
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"
}
}
},

1
core/register_command.php

@ -253,6 +253,7 @@ if ($config->getSystemValueBool('installed', false)) {
$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));

2
lib/composer/composer/autoload_classmap.php

@ -1349,6 +1349,7 @@ return array(
'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',
@ -1515,6 +1516,7 @@ return array(
'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',

2
lib/composer/composer/autoload_static.php

@ -1390,6 +1390,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',
@ -1556,6 +1557,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',

10
lib/private/TaskProcessing/Db/Task.php

@ -45,6 +45,8 @@ use OCP\TaskProcessing\Task as OCPTask;
* @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 @@ class Task extends Entity {
$this->addType('scheduledAt', 'integer');
$this->addType('startedAt', 'integer');
$this->addType('endedAt', 'integer');
$this->addType('allowCleanup', 'integer');
}
public function toRow(): array {
@ -122,6 +126,7 @@ class Task extends Entity {
'scheduledAt' => $task->getScheduledAt(),
'startedAt' => $task->getStartedAt(),
'endedAt' => $task->getEndedAt(),
'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
]);
return $taskEntity;
}
@ -144,6 +149,7 @@ class Task extends Entity {
$task->setScheduledAt($this->getScheduledAt());
$task->setStartedAt($this->getStartedAt());
$task->setEndedAt($this->getEndedAt());
$task->setAllowCleanup($this->getAllowCleanup() !== 0);
return $task;
}
}

25
lib/private/TaskProcessing/Db/TaskMapper.php

@ -183,16 +183,39 @@ class TaskMapper extends QBMapper {
/**
* @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<Task>
* @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);

94
lib/private/TaskProcessing/Manager.php

@ -30,6 +30,7 @@ use OCP\Files\IRootFolder;
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<IProvider>|null */
private ?array $providers = null;
@ -1448,6 +1451,97 @@ class Manager implements IManager {
}
}
/**
* @param Task $task
* @return list<int>
* @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<int> $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<int> $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
*

44
lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php

@ -10,17 +10,14 @@ use OC\TaskProcessing\Db\TaskMapper;
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 @@ class RemoveOldTasksBackgroundJob extends TimedJob {
$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]);
}
}
}
}
}

10
lib/public/TaskProcessing/IManager.php

@ -234,4 +234,14 @@ interface IManager {
* @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<int>
* @throws NotFoundException
* @since 32.0.0
*/
public function extractFileIdsFromTask(Task $task): array;
}

20
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 class Task implements \JsonSerializable {
}
/**
* @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<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|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<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|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 class Task implements \JsonSerializable {
'scheduledAt' => $this->getScheduledAt(),
'startedAt' => $this->getStartedAt(),
'endedAt' => $this->getEndedAt(),
'allowCleanup' => $this->getAllowCleanup(),
];
}

6
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"
}
}
},

1
tests/lib/TaskProcessing/TaskProcessingTest.php

@ -972,6 +972,7 @@ class TaskProcessingTest extends \Test\TestCase {
// run background job
$bgJob = new RemoveOldTasksBackgroundJob(
$timeFactory,
$this->manager,
$this->taskMapper,
Server::get(LoggerInterface::class),
Server::get(IAppDataFactory::class),

2
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';

Loading…
Cancel
Save