diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php index 5f2023dd0c242..fd22d485af380 100644 --- a/core/Controller/TaskProcessingApiController.php +++ b/core/Controller/TaskProcessingApiController.php @@ -424,6 +424,34 @@ public function listTasks(?string $taskType, ?string $customId = null): DataResp } } + /** + * Returns queue statistics for task processing + * + * Returns the count of scheduled and running tasks, optionally filtered + * by task type(s). Designed for external scalers (e.g. KEDA) to poll + * for task queue depth. Admin-only endpoint authenticated via app_password. + * + * @param list $taskTypeIds List of task type IDs to filter by + * @return DataResponse|DataResponse + * + * 200: Queue stats returned + */ + #[NoCSRFRequired] + #[ApiRoute(verb: 'GET', url: '/queue_stats', root: '/taskprocessing')] + public function queueStats(array $taskTypeIds = []): DataResponse { + try { + $scheduled = $this->taskProcessingManager->countTasks(Task::STATUS_SCHEDULED, $taskTypeIds); + $running = $this->taskProcessingManager->countTasks(Task::STATUS_RUNNING, $taskTypeIds); + + return new DataResponse([ + 'scheduled_count' => $scheduled, + 'running_count' => $running, + ]); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Returns the contents of a file referenced in a task * diff --git a/core/openapi-administration.json b/core/openapi-administration.json index 80a698fe9f808..c8bcc92c12362 100644 --- a/core/openapi-administration.json +++ b/core/openapi-administration.json @@ -129,6 +129,188 @@ } }, "paths": { + "/ocs/v2.php/taskprocessing/queue_stats": { + "get": { + "operationId": "task_processing_api-queue-stats", + "summary": "Returns queue statistics for task processing", + "description": "Returns the count of scheduled and running tasks, optionally filtered by task type(s). Designed for external scalers (e.g. KEDA) to poll for task queue depth. Admin-only endpoint authenticated via app_password.\nThis endpoint requires admin access", + "tags": [ + "task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskTypeIds[]", + "in": "query", + "description": "List of task type IDs to filter by", + "schema": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + { + "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": "Queue stats returned", + "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": [ + "scheduled_count", + "running_count" + ], + "properties": { + "scheduled_count": { + "type": "integer", + "format": "int64" + }, + "running_count": { + "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" + } + } + } + } + } + } + } + } + } + }, + "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": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "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/twofactor/state": { "get": { "operationId": "two_factor_api-state", diff --git a/core/openapi-full.json b/core/openapi-full.json index de4c07ec3910c..ddc050015992b 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -12649,6 +12649,188 @@ } } }, + "/ocs/v2.php/taskprocessing/queue_stats": { + "get": { + "operationId": "task_processing_api-queue-stats", + "summary": "Returns queue statistics for task processing", + "description": "Returns the count of scheduled and running tasks, optionally filtered by task type(s). Designed for external scalers (e.g. KEDA) to poll for task queue depth. Admin-only endpoint authenticated via app_password.\nThis endpoint requires admin access", + "tags": [ + "task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskTypeIds[]", + "in": "query", + "description": "List of task type IDs to filter by", + "schema": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + { + "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": "Queue stats returned", + "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": [ + "scheduled_count", + "running_count" + ], + "properties": { + "scheduled_count": { + "type": "integer", + "format": "int64" + }, + "running_count": { + "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" + } + } + } + } + } + } + } + } + } + }, + "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": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "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/twofactor/state": { "get": { "operationId": "two_factor_api-state", diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index f62bb41be3b77..fd3b81c0dfb35 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -265,6 +265,40 @@ public function findNOldestScheduledByType(array $taskTypes, array $taskIdsToIgn return $this->findEntities($qb); } + /** + * @param list $taskTypeIds + * @param int $status + * @return int + * @throws Exception + */ + public function countByStatus(array $taskTypeIds, int $status): int { + if ($taskTypeIds === []) { + return $this->countByStatusQuery($status); + } + + $count = 0; + foreach (array_chunk($taskTypeIds, 900) as $chunk) { + $count += $this->countByStatusQuery($status, $chunk); + } + return $count; + } + + private function countByStatusQuery(int $status, ?array $taskTypeIds = null): int { + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->func()->count('id')) + ->from($this->tableName) + ->where($qb->expr()->eq('status', $qb->createNamedParameter($status, IQueryBuilder::PARAM_INT))); + + if ($taskTypeIds !== null) { + $qb->andWhere($qb->expr()->in('type', $qb->createNamedParameter($taskTypeIds, IQueryBuilder::PARAM_STR_ARRAY))); + } + + $result = $qb->executeQuery(); + $count = (int)$result->fetchOne(); + $result->closeCursor(); + return $count; + } + /** * @throws Exception */ diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index 884499d34b0c7..ef8ece9c8233d 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -1357,6 +1357,14 @@ public function getTasks( } } + public function countTasks(int $status, array $taskTypeIds = []): int { + try { + return $this->taskMapper->countByStatus($taskTypeIds, $status); + } catch (\OCP\DB\Exception $e) { + throw new \OCP\TaskProcessing\Exception\Exception('There was a problem counting the tasks', 0, $e); + } + } + public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array { try { $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId); diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index 878acfc134c0d..2cd0244b52e8d 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -258,6 +258,17 @@ public function lockTask(Task $task): bool; */ public function setTaskStatus(Task $task, int $status): void; + /** + * Get the count of tasks filtered by status and optionally by task type(s) + * + * @param int $status The task status to filter by + * @param list $taskTypeIds Optional list of task type IDs to filter by + * @return int The count of matching tasks + * @throws Exception If the query failed + * @since 34.0.0 + */ + public function countTasks(int $status, array $taskTypeIds = []): int; + /** * Extract all input and output file IDs from a task * diff --git a/openapi.json b/openapi.json index 50c44e2bde168..364b31bc75332 100644 --- a/openapi.json +++ b/openapi.json @@ -16351,6 +16351,188 @@ } } }, + "/ocs/v2.php/taskprocessing/queue_stats": { + "get": { + "operationId": "core-task_processing_api-queue-stats", + "summary": "Returns queue statistics for task processing", + "description": "Returns the count of scheduled and running tasks, optionally filtered by task type(s). Designed for external scalers (e.g. KEDA) to poll for task queue depth. Admin-only endpoint authenticated via app_password.\nThis endpoint requires admin access", + "tags": [ + "core/task_processing_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "taskTypeIds[]", + "in": "query", + "description": "List of task type IDs to filter by", + "schema": { + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + { + "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": "Queue stats returned", + "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": [ + "scheduled_count", + "running_count" + ], + "properties": { + "scheduled_count": { + "type": "integer", + "format": "int64" + }, + "running_count": { + "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" + } + } + } + } + } + } + } + } + } + }, + "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": {} + } + } + } + } + } + } + }, + "403": { + "description": "Logged in account must be an admin", + "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/twofactor/state": { "get": { "operationId": "core-two_factor_api-state",