Coverage for qreward / utils / scheduler / context.py: 100.00%
115 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"""Execution context for managing state during a single schedule
2invocation."""
4import time
5from typing import Any, Callable, List, Optional
7from .config import ScheduleConfig
8from .limiter import LimiterPool
9from .metrics import ScheduleMetrics
10from .pools import RunningTaskPool
12# Exponent base and step for hedged request threshold calculation:
13# threshold = pool_size + multiply ** (
14# HEDGE_EXPONENT_BASE + times * HEDGE_EXPONENT_STEP
15# )
16HEDGE_EXPONENT_BASE = 0.5
17HEDGE_EXPONENT_STEP = 0.5
19# Minimum timeout (seconds) returned by get_limiter_timeout to avoid zero-waits
20MIN_LIMITER_TIMEOUT = 0.001
23class ExecutionContext:
24 """Manages execution state for a single schedule invocation."""
26 def __init__(
27 self,
28 func: Callable,
29 config: ScheduleConfig,
30 key: str,
31 running_task_pool: RunningTaskPool,
32 limiter: Optional[LimiterPool],
33 ):
34 """Initialize execution context.
36 Args:
37 func: Function being decorated
38 config: Schedule configuration
39 key: Pool key
40 running_task_pool: Running task pool instance
41 limiter: Rate limiter instance or None
42 """
43 self.func = func
44 self.config = config
45 self.key = key
46 self.running_task_pool = running_task_pool
47 self.limiter = limiter
48 self.priority: int = config.priority
50 # Execution state
51 self.cur_times = 0
52 self.cur_speed_up_multiply = 0
53 self.start_time = time.perf_counter()
54 self.last_submit_time = self.start_time
55 self.cur_hedged_request_times = 1
57 # Results
58 self.result: Any = None
59 self.result_exception: Optional[BaseException] = None
60 self.result_exception_list: List[str] = []
62 # Control
63 self.finish = False
65 def can_submit_task(self, run_tasks_count: int) -> bool:
66 """Check if a new task can be submitted.
68 Args:
69 run_tasks_count: Current number of running tasks
71 Returns:
72 True if can submit
73 """
74 if self.cur_times > self.config.retry_times:
75 return False
77 # Always allow if no running tasks
78 if run_tasks_count <= 0:
79 return True
81 # Allow if under speed up multiplier
82 if run_tasks_count < self.cur_speed_up_multiply:
83 if self.running_task_pool.can_submit(
84 run_tasks_count + 1
85 ):
86 return True
88 # Allow hedged request
89 if self._should_hedge(run_tasks_count):
90 return True
92 return False
94 def _should_hedge(self, run_tasks_count: int) -> bool:
95 """Check if hedged request should be submitted.
97 Args:
98 run_tasks_count: Current number of running tasks
100 Returns:
101 True if should hedge
102 """
103 if self.config.hedged_request_time <= 0:
104 return False
106 if self.cur_hedged_request_times > (
107 self.config.hedged_request_max_times
108 ):
109 return False
111 time_since_last = (
112 time.perf_counter() - self.last_submit_time
113 )
114 if not (
115 self.config.hedged_request_time < time_since_last
116 or self.cur_hedged_request_times > 1
117 ):
118 return False
120 threshold = run_tasks_count + (
121 self.config.hedged_request_multiply ** (
122 HEDGE_EXPONENT_BASE
123 + self.cur_hedged_request_times * HEDGE_EXPONENT_STEP
124 )
125 )
126 return self.running_task_pool.can_submit(threshold)
128 def is_hedge_submit(self, run_tasks_count: int) -> bool:
129 """Check if current submission is a hedge.
131 Args:
132 run_tasks_count: Current number of running tasks
134 Returns:
135 True if this is a hedge submission
136 """
137 if self.config.hedged_request_time <= 0:
138 return False
140 if self.cur_speed_up_multiply > run_tasks_count:
141 return False
143 time_since_last = (
144 time.perf_counter() - self.last_submit_time
145 )
146 if not (
147 self.config.hedged_request_time < time_since_last
148 or self.cur_hedged_request_times > 1
149 ):
150 return False
152 if self.cur_hedged_request_times > (
153 self.config.hedged_request_max_times
154 ):
155 return False
157 threshold = run_tasks_count + (
158 self.config.hedged_request_multiply ** (
159 HEDGE_EXPONENT_BASE
160 + self.cur_hedged_request_times * HEDGE_EXPONENT_STEP
161 )
162 )
163 return self.running_task_pool.can_submit(threshold)
165 def compute_timeout(self, run_tasks_count: int) -> float:
166 """Compute timeout for current wait.
168 Args:
169 run_tasks_count: Current number of running tasks
171 Returns:
172 Timeout in seconds (0 means no timeout)
173 """
174 cur_timeout = 0.0
176 # Base timeout
177 if self.config.timeout > 0:
178 cur_timeout = (
179 self.start_time
180 + self.config.timeout
181 - time.perf_counter()
182 )
184 # Hedge request timeout
185 if (
186 self.cur_hedged_request_times
187 <= self.config.hedged_request_max_times
188 and self.config.hedged_request_time > 0
189 ):
190 hedge_timeout = (
191 self.start_time
192 + self.config.hedged_request_time
193 - time.perf_counter()
194 )
195 if cur_timeout == 0 or hedge_timeout < cur_timeout:
196 cur_timeout = hedge_timeout
198 # Speed up timeout (retry_interval)
199 if run_tasks_count < self.cur_speed_up_multiply:
200 if self.cur_times < self.config.retry_times:
201 if (
202 cur_timeout > self.config.retry_interval
203 or cur_timeout == 0
204 ):
205 cur_timeout = self.config.retry_interval
207 if cur_timeout < 0:
208 cur_timeout = self.config.retry_interval
210 return cur_timeout
212 def get_limiter_timeout(self, run_tasks_count: int) -> float:
213 """Get timeout for rate limiter.
215 Args:
216 run_tasks_count: Current number of running tasks
218 Returns:
219 Timeout in seconds (0 means no timeout)
220 """
221 if run_tasks_count == 0:
222 if self.config.timeout > 0:
223 return max(
224 MIN_LIMITER_TIMEOUT,
225 self.config.timeout - self.elapsed
226 )
227 return 0
229 cur_timeout = self.config.retry_interval
230 if self.config.timeout > 0:
231 remaining = self.config.timeout - self.elapsed
232 if remaining < self.config.retry_interval:
233 cur_timeout = max(
234 MIN_LIMITER_TIMEOUT, remaining
235 )
236 return cur_timeout
238 @property
239 def elapsed(self) -> float:
240 """Get elapsed time since start."""
241 return time.perf_counter() - self.start_time
243 @property
244 def is_timeout(self) -> bool:
245 """Check if execution has timed out."""
246 if self.config.timeout <= 0:
247 return False
248 return self.elapsed > self.config.timeout
250 def record_exception(self, exception: BaseException):
251 """Record exception info."""
252 self.result_exception_list.append(
253 f"{type(exception).__name__} {str(exception)}"
254 )
256 def record_hedge(self):
257 """Record hedge request."""
258 self.result_exception_list.append(f"hedged_request: {self.elapsed}")
259 self.cur_hedged_request_times += 1
261 def mark_task_submitted(self):
262 """Mark that a task was submitted."""
263 self.cur_times += 1
264 self.last_submit_time = time.perf_counter()
266 def can_increase_speed(self) -> bool:
267 """Check if speed can be increased."""
268 return self.cur_speed_up_multiply < self.config.speed_up_max_multiply
270 def increase_speed(self):
271 """Increase speed multiplier."""
272 self.cur_speed_up_multiply += 1
274 def reset_speed(self):
275 """Reset speed multiplier (on overload)."""
276 self.cur_speed_up_multiply = 0
278 def build_metrics(self) -> ScheduleMetrics:
279 """Build execution metrics from the current context state.
281 Returns:
282 ScheduleMetrics with aggregated execution statistics.
283 """
284 total_latency_ms = self.elapsed * 1000.0
285 retry_count = max(0, self.cur_times - 1)
286 failure_count = len(self.result_exception_list)
287 success_count = 1 if self.result_exception is None else 0
288 total_calls = self.cur_times
289 avg_latency_ms = (
290 total_latency_ms / total_calls if total_calls > 0 else 0.0
291 )
292 return ScheduleMetrics(
293 total_calls=total_calls,
294 success_count=success_count,
295 failure_count=failure_count,
296 retry_count=retry_count,
297 total_latency_ms=total_latency_ms,
298 avg_latency_ms=avg_latency_ms,
299 )