Coverage for qreward / utils / scheduler / async_runner.py: 100.00%
59 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"""Async runner implementation for asynchronous task execution."""
3import asyncio
4import logging
5from typing import Any, Callable, List, Optional, Sequence
7from .base import BaseRunner, _CancelledErrorGroups
8from .config import ScheduleConfig
9from .context import ExecutionContext
11logger = logging.getLogger(__name__)
14class AsyncRunner(BaseRunner):
15 """Async runner implementation."""
17 def create_task(
18 self, func: Callable, *args, **kwargs
19 ) -> asyncio.Task:
20 """Create an asyncio task."""
21 return asyncio.create_task(func(*args, **kwargs))
23 def get_task_result(
24 self, task: asyncio.Task
25 ) -> Any:
26 """Get result from a completed asyncio task."""
27 return task.result()
29 def get_task_exception(
30 self, task: asyncio.Task
31 ) -> Optional[BaseException]:
32 """Get exception from a completed asyncio task."""
33 return task.exception()
35 def is_task_cancelled(
36 self, task: asyncio.Task
37 ) -> bool:
38 """Check if an asyncio task was cancelled."""
39 return task.cancelled()
41 async def _cancel_tasks(
42 self,
43 pending: Sequence[asyncio.Task],
44 done: List[asyncio.Task],
45 retry_interval: float,
46 ) -> None: # pragma: no cover
47 """Cancel pending tasks and consume done tasks.
49 Args:
50 pending: Pending tasks to cancel.
51 done: Done tasks to consume.
52 retry_interval: Time to wait for cancellation.
53 """
54 while done:
55 try:
56 done.pop()
57 except _CancelledErrorGroups:
58 pass
60 if pending:
61 for task in pending:
62 if not task.done():
63 task.cancel()
64 try:
65 await asyncio.wait_for(
66 asyncio.gather(*pending, return_exceptions=True),
67 timeout=retry_interval if retry_interval > 0 else None,
68 )
69 except _CancelledErrorGroups:
70 pass
72 async def execute_impl(
73 self,
74 context: ExecutionContext,
75 config: ScheduleConfig,
76 *args: Any,
77 **kwargs: Any,
78 ) -> Any:
79 """Async execution implementation.
81 Args:
82 context: Execution context.
83 config: Schedule configuration.
84 *args: Function arguments.
85 **kwargs: Function keyword arguments.
87 Returns:
88 Function result.
89 """
90 run_tasks: List[asyncio.Task] = []
92 try:
93 while not context.finish and (
94 context.cur_times <= config.retry_times or len(run_tasks) > 0
95 ):
96 # 1. Submit new tasks if allowed
97 if context.can_submit_task(len(run_tasks)):
98 limiter_timeout = context.get_limiter_timeout(
99 len(run_tasks)
100 )
102 if not context.limiter or await (
103 context.limiter.async_allow(
104 limiter_timeout if limiter_timeout > 0 else None
105 )
106 ):
107 if context.is_hedge_submit(
108 len(run_tasks)
109 ): # pragma: no cover
110 context.record_hedge()
111 elif (
112 context.result_exception is not None
113 ): # pragma: no cover
114 context.record_exception(context.result_exception)
115 context.result_exception = None
117 run_tasks.append(
118 asyncio.create_task(context.func(*args, **kwargs))
119 )
120 context.mark_task_submitted()
122 # 2. Wait for tasks
123 done, pending = set(), set()
124 if run_tasks: # pragma: no branch
125 cur_timeout = context.compute_timeout(len(run_tasks))
126 if cur_timeout > 0:
127 done, pending = await asyncio.wait(
128 run_tasks,
129 timeout=cur_timeout,
130 return_when=asyncio.FIRST_COMPLETED,
131 )
132 else: # pragma: no cover
133 done, pending = await asyncio.wait(
134 run_tasks, return_when=asyncio.FIRST_COMPLETED
135 )
137 # 3. Process results
138 can_add_speed_up = context.can_increase_speed()
139 cancel_wait = self._compute_cancel_wait(config, context)
141 for finished in list(done):
142 if finished.cancelled(): # pragma: no cover
143 run_tasks.remove(finished)
144 continue
146 exception = finished.exception()
147 if exception is None:
148 # Success
149 await self._cancel_tasks(
150 list(pending), list(done), cancel_wait
151 )
152 context.result = finished.result()
153 context.result_exception = None
154 context.finish = True
155 run_tasks.remove(finished)
156 return context.result
158 # Handle exception
159 run_tasks.remove(finished)
160 retryable, increased = self._handle_exception(
161 context, config, finished, can_add_speed_up
162 )
163 if increased:
164 can_add_speed_up = False
166 if retryable:
167 await asyncio.sleep(cancel_wait)
168 break
169 else:
170 await self._cancel_tasks(
171 list(pending), list(done), cancel_wait
172 )
173 context.finish = True
174 break
176 # 4. Check timeout
177 if config.timeout > 0 and context.elapsed > config.timeout:
178 context.result_exception = asyncio.TimeoutError(
179 f"execute more than {config.timeout} seconds"
180 )
181 await self._cancel_tasks(
182 list(pending) + run_tasks, [], cancel_wait
183 )
184 context.finish = True
186 # 5. Return result
187 return self._return_result(context, config, args, kwargs)
189 finally:
190 context.running_task_pool.add(-1)
191 if config.debug:
192 self._log_finish(context)