Coverage for qreward / utils / scheduler / config.py: 100.00%
72 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"""Configuration for schedule decorator.
3Example:
4 >>> from qreward.utils.scheduler.config import ScheduleConfig
5 >>> config = ScheduleConfig(timeout=30, retry_times=3, retry_interval=2.0)
6 >>> config.timeout
7 30
8"""
10import threading
11import warnings
12from dataclasses import dataclass, field
13from typing import Any, Callable, List, Tuple, Type, Union
15from .priority_queue import Priority
17# Sentinel for distinguishing None from unset
18_sentinel_none = object()
20# Minimum allowed proportion for hedged requests
21MIN_HEDGE_PROPORTION = 1e-6
23# Minimum fallback wait time (seconds) when computed wait is zero or negative
24MIN_WAIT_TIME = 0.01
27@dataclass
28class ScheduleConfig:
29 """Configuration for schedule decorator.
31 Attributes:
32 timeout: Timeout in seconds, 0 means no timeout
33 hedged_request_time: Time to trigger hedged requests in seconds
34 hedged_request_proportion: Max proportion of hedged requests
35 hedged_request_max_times: Max number of hedged requests
36 speed_up_max_multiply: Max speed up multiplier
37 retry_times: Max retry times (not including first call)
38 retry_interval: Retry interval in seconds
39 limit_size: Rate limit size (requests per window)
40 limit_window: Rate limit window in seconds
41 key_func: Function to generate key for pool identification
42 exception_types: Exception types to catch and retry
43 default_result: Default result on failure (callable or value)
44 debug: Enable debug logging
45 adaptive_limit: Enable adaptive rate limiting (default False).
46 adaptive_limit_min: Minimum rate limit floor.
47 adaptive_limit_max: Maximum rate limit ceiling.
48 adaptive_error_threshold: Error rate threshold to trigger slowdown.
49 adaptive_latency_threshold: Avg latency threshold (seconds).
50 adaptive_window_seconds: Sliding window duration for statistics.
51 priority: Default task priority (Priority.HIGH / NORMAL / LOW or 0-9).
52 """
54 timeout: float = 0
55 hedged_request_time: float = 0
56 hedged_request_proportion: float = 0.05
57 hedged_request_max_times: int = 2
58 speed_up_max_multiply: int = 5
59 retry_times: int = 0
60 retry_interval: float = 1
61 limit_size: int = 0
62 limit_window: float = 1.0
63 key_func: Any = field(default=_sentinel_none)
64 exception_types: Union[
65 Type[BaseException], Tuple[Type[BaseException], ...]
66 ] = (BaseException,)
67 default_result: Any = field(default=_sentinel_none)
68 debug: bool = False
69 adaptive_limit: bool = False
70 adaptive_limit_min: int = 10
71 adaptive_limit_max: int = 500
72 adaptive_error_threshold: float = 0.3
73 adaptive_latency_threshold: float = 5.0
74 adaptive_window_seconds: float = 10.0
75 priority: int = Priority.NORMAL
77 def __post_init__(self):
78 # Validate timeout
79 if self.timeout < 0:
80 raise ValueError("timeout must be >= 0")
82 # Validate retry_interval
83 if self.retry_interval < 0:
84 raise ValueError("retry_interval must be >= 0")
86 # Normalize exception_types to tuple
87 if self.exception_types is None:
88 self.exception_types = (BaseException,)
89 elif not isinstance(self.exception_types, tuple):
90 self.exception_types = (self.exception_types,)
92 # Validate hedged_request_proportion
93 if self.hedged_request_time > 0 and (
94 self.hedged_request_proportion <= MIN_HEDGE_PROPORTION
95 or self.hedged_request_proportion > 1.0
96 ):
97 raise ValueError(
98 f"hedged_request_proportion must be in "
99 f"({MIN_HEDGE_PROPORTION}, 1]"
100 )
102 @property
103 def hedged_request_multiply(self) -> float:
104 """Calculate multiply factor for hedged requests."""
105 if self.hedged_request_time > 0 and self.hedged_request_proportion > 0:
106 return 1 / self.hedged_request_proportion - 1
107 return 0
109 def adjust_wait_time(
110 self, basic_wait_time: float, has_wait_time: float,
111 max_wait_time: float
112 ) -> float:
113 """Calculate max wait time considering timeout.
115 Args:
116 basic_wait_time: Basic wait time
117 has_wait_time: Already waited time
118 max_wait_time: Maximum wait time (timeout)
120 Returns:
121 Adjusted wait time
122 """
123 if basic_wait_time < 0:
124 basic_wait_time = MIN_WAIT_TIME
125 if (
126 max_wait_time <= 0
127 or basic_wait_time + has_wait_time < max_wait_time
128 ):
129 return basic_wait_time
130 if has_wait_time > max_wait_time:
131 return MIN_WAIT_TIME
132 return max_wait_time - has_wait_time
134 def get_max_wait_time(
135 self, basic_wait_time: float, has_wait_time: float,
136 max_wait_time: float
137 ) -> float:
138 warnings.warn(
139 "get_max_wait_time is deprecated, use adjust_wait_time instead",
140 DeprecationWarning,
141 stacklevel=2,
142 )
143 return self.adjust_wait_time(
144 basic_wait_time, has_wait_time, max_wait_time
145 )
147 # --- Hot-reload support ---
149 _change_callbacks: List[Callable[["ScheduleConfig"], None]] = field(
150 default_factory=list, repr=False, compare=False,
151 )
152 _update_lock: threading.Lock = field(
153 default_factory=threading.Lock, repr=False, compare=False,
154 )
156 def on_change(self, callback: Callable[["ScheduleConfig"], None]) -> None:
157 """Register a callback invoked after config is updated.
159 Args:
160 callback: Function receiving the updated config instance.
161 """
162 self._change_callbacks.append(callback)
164 def update(self, **kwargs: Any) -> None:
165 """Atomically update config fields and notify listeners.
167 Only known dataclass fields are updated; unknown keys are ignored.
168 Validation is re-run after update.
170 Args:
171 **kwargs: Field name → new value pairs.
172 """
173 with self._update_lock:
174 for key, value in kwargs.items():
175 if hasattr(self, key) and key not in (
176 "_change_callbacks",
177 "_update_lock",
178 ):
179 setattr(self, key, value)
180 self.__post_init__()
181 for cb in self._change_callbacks:
182 cb(self)
184 def snapshot(self) -> dict[str, Any]:
185 """Return a dict snapshot of the current config values."""
186 from dataclasses import fields as dc_fields
187 return {
188 f.name: getattr(self, f.name)
189 for f in dc_fields(self)
190 if not f.name.startswith("_")
191 }