From 6a41cfd9a341c4ec08f9d4ff979544896f356762 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Wed, 22 Jul 2026 13:49:14 +0200 Subject: [PATCH] feat(taskprocessing): add an endpoint to get the position of a scheduled task in the queue Signed-off-by: Julien Veyssier --- .../TaskProcessingApiController.php | 25 ++ core/openapi-full.json | 214 ++++++++++++++++++ core/openapi.json | 214 ++++++++++++++++++ lib/private/TaskProcessing/Db/TaskMapper.php | 17 ++ lib/private/TaskProcessing/Manager.php | 23 ++ lib/public/TaskProcessing/IManager.php | 11 + openapi.json | 214 ++++++++++++++++++ 7 files changed, 718 insertions(+) diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php index 58b3270cc3c60..983fe97631035 100644 --- a/core/Controller/TaskProcessingApiController.php +++ b/core/Controller/TaskProcessingApiController.php @@ -740,6 +740,31 @@ public function cancelTaskExAppEndpoint(int $taskId): DataResponse { return $this->handleCancelTaskInternal($taskId); } + /** + * Get a task's position in the queue + * + * @param int $taskId The id of the task + * @return DataResponse|DataResponse + * + * 200: The position was found + * 404: Task not found + * 412: The task is not scheduled + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/queue_position', root: '/taskprocessing')] + public function getTaskQueuePosition(int $taskId): DataResponse { + try { + $position = $this->taskProcessingManager->getTaskQueuePosition($taskId, $this->userId); + return new DataResponse($position); + } catch (NotFoundException) { + return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + } catch (PreConditionNotMetException $e) { + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_PRECONDITION_FAILED); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Returns the next scheduled task for the taskTypeId * diff --git a/core/openapi-full.json b/core/openapi-full.json index 6ef3225218ec1..32740488c7743 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -6321,6 +6321,220 @@ } } }, + "/ocs/v2.php/taskprocessing/tasks/{taskId}/queue_position": { + "get": { + "operationId": "task_processing_api-get-task-queue-position", + "summary": "Get a task's position in the queue", + "tags": [ + "task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The position was found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "412": { + "description": "The task is not scheduled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/teams/{teamId}/resources": { "get": { "operationId": "teams_api-resolve-one", diff --git a/core/openapi.json b/core/openapi.json index 344f5f1e9a4f5..0673f3f96c649 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -6321,6 +6321,220 @@ } } }, + "/ocs/v2.php/taskprocessing/tasks/{taskId}/queue_position": { + "get": { + "operationId": "task_processing_api-get-task-queue-position", + "summary": "Get a task's position in the queue", + "tags": [ + "task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The position was found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "412": { + "description": "The task is not scheduled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/teams/{teamId}/resources": { "get": { "operationId": "teams_api-resolve-one", diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index dcb0ad56ae2dc..0f36f19fcd0c1 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -483,4 +483,21 @@ public function hasRunningTasksForTaskType(string $getTaskTypeId): bool { $result->closeCursor(); return $hasRunningTasks; } + + /** + * @param int $timestamp + * @return int + * @throws Exception + */ + public function countPendingTasksBefore(int $timestamp): int { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('id')) + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT))); + $result = $qb->executeQuery(); + $count = (int)$result->fetchOne(); + $result->closeCursor(); + return $count; + } } diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index 27c5bac611416..4cc1e7a4ccd46 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -1521,6 +1521,29 @@ public function fillInputFileData(?string $userId, array $input, ...$specs): arr return $newInputOutput; } + #[\Override] + public function getTaskQueuePosition(int $id, ?string $userId): int { + try { + $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId); + } catch (DoesNotExistException $e) { + throw new NotFoundException('Could not find the task', 0, $e); + } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) { + throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e); + } catch (\JsonException $e) { + throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e); + } + + if ($taskEntity->getStatus() !== Task::STATUS_SCHEDULED) { + throw new PreConditionNotMetException('This task is not scheduled'); + } + + try { + return 1 + $this->taskMapper->countPendingTasksBefore($taskEntity->getLastUpdated()); + } catch (\OCP\DB\Exception $e) { + throw new \OCP\TaskProcessing\Exception\Exception('There was a problem counting pending tasks', 0, $e); + } + } + #[\Override] public function getUserTask(int $id, ?string $userId): Task { try { diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index 54be8c54e3392..e28e5de9fcfc6 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -202,6 +202,17 @@ public function getNextScheduledTasks(array $taskTypeIds = [], array $taskIdsToI */ public function claimNextScheduledTask(array $taskTypeIds = []): ?Task; + /** + * @param int $id The id of the task + * @param string|null $userId The user id that scheduled the task + * @return int + * @throws Exception If the query failed + * @throws NotFoundException If the task could not be found + * @throws PreConditionNotMetException If the task is not scheduled + * @since 35.0.0 + */ + public function getTaskQueuePosition(int $id, ?string $userId): int; + /** * @param int $id The id of the task * @param string|null $userId The user id that scheduled the task diff --git a/openapi.json b/openapi.json index 895290b17b998..ff3275401ac74 100644 --- a/openapi.json +++ b/openapi.json @@ -10590,6 +10590,220 @@ } } }, + "/ocs/v2.php/taskprocessing/tasks/{taskId}/queue_position": { + "get": { + "operationId": "core-task_processing_api-get-task-queue-position", + "summary": "Get a task's position in the queue", + "tags": [ + "core/task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskId", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The position was found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "412": { + "description": "The task is not scheduled", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/teams/{teamId}/resources": { "get": { "operationId": "core-teams_api-resolve-one",