test_task_scheduler.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  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 import unittest
  22. class TestTaskScheduler(unittest.HomeserverTestCase):
  23. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  24. self.task_scheduler = hs.get_task_scheduler()
  25. self.task_scheduler.register_action(self._test_task, "_test_task")
  26. self.task_scheduler.register_action(self._sleeping_task, "_sleeping_task")
  27. self.task_scheduler.register_action(self._raising_task, "_raising_task")
  28. self.task_scheduler.register_action(self._resumable_task, "_resumable_task")
  29. async def _test_task(
  30. self, task: ScheduledTask, first_launch: bool
  31. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  32. # This test task will copy the parameters to the result
  33. result = None
  34. if task.params:
  35. result = task.params
  36. return (TaskStatus.COMPLETE, result, None)
  37. def test_schedule_task(self) -> None:
  38. """Schedule a task in the future with some parameters to be copied as a result and check it executed correctly.
  39. Also check that it get removed after `KEEP_TASKS_FOR_MS`."""
  40. timestamp = self.clock.time_msec() + 30 * 1000
  41. task_id = self.get_success(
  42. self.task_scheduler.schedule_task(
  43. "_test_task",
  44. timestamp=timestamp,
  45. params={"val": 1},
  46. )
  47. )
  48. task = self.get_success(self.task_scheduler.get_task(task_id))
  49. assert task is not None
  50. self.assertEqual(task.status, TaskStatus.SCHEDULED)
  51. self.assertIsNone(task.result)
  52. # The timestamp being 30s after now the task should been executed
  53. # after the first scheduling loop is run
  54. self.reactor.advance(TaskScheduler.SCHEDULE_INTERVAL_MS / 1000)
  55. task = self.get_success(self.task_scheduler.get_task(task_id))
  56. assert task is not None
  57. self.assertEqual(task.status, TaskStatus.COMPLETE)
  58. assert task.result is not None
  59. # The passed parameter should have been copied to the result
  60. self.assertTrue(task.result.get("val") == 1)
  61. # Let's wait for the complete task to be deleted and hence unavailable
  62. self.reactor.advance((TaskScheduler.KEEP_TASKS_FOR_MS / 1000) + 1)
  63. task = self.get_success(self.task_scheduler.get_task(task_id))
  64. self.assertIsNone(task)
  65. async def _sleeping_task(
  66. self, task: ScheduledTask, first_launch: bool
  67. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  68. # Sleep for a second
  69. await deferLater(self.reactor, 1, lambda: None)
  70. return TaskStatus.COMPLETE, None, None
  71. def test_schedule_lot_of_tasks(self) -> None:
  72. """Schedule more than `TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS` tasks and check the behavior."""
  73. timestamp = self.clock.time_msec() + 30 * 1000
  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. timestamp=timestamp,
  81. params={"val": i},
  82. )
  83. )
  84. )
  85. # The timestamp being 30s after now the task should been executed
  86. # after the first scheduling loop is run
  87. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  88. # This is to give the time to the sleeping tasks to finish
  89. self.reactor.advance(1)
  90. # Check that only MAX_CONCURRENT_RUNNING_TASKS tasks has run and that one
  91. # is still scheduled.
  92. tasks = [
  93. self.get_success(self.task_scheduler.get_task(task_id))
  94. for task_id in task_ids
  95. ]
  96. self.assertEquals(
  97. len(
  98. [t for t in tasks if t is not None and t.status == TaskStatus.COMPLETE]
  99. ),
  100. TaskScheduler.MAX_CONCURRENT_RUNNING_TASKS,
  101. )
  102. scheduled_tasks = [
  103. t for t in tasks if t is not None and t.status == TaskStatus.SCHEDULED
  104. ]
  105. self.assertEquals(len(scheduled_tasks), 1)
  106. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  107. self.reactor.advance(1)
  108. # Check that the last task has been properly executed after the next scheduler loop run
  109. prev_scheduled_task = self.get_success(
  110. self.task_scheduler.get_task(scheduled_tasks[0].id)
  111. )
  112. assert prev_scheduled_task is not None
  113. self.assertEquals(
  114. prev_scheduled_task.status,
  115. TaskStatus.COMPLETE,
  116. )
  117. async def _raising_task(
  118. self, task: ScheduledTask, first_launch: bool
  119. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  120. raise Exception("raising")
  121. def test_schedule_raising_task(self) -> None:
  122. """Schedule a task raising an exception and check it runs to failure and report exception content."""
  123. task_id = self.get_success(self.task_scheduler.schedule_task("_raising_task"))
  124. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  125. task = self.get_success(self.task_scheduler.get_task(task_id))
  126. assert task is not None
  127. self.assertEqual(task.status, TaskStatus.FAILED)
  128. self.assertEqual(task.error, "raising")
  129. async def _resumable_task(
  130. self, task: ScheduledTask, first_launch: bool
  131. ) -> Tuple[TaskStatus, Optional[JsonMapping], Optional[str]]:
  132. if task.result and "in_progress" in task.result:
  133. return TaskStatus.COMPLETE, {"success": True}, None
  134. else:
  135. await self.task_scheduler.update_task(task.id, result={"in_progress": True})
  136. # Await forever to simulate an aborted task because of a restart
  137. await deferLater(self.reactor, 2**16, lambda: None)
  138. # This should never been called
  139. return TaskStatus.ACTIVE, None, None
  140. def test_schedule_resumable_task(self) -> None:
  141. """Schedule a resumable task and check that it gets properly resumed and complete after simulating a synapse restart."""
  142. task_id = self.get_success(self.task_scheduler.schedule_task("_resumable_task"))
  143. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  144. task = self.get_success(self.task_scheduler.get_task(task_id))
  145. assert task is not None
  146. self.assertEqual(task.status, TaskStatus.ACTIVE)
  147. # Simulate a synapse restart by emptying the list of running tasks
  148. self.task_scheduler._running_tasks = set()
  149. self.reactor.advance((TaskScheduler.SCHEDULE_INTERVAL_MS / 1000))
  150. task = self.get_success(self.task_scheduler.get_task(task_id))
  151. assert task is not None
  152. self.assertEqual(task.status, TaskStatus.COMPLETE)
  153. assert task.result is not None
  154. self.assertTrue(task.result.get("success"))