summary refs log tree commit diff
path: root/packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch
diff options
context:
space:
mode:
Diffstat (limited to 'packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch')
-rw-r--r--packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch383
1 files changed, 0 insertions, 383 deletions
diff --git a/packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch b/packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch
deleted file mode 100644

index c874ee0..0000000 --- a/packages/overlays/matrix-synapse/patches/0023-Add-an-Admin-API-endpoint-to-fetch-scheduled-tasks-1.patch +++ /dev/null
@@ -1,383 +0,0 @@ -From 6dc1ecd35972c95ce62c5e0563245845c9c64e49 Mon Sep 17 00:00:00 2001 -From: Shay <hillerys@element.io> -Date: Thu, 1 May 2025 11:30:00 -0700 -Subject: [PATCH 23/74] Add an Admin API endpoint to fetch scheduled tasks - (#18214) - ---- - changelog.d/18214.feature | 1 + - docs/admin_api/scheduled_tasks.md | 54 +++++++ - synapse/rest/admin/__init__.py | 2 + - synapse/rest/admin/scheduled_tasks.py | 70 +++++++++ - tests/rest/admin/test_scheduled_tasks.py | 192 +++++++++++++++++++++++ - 5 files changed, 319 insertions(+) - create mode 100644 changelog.d/18214.feature - create mode 100644 docs/admin_api/scheduled_tasks.md - create mode 100644 synapse/rest/admin/scheduled_tasks.py - create mode 100644 tests/rest/admin/test_scheduled_tasks.py - -diff --git a/changelog.d/18214.feature b/changelog.d/18214.feature -new file mode 100644 -index 0000000000..751cb7d383 ---- /dev/null -+++ b/changelog.d/18214.feature -@@ -0,0 +1 @@ -+Add an Admin API endpoint `GET /_synapse/admin/v1/scheduled_tasks` to fetch scheduled tasks. -\ No newline at end of file -diff --git a/docs/admin_api/scheduled_tasks.md b/docs/admin_api/scheduled_tasks.md -new file mode 100644 -index 0000000000..1708871a6d ---- /dev/null -+++ b/docs/admin_api/scheduled_tasks.md -@@ -0,0 +1,54 @@ -+# 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 -+ } -+ ] -+} -+``` -+ -+**Query parameters:** -+ -+* `action_name`: string - Is optional. Returns only the scheduled tasks with the given action name. -+* `resource_id`: string - Is optional. Returns only the scheduled tasks with the given resource id. -+* `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 -+ -+* `max_timestamp`: int - Is optional. Returns only the scheduled tasks with a timestamp inferior to the specified one. -+ -+**Response** -+ -+The following fields are returned in the JSON response body along with a `200` HTTP status code: -+ -+* `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_ms`: 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 -diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py -index 5977ded4a0..cf809d1a27 100644 ---- a/synapse/rest/admin/__init__.py -+++ b/synapse/rest/admin/__init__.py -@@ -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, -@@ -338,6 +339,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( -diff --git a/synapse/rest/admin/scheduled_tasks.py b/synapse/rest/admin/scheduled_tasks.py -new file mode 100644 -index 0000000000..2ae13021b9 ---- /dev/null -+++ b/synapse/rest/admin/scheduled_tasks.py -@@ -0,0 +1,70 @@ -+# -+# 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>. -+# -+# -+# -+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_ms": task.timestamp, -+ "resource_id": task.resource_id, -+ "result": task.result, -+ "error": task.error, -+ } -+ json_tasks.append(result_task) -+ -+ return 200, {"scheduled_tasks": json_tasks} -diff --git a/tests/rest/admin/test_scheduled_tasks.py b/tests/rest/admin/test_scheduled_tasks.py -new file mode 100644 -index 0000000000..9654e9322b ---- /dev/null -+++ b/tests/rest/admin/test_scheduled_tasks.py -@@ -0,0 +1,192 @@ -+# -+# 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>. -+# -+# -+# -+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 ScheduledTasksAdminApiTestCase(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") --- -2.49.0 -