Coverage for qreward / utils / scheduler / sync_runner.py: 100.00%
66 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-23 19:32 +0000
1"""Sync runner module for synchronous task execution."""
3import concurrent.futures
4import logging
5import time
6from typing import Any, Callable, List, Optional, Sequence
8from .base import BaseRunner, _CancelledErrorGroups
9from .config import ScheduleConfig
10from .context import ExecutionContext
12logger = logging.getLogger(__name__)
15class SyncRunner(BaseRunner):
16 """Synchronous runner for executing scheduled tasks.
18 Uses ThreadPoolExecutor to execute tasks synchronously,
19 supporting retry, hedge, and rate limiting features.
20 """
22 def __init__(self):
23 super().__init__()
24 self._executor: Optional[
25 concurrent.futures.ThreadPoolExecutor
26 ] = None
28 def set_executor(
29 self, executor: concurrent.futures.ThreadPoolExecutor
30 ):
31 """Set the thread pool executor.
33 Args:
34 executor: Thread pool executor for task execution.
35 """
36 self._executor = executor
38 def create_task(
39 self, func: Callable, *args, **kwargs
40 ) -> concurrent.futures.Future:
41 """Create and submit a task to the executor."""
42 if self._executor is None:
43 raise RuntimeError("Executor not set")
44 return self._executor.submit(func, *args, **kwargs)
46 def get_task_result(
47 self, task: concurrent.futures.Future
48 ) -> Any:
49 """Get the result of a completed task."""
50 return task.result()
52 def get_task_exception(
53 self, task: concurrent.futures.Future
54 ) -> Optional[BaseException]:
55 """Get the exception from a failed task, or None."""
56 return task.exception()
58 def is_task_cancelled(
59 self, task: concurrent.futures.Future
60 ) -> bool:
61 """Check if a task was cancelled."""
62 return task.cancelled()
64 def _cancel_tasks(
65 self,
66 pending: Sequence[concurrent.futures.Future],
67 done: List,
68 retry_interval: float,
69 ) -> None: # pragma: no cover
70 """Cancel pending tasks and consume done tasks."""
71 while done:
72 try:
73 done.pop()
74 except _CancelledErrorGroups:
75 pass
77 for task in pending:
78 if not task.done():
79 task.cancel()
81 if pending:
82 try:
83 concurrent.futures.wait(
84 pending,
85 timeout=retry_interval,
86 return_when=concurrent.futures.ALL_COMPLETED,
87 )
88 except _CancelledErrorGroups:
89 pass
91 def execute_impl(
92 self,
93 context: ExecutionContext,
94 config: ScheduleConfig,
95 *args: Any,
96 **kwargs: Any,
97 ) -> Any:
98 """Sync execution implementation."""
99 run_tasks: List[concurrent.futures.Future] = []
101 try:
102 while not context.finish and (
103 context.cur_times <= config.retry_times or len(run_tasks) > 0
104 ):
105 # 1. Submit new tasks if allowed
106 if context.can_submit_task(len(run_tasks)):
107 limiter_timeout = context.get_limiter_timeout(
108 len(run_tasks)
109 )
111 if not context.limiter or context.limiter.allow(
112 limiter_timeout if limiter_timeout > 0 else None
113 ):
114 if context.is_hedge_submit(
115 len(run_tasks)
116 ): # pragma: no cover
117 context.record_hedge()
118 elif (
119 context.result_exception is not None
120 ): # pragma: no cover
121 context.record_exception(context.result_exception)
122 context.result_exception = None
124 run_tasks.append(
125 self._executor.submit(
126 context.func, *args, **kwargs
127 )
128 )
129 context.mark_task_submitted()
131 # 2. Wait for tasks
132 done, pending = set(), set()
133 if run_tasks: # pragma: no branch
134 cur_timeout = context.compute_timeout(len(run_tasks))
135 if cur_timeout > 0:
136 done, pending = concurrent.futures.wait(
137 run_tasks,
138 timeout=cur_timeout,
139 return_when=concurrent.futures.FIRST_COMPLETED,
140 )
141 else: # pragma: no cover
142 done, pending = concurrent.futures.wait(
143 run_tasks,
144 return_when=concurrent.futures.FIRST_COMPLETED,
145 )
147 # 3. Process results
148 can_add_speed_up = context.can_increase_speed()
149 cancel_wait = self._compute_cancel_wait(config, context)
151 for finished in list(done):
152 run_tasks.remove(finished)
154 if finished.cancelled(): # pragma: no cover
155 continue
157 exception = finished.exception()
158 if exception is None:
159 # Success
160 self._cancel_tasks(
161 list(pending), list(done), cancel_wait
162 )
163 context.result = finished.result()
164 context.result_exception = None
165 context.finish = True
166 break
168 # Handle exception
169 retryable, increased = self._handle_exception(
170 context, config, finished, can_add_speed_up
171 )
172 if increased:
173 can_add_speed_up = False
175 if retryable:
176 time.sleep(cancel_wait)
177 break
178 else:
179 self._cancel_tasks(
180 list(pending), [], cancel_wait
181 )
182 context.finish = True
183 break
185 # 4. Check timeout
186 if config.timeout > 0 and context.elapsed > config.timeout:
187 context.result_exception = TimeoutError(
188 f"execute more than {config.timeout} seconds"
189 )
190 self._cancel_tasks(
191 list(pending) + run_tasks, [], cancel_wait
192 )
193 context.finish = True
195 # 5. Return result
196 return self._return_result(context, config, args, kwargs)
198 finally:
199 context.running_task_pool.add(-1)
200 if config.debug:
201 self._log_finish(context)