Merge pull request #58376 from nextcloud/feat/taskprocessing/keda-autoscaler

feat(taskprocessing): Add queue_stats API endpoint for external autoscalers
This commit is contained in:
Oleksandr Piskun 2026-03-30 16:27:54 +03:00 committed by GitHub
commit 5ce08e9704
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 627 additions and 0 deletions

View file

@ -424,6 +424,34 @@ class TaskProcessingApiController extends OCSController {
}
}
/**
* 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<string> $taskTypeIds List of task type IDs to filter by
* @return DataResponse<Http::STATUS_OK, array{scheduled_count: int, running_count: int}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 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
*

View file

@ -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",

View file

@ -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",

View file

@ -265,6 +265,40 @@ class TaskMapper extends QBMapper {
return $this->findEntities($qb);
}
/**
* @param list<string> $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
*/

View file

@ -1357,6 +1357,14 @@ class Manager implements IManager {
}
}
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);

View file

@ -258,6 +258,17 @@ interface IManager {
*/
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<string> $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
*

View file

@ -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",