123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- # Copyright 2023 The Matrix.org Foundation C.I.C.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from typing import Optional, Tuple
- from twisted.internet.task import deferLater
- from twisted.test.proto_helpers import MemoryReactor
- from synapse.server import HomeServer
- from synapse.types import JsonMapping, ScheduledTask, TaskStatus
- from synapse.util import Clock
- from synapse.util.task_scheduler import TaskScheduler
- from tests import unittest
- class TestTaskScheduler(unittest.HomeserverTestCase):
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- self.task_scheduler = hs.get_task_scheduler()
- self.task_scheduler.register_action(self._test_task, "_test_task")
- self.task_scheduler.register_action(self._sleeping_task, "_sleeping_task")
- self.task_scheduler.register_action(self._raising_task, "_raising_task")
- self.task_scheduler.register_action(self._resumable_task, "_resumable_task")
- async def _test_task(
- self, task: ScheduledTask, first_launch: bool
- ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
- # This test task will copy the parameters to the result
- result = None
- if task.params:
- result = task.params
- return (TaskStatus.COMPLETE, result, None)
- def test_schedule_task(self) -> None:
- """Schedule a task in the future with some parameters to be copied as a result and check it executed correctly.
- Also check that it get removed after `KEEP_TASKS_FOR_MS`."""
- timestamp = self.clock.time_msec() + 30 * 1000
- task_id = self.get_success(
- self.task_scheduler.schedule_task(
- "_test_task",
- timestamp=timestamp,
- params={"val": 1},
- )
- )
- task = self.get_success(self.task_scheduler.get_task(task_id))
- assert task is not None
- self.assertEqual(task.status, TaskStatus.SCHEDULED)
- self.assertIsNone(task.result)
- # The timestamp being 30s after now the task should been executed
- # after the first scheduling loop is run
- self.reactor.advance(TaskScheduler.SCHEDULE_INTERVAL_MS / 1000)
- task = self.get_success(self.task_scheduler.get_task(task_id))
- assert task is not None
- self.assertEqual(task.status, TaskStatus.COMPLETE)
- assert task.result is not None
- # The passed parameter should have been copied to the result
- self.assertTrue(task.result.get("val") == 1)
- # Let's wait for the complete task to be deleted and hence unavailable
- self.reactor.advance((TaskScheduler.KEEP_TASKS_FOR_MS / 1000) + 1)
- task = self.get_success(self.task_scheduler.get_task(task_id))
- self.assertIsNone(task)
- async def _sleeping_task(
- self, task: ScheduledTask, first_launch: bool
- ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
- # Sleep for a second
- await deferLater(self.reactor, 1, lambda: None)
- return TaskStatus.COMPLETE, None, None
- def test_schedule_lot_of_tasks(self) -> None:
- """Schedule more than `TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS` tasks and check the behavior."""
- timestamp = self.clock.time_msec() + 30 * 1000
- task_ids = []
- for i in range(TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS + 1):
- task_ids.append(
- self.get_success(
- self.task_scheduler.schedule_task(
- "_sleeping_task",
- timestamp=timestamp,
- params={"val": i},
- )
- )
- )
- # The timestamp being 30s after now the task should been executed
- # after the first scheduling loop is run
- self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
- # This is to give the time to the sleeping tasks to finish
- self.reactor.advance(1)
- # Check that only MAX_CONCURRENT_RUNNING_TASKS tasks has run and that one
- # is still scheduled.
- tasks = [
- self.get_success(self.task_scheduler.get_task(task_id))
- for task_id in task_ids
- ]
- self.assertEquals(
- len(
- [t for t in tasks if t is not None and t.status == TaskStatus.COMPLETE]
- ),
- TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS,
- )
- scheduled_tasks = [
- t for t in tasks if t is not None and t.status == TaskStatus.SCHEDULED
- ]
- self.assertEquals(len(scheduled_tasks), 1)
- self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
- self.reactor.advance(1)
- # Check that the last task has been properly executed after the next scheduler loop run
- prev_scheduled_task = self.get_success(
- self.task_scheduler.get_task(scheduled_tasks[0].id)
- )
- assert prev_scheduled_task is not None
- self.assertEquals(
- prev_scheduled_task.status,
- TaskStatus.COMPLETE,
- )
- async def _raising_task(
- self, task: ScheduledTask, first_launch: bool
- ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
- raise Exception("raising")
- def test_schedule_raising_task(self) -> None:
- """Schedule a task raising an exception and check it runs to failure and report exception content."""
- task_id = self.get_success(self.task_scheduler.schedule_task("_raising_task"))
- self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
- task = self.get_success(self.task_scheduler.get_task(task_id))
- assert task is not None
- self.assertEqual(task.status, TaskStatus.FAILED)
- self.assertEqual(task.error, "raising")
- async def _resumable_task(
- self, task: ScheduledTask, first_launch: bool
- ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
- if task.result and "in_progress" in task.result:
- return TaskStatus.COMPLETE, {"success": True}, None
- else:
- await self.task_scheduler.update_task(task.id, result={"in_progress": True})
- # Await forever to simulate an aborted task because of a restart
- await deferLater(self.reactor, 2**16, lambda: None)
- # This should never been called
- return TaskStatus.ACTIVE, None, None
- def test_schedule_resumable_task(self) -> None:
- """Schedule a resumable task and check that it gets properly resumed and complete after simulating a synapse restart."""
- task_id = self.get_success(self.task_scheduler.schedule_task("_resumable_task"))
- self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
- task = self.get_success(self.task_scheduler.get_task(task_id))
- assert task is not None
- self.assertEqual(task.status, TaskStatus.ACTIVE)
- # Simulate a synapse restart by emptying the list of running tasks
- self.task_scheduler._running_tasks = set()
- self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
- task = self.get_success(self.task_scheduler.get_task(task_id))
- assert task is not None
- self.assertEqual(task.status, TaskStatus.COMPLETE)
- assert task.result is not None
- self.assertTrue(task.result.get("success"))
|