mirror of
https://github.com/element-hq/synapse.git
synced 2025-03-14 09:45:51 +00:00
Merge f14ff317db
into 59a15da433
This commit is contained in:
commit
d948c7817c
5 changed files with 328 additions and 0 deletions
1
changelog.d/18214.feature
Normal file
1
changelog.d/18214.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add an Admin API endpoint to fetch scheduled tasks.
|
55
docs/admin_api/scheduled_tasks.md
Normal file
55
docs/admin_api/scheduled_tasks.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Show scheduled tasks
|
||||
|
||||
This API returns information about scheduled tasks.
|
||||
|
||||
To use it, you will need to authenticate by providing an `access_token`
|
||||
for a server admin: see [Admin API](../usage/administration/admin_api/).
|
||||
|
||||
The api is:
|
||||
```
|
||||
GET /_synapse/admin/v1/scheduled_tasks
|
||||
```
|
||||
|
||||
It returns a JSON body like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"scheduled_tasks": [
|
||||
{
|
||||
"id": "GSA124oegf1",
|
||||
"action": "shutdown_room",
|
||||
"status": "complete",
|
||||
"timestamp": 23423523,
|
||||
"resource_id": "!roomid",
|
||||
"result": "some result",
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**URL parameters:**
|
||||
|
||||
* `action_name`: string - Is optional. Returns only the scheduled tasks with the given action name. Defaults to None.
|
||||
* `resource_id`: string - Is optional. Returns only the scheduled tasks with the given resource id. Defaults to None.
|
||||
* `status`: string - Is optional. Returns only the scheduled tasks matching the given status, one of
|
||||
- "scheduled" - Task is scheduled but not active
|
||||
- "active" - Task is active and probably running, and if not will be run on next scheduler loop run
|
||||
- "complete" - Task has completed successfully
|
||||
- "failed" - Task is over and either returned a failed status, or had an exception
|
||||
|
||||
Defaults to None.
|
||||
* `max_timestamp`: int - Is optional. Returns only the scheduled tasks with a timestamp inferior to the specified one. Defaults to None.
|
||||
|
||||
**Response**
|
||||
|
||||
The following fields are returned in the JSON response body:
|
||||
|
||||
* `id`: string - ID of scheduled task.
|
||||
* `action`: string - The name of the scheduled task's action.
|
||||
* `status`: string - The status of the scheduled task.
|
||||
* `timestamp`: integer - The timestamp (in milliseconds since the unix epoch) of the given task - If the status is "scheduled" then this represents when it should be launched,
|
||||
otherwise it represents the last time this task got a change of state.
|
||||
* `resource_id`: Optional string - The resource id of the scheduled task, if it possesses one
|
||||
* `result`: Optional Json - Any result of the scheduled task, if given
|
||||
* `error`: Optional string - If the task has the status "failed", the error associated with this failure
|
|
@ -86,6 +86,7 @@ from synapse.rest.admin.rooms import (
|
|||
RoomStateRestServlet,
|
||||
RoomTimestampToEventRestServlet,
|
||||
)
|
||||
from synapse.rest.admin.scheduled_tasks import ScheduledTasksRestServlet
|
||||
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
||||
from synapse.rest.admin.statistics import (
|
||||
LargestRoomsStatistics,
|
||||
|
@ -337,6 +338,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
|
|||
BackgroundUpdateStartJobRestServlet(hs).register(http_server)
|
||||
ExperimentalFeaturesRestServlet(hs).register(http_server)
|
||||
SuspendAccountRestServlet(hs).register(http_server)
|
||||
ScheduledTasksRestServlet(hs).register(http_server)
|
||||
|
||||
|
||||
def register_servlets_for_client_rest_resource(
|
||||
|
|
74
synapse/rest/admin/scheduled_tasks.py
Normal file
74
synapse/rest/admin/scheduled_tasks.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2025 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
from synapse.http.servlet import RestServlet, parse_integer, parse_string
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.admin import admin_patterns, assert_requester_is_admin
|
||||
from synapse.types import JsonDict, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from synapse.server import HomeServer
|
||||
|
||||
|
||||
class ScheduledTasksRestServlet(RestServlet):
|
||||
"""Get a list of scheduled tasks and their statuses
|
||||
optionally filtered by action name, resource id, status, and max timestamp
|
||||
"""
|
||||
|
||||
PATTERNS = admin_patterns("/scheduled_tasks$")
|
||||
|
||||
def __init__(self, hs: "HomeServer"):
|
||||
self._auth = hs.get_auth()
|
||||
self._store = hs.get_datastores().main
|
||||
|
||||
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||
await assert_requester_is_admin(self._auth, request)
|
||||
|
||||
# extract query params
|
||||
action_name = parse_string(request, "action_name")
|
||||
resource_id = parse_string(request, "resource_id")
|
||||
status = parse_string(request, "job_status")
|
||||
max_timestamp = parse_integer(request, "max_timestamp")
|
||||
|
||||
actions = [action_name] if action_name else None
|
||||
statuses = [TaskStatus(status)] if status else None
|
||||
|
||||
tasks = await self._store.get_scheduled_tasks(
|
||||
actions=actions,
|
||||
resource_id=resource_id,
|
||||
statuses=statuses,
|
||||
max_timestamp=max_timestamp,
|
||||
)
|
||||
|
||||
json_tasks = []
|
||||
for task in tasks:
|
||||
result_task = {
|
||||
"id": task.id,
|
||||
"action": task.action,
|
||||
"status": task.status,
|
||||
"timestamp": task.timestamp,
|
||||
"resource_id": task.resource_id,
|
||||
"result": task.result,
|
||||
"error": task.error,
|
||||
}
|
||||
json_tasks.append(result_task)
|
||||
|
||||
return 200, {"scheduled_tasks": json_tasks}
|
196
tests/rest/admin/test_scheduled_tasks.py
Normal file
196
tests/rest/admin/test_scheduled_tasks.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2025 New Vector, Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
# Originally licensed under the Apache License, Version 2.0:
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||
#
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
from typing import Mapping, Optional, Tuple
|
||||
|
||||
from twisted.test.proto_helpers import MemoryReactor
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest.client import login
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonMapping, ScheduledTask, TaskStatus
|
||||
from synapse.util import Clock
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class ScheduledTasksTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||
self.store = hs.get_datastores().main
|
||||
self.admin_user = self.register_user("admin", "pass", admin=True)
|
||||
self.admin_user_tok = self.login("admin", "pass")
|
||||
self._task_scheduler = hs.get_task_scheduler()
|
||||
|
||||
# create and schedule a few tasks
|
||||
async def _test_task(
|
||||
task: ScheduledTask,
|
||||
) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
|
||||
return TaskStatus.ACTIVE, None, None
|
||||
|
||||
async def _finished_test_task(
|
||||
task: ScheduledTask,
|
||||
) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
|
||||
return TaskStatus.COMPLETE, None, None
|
||||
|
||||
async def _failed_test_task(
|
||||
task: ScheduledTask,
|
||||
) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
|
||||
return TaskStatus.FAILED, None, "Everything failed"
|
||||
|
||||
self._task_scheduler.register_action(_test_task, "test_task")
|
||||
self.get_success(
|
||||
self._task_scheduler.schedule_task("test_task", resource_id="test")
|
||||
)
|
||||
|
||||
self._task_scheduler.register_action(_finished_test_task, "finished_test_task")
|
||||
self.get_success(
|
||||
self._task_scheduler.schedule_task(
|
||||
"finished_test_task", resource_id="finished_task"
|
||||
)
|
||||
)
|
||||
|
||||
self._task_scheduler.register_action(_failed_test_task, "failed_test_task")
|
||||
self.get_success(
|
||||
self._task_scheduler.schedule_task(
|
||||
"failed_test_task", resource_id="failed_task"
|
||||
)
|
||||
)
|
||||
|
||||
def check_scheduled_tasks_response(self, scheduled_tasks: Mapping) -> list:
|
||||
result = []
|
||||
for task in scheduled_tasks:
|
||||
if task["resource_id"] == "test":
|
||||
self.assertEqual(task["status"], TaskStatus.ACTIVE)
|
||||
self.assertEqual(task["action"], "test_task")
|
||||
result.append(task)
|
||||
if task["resource_id"] == "finished_task":
|
||||
self.assertEqual(task["status"], TaskStatus.COMPLETE)
|
||||
self.assertEqual(task["action"], "finished_test_task")
|
||||
result.append(task)
|
||||
if task["resource_id"] == "failed_task":
|
||||
self.assertEqual(task["status"], TaskStatus.FAILED)
|
||||
self.assertEqual(task["action"], "failed_test_task")
|
||||
result.append(task)
|
||||
|
||||
return result
|
||||
|
||||
def test_requester_is_not_admin(self) -> None:
|
||||
"""
|
||||
If the user is not a server admin, an error 403 is returned.
|
||||
"""
|
||||
|
||||
self.register_user("user", "pass", admin=False)
|
||||
other_user_tok = self.login("user", "pass")
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks",
|
||||
content={},
|
||||
access_token=other_user_tok,
|
||||
)
|
||||
|
||||
self.assertEqual(403, channel.code, msg=channel.json_body)
|
||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||
|
||||
def test_scheduled_tasks(self) -> None:
|
||||
"""
|
||||
Test that endpoint returns scheduled tasks.
|
||||
"""
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks",
|
||||
content={},
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
scheduled_tasks = channel.json_body["scheduled_tasks"]
|
||||
|
||||
# make sure we got back all the scheduled tasks
|
||||
found_tasks = self.check_scheduled_tasks_response(scheduled_tasks)
|
||||
self.assertEqual(len(found_tasks), 3)
|
||||
|
||||
def test_filtering_scheduled_tasks(self) -> None:
|
||||
"""
|
||||
Test that filtering the scheduled tasks response via query params works as expected.
|
||||
"""
|
||||
# filter via job_status
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks?job_status=active",
|
||||
content={},
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
scheduled_tasks = channel.json_body["scheduled_tasks"]
|
||||
found_tasks = self.check_scheduled_tasks_response(scheduled_tasks)
|
||||
|
||||
# only the active task should have been returned
|
||||
self.assertEqual(len(found_tasks), 1)
|
||||
self.assertEqual(found_tasks[0]["status"], "active")
|
||||
|
||||
# filter via action_name
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks?action_name=test_task",
|
||||
content={},
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
scheduled_tasks = channel.json_body["scheduled_tasks"]
|
||||
|
||||
# only test_task should have been returned
|
||||
found_tasks = self.check_scheduled_tasks_response(scheduled_tasks)
|
||||
self.assertEqual(len(found_tasks), 1)
|
||||
self.assertEqual(found_tasks[0]["action"], "test_task")
|
||||
|
||||
# filter via max_timestamp
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks?max_timestamp=0",
|
||||
content={},
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
scheduled_tasks = channel.json_body["scheduled_tasks"]
|
||||
found_tasks = self.check_scheduled_tasks_response(scheduled_tasks)
|
||||
|
||||
# none should have been returned
|
||||
self.assertEqual(len(found_tasks), 0)
|
||||
|
||||
# filter via resource id
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_synapse/admin/v1/scheduled_tasks?resource_id=failed_task",
|
||||
content={},
|
||||
access_token=self.admin_user_tok,
|
||||
)
|
||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||
scheduled_tasks = channel.json_body["scheduled_tasks"]
|
||||
found_tasks = self.check_scheduled_tasks_response(scheduled_tasks)
|
||||
|
||||
# only the task with the matching resource id should have been returned
|
||||
self.assertEqual(len(found_tasks), 1)
|
||||
self.assertEqual(found_tasks[0]["resource_id"], "failed_task")
|
Loading…
Add table
Reference in a new issue