test_task_scheduler.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # Copyright 2023 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Optional, Tuple
  15. from twisted.internet.task import deferLater
  16. from twisted.test.proto_helpers import MemoryReactor
  17. from synapse.server import HomeServer
  18. from synapse.types import JsonMapping, ScheduledTask, TaskStatus
  19. from synapse.util import Clock
  20. from synapse.util.task_scheduler import TaskScheduler
  21. from tests.replication._base import BaseMultiWorkerStreamTestCase
  22. from tests.unittest import HomeserverTestCase, override_config
  23. class TestTaskScheduler(HomeserverTestCase):
  24. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  25. self.task_scheduler = hs.get_task_scheduler()
  26. self.task_scheduler.register_action(self._test_task, "_test_task")
  27. self.task_scheduler.register_action(self._sleeping_task, "_sleeping_task")
  28. self.task_scheduler.register_action(self._raising_task, "_raising_task")
  29. self.task_scheduler.register_action(self._resumable_task, "_resumable_task")
  30. async def _test_task(
  31. self, task: ScheduledTask
  32. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  33. # This test task will copy the parameters to the result
  34. result = None
  35. if task.params:
  36. result = task.params
  37. return (TaskStatus.COMPLETE, result, None)
  38. def test_schedule_task(self) -> None:
  39. """Schedule a task in the future with some parameters to be copied as a result and check it executed correctly.
  40. Also check that it get removed after `KEEP_TASKS_FOR_MS`."""
  41. timestamp = self.clock.time_msec() + 30 * 1000
  42. task_id = self.get_success(
  43. self.task_scheduler.schedule_task(
  44. "_test_task",
  45. timestamp=timestamp,
  46. params={"val": 1},
  47. )
  48. )
  49. task = self.get_success(self.task_scheduler.get_task(task_id))
  50. assert task is not None
  51. self.assertEqual(task.status, TaskStatus.SCHEDULED)
  52. self.assertIsNone(task.result)
  53. # The timestamp being 30s after now the task should been executed
  54. # after the first scheduling loop is run
  55. self.reactor.advance(TaskScheduler.SCHEDULE_INTERVAL_MS / 1000)
  56. task = self.get_success(self.task_scheduler.get_task(task_id))
  57. assert task is not None
  58. self.assertEqual(task.status, TaskStatus.COMPLETE)
  59. assert task.result is not None
  60. # The passed parameter should have been copied to the result
  61. self.assertTrue(task.result.get("val") == 1)
  62. # Let's wait for the complete task to be deleted and hence unavailable
  63. self.reactor.advance((TaskScheduler.KEEP_TASKS_FOR_MS / 1000) + 1)
  64. task = self.get_success(self.task_scheduler.get_task(task_id))
  65. self.assertIsNone(task)
  66. async def _sleeping_task(
  67. self, task: ScheduledTask
  68. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  69. # Sleep for a second
  70. await deferLater(self.reactor, 1, lambda: None)
  71. return TaskStatus.COMPLETE, None, None
  72. def test_schedule_lot_of_tasks(self) -> None:
  73. """Schedule more than `TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS` tasks and check the behavior."""
  74. task_ids = []
  75. for i in range(TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS + 1):
  76. task_ids.append(
  77. self.get_success(
  78. self.task_scheduler.schedule_task(
  79. "_sleeping_task",
  80. params={"val": i},
  81. )
  82. )
  83. )
  84. # This is to give the time to the active tasks to finish
  85. self.reactor.advance(1)
  86. # Check that only MAX_CONCURRENT_RUNNING_TASKS tasks has run and that one
  87. # is still scheduled.
  88. tasks = [
  89. self.get_success(self.task_scheduler.get_task(task_id))
  90. for task_id in task_ids
  91. ]
  92. self.assertEquals(
  93. len(
  94. [t for t in tasks if t is not None and t.status == TaskStatus.COMPLETE]
  95. ),
  96. TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS,
  97. )
  98. scheduled_tasks = [
  99. t for t in tasks if t is not None and t.status == TaskStatus.ACTIVE
  100. ]
  101. self.assertEquals(len(scheduled_tasks), 1)
  102. # We need to wait for the next run of the scheduler loop
  103. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  104. self.reactor.advance(1)
  105. # Check that the last task has been properly executed after the next scheduler loop run
  106. prev_scheduled_task = self.get_success(
  107. self.task_scheduler.get_task(scheduled_tasks[0].id)
  108. )
  109. assert prev_scheduled_task is not None
  110. self.assertEquals(
  111. prev_scheduled_task.status,
  112. TaskStatus.COMPLETE,
  113. )
  114. async def _raising_task(
  115. self, task: ScheduledTask
  116. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  117. raise Exception("raising")
  118. def test_schedule_raising_task(self) -> None:
  119. """Schedule a task raising an exception and check it runs to failure and report exception content."""
  120. task_id = self.get_success(self.task_scheduler.schedule_task("_raising_task"))
  121. task = self.get_success(self.task_scheduler.get_task(task_id))
  122. assert task is not None
  123. self.assertEqual(task.status, TaskStatus.FAILED)
  124. self.assertEqual(task.error, "raising")
  125. async def _resumable_task(
  126. self, task: ScheduledTask
  127. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  128. if task.result and "in_progress" in task.result:
  129. return TaskStatus.COMPLETE, {"success": True}, None
  130. else:
  131. await self.task_scheduler.update_task(task.id, result={"in_progress": True})
  132. # Await forever to simulate an aborted task because of a restart
  133. await deferLater(self.reactor, 2**16, lambda: None)
  134. # This should never been called
  135. return TaskStatus.ACTIVE, None, None
  136. def test_schedule_resumable_task(self) -> None:
  137. """Schedule a resumable task and check that it gets properly resumed and complete after simulating a synapse restart."""
  138. task_id = self.get_success(self.task_scheduler.schedule_task("_resumable_task"))
  139. task = self.get_success(self.task_scheduler.get_task(task_id))
  140. assert task is not None
  141. self.assertEqual(task.status, TaskStatus.ACTIVE)
  142. # Simulate a synapse restart by emptying the list of running tasks
  143. self.task_scheduler._running_tasks = set()
  144. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  145. task = self.get_success(self.task_scheduler.get_task(task_id))
  146. assert task is not None
  147. self.assertEqual(task.status, TaskStatus.COMPLETE)
  148. assert task.result is not None
  149. self.assertTrue(task.result.get("success"))
  150. class TestTaskSchedulerWithBackgroundWorker(BaseMultiWorkerStreamTestCase):
  151. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  152. self.task_scheduler = hs.get_task_scheduler()
  153. self.task_scheduler.register_action(self._test_task, "_test_task")
  154. async def _test_task(
  155. self, task: ScheduledTask
  156. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  157. return (TaskStatus.COMPLETE, None, None)
  158. @override_config({"run_background_tasks_on": "worker1"})
  159. def test_schedule_task(self) -> None:
  160. """Check that a task scheduled to run now is launch right away on the background worker."""
  161. bg_worker_hs = self.make_worker_hs(
  162. "synapse.app.generic_worker",
  163. extra_config={"worker_name": "worker1"},
  164. )
  165. bg_worker_hs.get_task_scheduler().register_action(self._test_task, "_test_task")
  166. task_id = self.get_success(
  167. self.task_scheduler.schedule_task(
  168. "_test_task",
  169. )
  170. )
  171. task = self.get_success(self.task_scheduler.get_task(task_id))
  172. assert task is not None
  173. self.assertEqual(task.status, TaskStatus.COMPLETE)