Coverage for qreward / utils / scheduler / base.py: 100.00%
53 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"""Base runner implementing template method pattern for sync/async
2execution."""
4import asyncio
5import concurrent.futures
6import logging
7from abc import ABC, abstractmethod
8from typing import Any, Callable, Optional, Union
10from .config import ScheduleConfig, _sentinel_none
11from .context import ExecutionContext
12from .overload import OverloadChecker
14logger = logging.getLogger(__name__)
16# Cancelled error groups
17_CancelledErrorGroups = (
18 asyncio.CancelledError,
19 asyncio.TimeoutError,
20 concurrent.futures.CancelledError,
21 TimeoutError,
22)
25class BaseRunner(ABC):
26 """Abstract base runner using template method pattern.
28 Subclasses implement the I/O primitives; shared pure-logic helpers
29 live here so that sync and async runners share the same logic.
30 """
32 def __init__(self):
33 self.overload_checker = OverloadChecker()
35 # ------------------------------------------------------------------
36 # Abstract I/O primitives
37 # ------------------------------------------------------------------
39 @abstractmethod
40 def create_task(
41 self, func: Callable, *args, **kwargs
42 ) -> Union[asyncio.Task, concurrent.futures.Future]:
43 """Create and submit a task for execution."""
45 @abstractmethod
46 def get_task_result(
47 self, task: Union[asyncio.Task, concurrent.futures.Future]
48 ) -> Any:
49 """Get result from a completed task."""
51 @abstractmethod
52 def get_task_exception(
53 self, task: Union[asyncio.Task, concurrent.futures.Future]
54 ) -> Optional[BaseException]:
55 """Get exception from a completed task, or None."""
57 @abstractmethod
58 def is_task_cancelled(
59 self, task: Union[asyncio.Task, concurrent.futures.Future]
60 ) -> bool:
61 """Check if a task was cancelled."""
63 # ------------------------------------------------------------------
64 # Shared pure-logic helpers (no I/O, no await)
65 # ------------------------------------------------------------------
67 def _handle_exception(
68 self,
69 context: ExecutionContext,
70 config: ScheduleConfig,
71 finished: Union[asyncio.Task, concurrent.futures.Future],
72 can_add_speed_up: bool,
73 ) -> tuple[bool, bool]:
74 """Handle task exception.
76 Args:
77 context: Execution context.
78 config: Schedule configuration.
79 finished: Completed task with exception.
80 can_add_speed_up: Whether speed can be increased.
82 Returns:
83 Tuple of (is_retryable, did_increase_speed).
84 """
85 exception = self.get_task_exception(finished)
86 context.result_exception = exception
87 context.record_exception(exception)
89 # Never catch system-level exceptions
90 if isinstance(exception, (KeyboardInterrupt, SystemExit)):
91 raise exception
93 if any(isinstance(exception, t) for t in config.exception_types):
94 did_increase = False
95 if can_add_speed_up:
96 context.increase_speed()
97 did_increase = True
99 if self.overload_checker.check(exception):
100 context.reset_speed()
102 return True, did_increase
104 return False, False
106 @staticmethod
107 def _compute_cancel_wait(
108 config: ScheduleConfig, context: ExecutionContext
109 ) -> float:
110 """Compute wait time for cancel_tasks calls."""
111 return config.adjust_wait_time(
112 config.retry_interval, context.elapsed, config.timeout
113 )
115 @staticmethod
116 def _log_finish(context: ExecutionContext) -> None:
117 """Emit debug log on execution finish."""
118 logger.debug(
119 "[schedule] %s execute finish, "
120 "executeTimes: %d, speedUpMultiply: %d, "
121 "consumeTime: %.4f, exceptions: %s",
122 context.func.__qualname__,
123 context.cur_times,
124 context.cur_speed_up_multiply,
125 context.elapsed,
126 context.result_exception_list,
127 )
129 @staticmethod
130 def _return_result(
131 context: ExecutionContext,
132 config: ScheduleConfig,
133 args: tuple,
134 kwargs: dict,
135 ) -> Any:
136 """Return the final result, applying default_result if needed."""
137 if context.result_exception is not None:
138 if config.default_result is not _sentinel_none:
139 if callable(config.default_result):
140 return config.default_result(*args, **kwargs)
141 return config.default_result
142 raise context.result_exception
143 return context.result
146# Re-export AsyncRunner and SyncRunner for backward compatibility.
147from .async_runner import AsyncRunner # noqa: F401, E402
148from .sync_runner import SyncRunner # noqa: F401, E402