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

1"""Base runner implementing template method pattern for sync/async 

2execution.""" 

3 

4import asyncio 

5import concurrent.futures 

6import logging 

7from abc import ABC, abstractmethod 

8from typing import Any, Callable, Optional, Union 

9 

10from .config import ScheduleConfig, _sentinel_none 

11from .context import ExecutionContext 

12from .overload import OverloadChecker 

13 

14logger = logging.getLogger(__name__) 

15 

16# Cancelled error groups 

17_CancelledErrorGroups = ( 

18 asyncio.CancelledError, 

19 asyncio.TimeoutError, 

20 concurrent.futures.CancelledError, 

21 TimeoutError, 

22) 

23 

24 

25class BaseRunner(ABC): 

26 """Abstract base runner using template method pattern. 

27 

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 """ 

31 

32 def __init__(self): 

33 self.overload_checker = OverloadChecker() 

34 

35 # ------------------------------------------------------------------ 

36 # Abstract I/O primitives 

37 # ------------------------------------------------------------------ 

38 

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.""" 

44 

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.""" 

50 

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.""" 

56 

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.""" 

62 

63 # ------------------------------------------------------------------ 

64 # Shared pure-logic helpers (no I/O, no await) 

65 # ------------------------------------------------------------------ 

66 

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. 

75 

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. 

81 

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) 

88 

89 # Never catch system-level exceptions 

90 if isinstance(exception, (KeyboardInterrupt, SystemExit)): 

91 raise exception 

92 

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 

98 

99 if self.overload_checker.check(exception): 

100 context.reset_speed() 

101 

102 return True, did_increase 

103 

104 return False, False 

105 

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 ) 

114 

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 ) 

128 

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 

144 

145 

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