unrealon 1.1.5__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unrealon-1.1.5.dist-info/licenses → unrealon-2.0.4.dist-info}/LICENSE +1 -1
- unrealon-2.0.4.dist-info/METADATA +491 -0
- unrealon-2.0.4.dist-info/RECORD +129 -0
- {unrealon-1.1.5.dist-info → unrealon-2.0.4.dist-info}/WHEEL +2 -1
- unrealon-2.0.4.dist-info/entry_points.txt +3 -0
- unrealon-2.0.4.dist-info/top_level.txt +3 -0
- unrealon_browser/__init__.py +5 -2
- unrealon_browser/cli/browser_cli.py +18 -9
- unrealon_browser/cli/interactive_mode.py +18 -7
- unrealon_browser/core/browser_manager.py +76 -13
- unrealon_browser/dto/__init__.py +21 -0
- unrealon_browser/dto/bot_detection.py +175 -0
- unrealon_browser/dto/models/config.py +14 -1
- unrealon_browser/managers/__init__.py +4 -1
- unrealon_browser/managers/logger_bridge.py +3 -6
- unrealon_browser/managers/page_wait_manager.py +198 -0
- unrealon_browser/stealth/__init__.py +27 -0
- unrealon_browser/stealth/bypass_techniques.pyc +0 -0
- unrealon_browser/stealth/manager.pyc +0 -0
- unrealon_browser/stealth/nodriver_stealth.pyc +0 -0
- unrealon_browser/stealth/playwright_stealth.pyc +0 -0
- unrealon_browser/stealth/scanner_tester.pyc +0 -0
- unrealon_browser/stealth/undetected_chrome.pyc +0 -0
- unrealon_core/__init__.py +160 -0
- unrealon_core/config/__init__.py +16 -0
- unrealon_core/config/environment.py +98 -0
- unrealon_core/config/urls.py +93 -0
- unrealon_core/enums/__init__.py +24 -0
- unrealon_core/enums/status.py +216 -0
- unrealon_core/enums/types.py +240 -0
- unrealon_core/error_handling/__init__.py +45 -0
- unrealon_core/error_handling/circuit_breaker.py +292 -0
- unrealon_core/error_handling/error_context.py +324 -0
- unrealon_core/error_handling/recovery.py +371 -0
- unrealon_core/error_handling/retry.py +268 -0
- unrealon_core/exceptions/__init__.py +46 -0
- unrealon_core/exceptions/base.py +292 -0
- unrealon_core/exceptions/communication.py +22 -0
- unrealon_core/exceptions/driver.py +11 -0
- unrealon_core/exceptions/proxy.py +11 -0
- unrealon_core/exceptions/task.py +12 -0
- unrealon_core/exceptions/validation.py +17 -0
- unrealon_core/models/__init__.py +98 -0
- unrealon_core/models/arq_context.py +252 -0
- unrealon_core/models/arq_responses.py +125 -0
- unrealon_core/models/base.py +291 -0
- unrealon_core/models/bridge_stats.py +58 -0
- unrealon_core/models/communication.py +39 -0
- unrealon_core/models/config.py +47 -0
- unrealon_core/models/connection_stats.py +47 -0
- unrealon_core/models/driver.py +30 -0
- unrealon_core/models/driver_details.py +98 -0
- unrealon_core/models/logging.py +28 -0
- unrealon_core/models/task.py +21 -0
- unrealon_core/models/typed_responses.py +210 -0
- unrealon_core/models/websocket/__init__.py +91 -0
- unrealon_core/models/websocket/base.py +49 -0
- unrealon_core/models/websocket/config.py +200 -0
- unrealon_core/models/websocket/driver.py +215 -0
- unrealon_core/models/websocket/errors.py +138 -0
- unrealon_core/models/websocket/heartbeat.py +100 -0
- unrealon_core/models/websocket/logging.py +261 -0
- unrealon_core/models/websocket/proxy.py +496 -0
- unrealon_core/models/websocket/tasks.py +275 -0
- unrealon_core/models/websocket/utils.py +153 -0
- unrealon_core/models/websocket_session.py +144 -0
- unrealon_core/monitoring/__init__.py +43 -0
- unrealon_core/monitoring/alerts.py +398 -0
- unrealon_core/monitoring/dashboard.py +307 -0
- unrealon_core/monitoring/health_check.py +354 -0
- unrealon_core/monitoring/metrics.py +352 -0
- unrealon_core/utils/__init__.py +11 -0
- unrealon_core/utils/time.py +61 -0
- unrealon_core/version.py +219 -0
- unrealon_driver/__init__.py +88 -50
- unrealon_driver/core_module/__init__.py +34 -0
- unrealon_driver/core_module/base.py +184 -0
- unrealon_driver/core_module/config.py +30 -0
- unrealon_driver/core_module/event_manager.py +127 -0
- unrealon_driver/core_module/protocols.py +98 -0
- unrealon_driver/core_module/registry.py +146 -0
- unrealon_driver/decorators/__init__.py +15 -0
- unrealon_driver/decorators/retry.py +117 -0
- unrealon_driver/decorators/schedule.py +137 -0
- unrealon_driver/decorators/task.py +61 -0
- unrealon_driver/decorators/timing.py +132 -0
- unrealon_driver/driver/__init__.py +20 -0
- unrealon_driver/driver/communication/__init__.py +10 -0
- unrealon_driver/driver/communication/session.py +203 -0
- unrealon_driver/driver/communication/websocket_client.py +197 -0
- unrealon_driver/driver/core/__init__.py +10 -0
- unrealon_driver/driver/core/config.py +85 -0
- unrealon_driver/driver/core/driver.py +221 -0
- unrealon_driver/driver/factory/__init__.py +9 -0
- unrealon_driver/driver/factory/manager_factory.py +130 -0
- unrealon_driver/driver/lifecycle/__init__.py +11 -0
- unrealon_driver/driver/lifecycle/daemon.py +76 -0
- unrealon_driver/driver/lifecycle/initialization.py +97 -0
- unrealon_driver/driver/lifecycle/shutdown.py +48 -0
- unrealon_driver/driver/monitoring/__init__.py +9 -0
- unrealon_driver/driver/monitoring/health.py +63 -0
- unrealon_driver/driver/utilities/__init__.py +10 -0
- unrealon_driver/driver/utilities/logging.py +51 -0
- unrealon_driver/driver/utilities/serialization.py +61 -0
- unrealon_driver/managers/__init__.py +32 -0
- unrealon_driver/managers/base.py +174 -0
- unrealon_driver/managers/browser.py +98 -0
- unrealon_driver/managers/cache.py +116 -0
- unrealon_driver/managers/http.py +107 -0
- unrealon_driver/managers/logger.py +286 -0
- unrealon_driver/managers/proxy.py +99 -0
- unrealon_driver/managers/registry.py +87 -0
- unrealon_driver/managers/threading.py +54 -0
- unrealon_driver/managers/update.py +107 -0
- unrealon_driver/utils/__init__.py +9 -0
- unrealon_driver/utils/time.py +10 -0
- unrealon/__init__.py +0 -40
- unrealon-1.1.5.dist-info/METADATA +0 -621
- unrealon-1.1.5.dist-info/RECORD +0 -54
- unrealon-1.1.5.dist-info/entry_points.txt +0 -9
- unrealon_browser/managers/stealth.py +0 -388
- unrealon_driver/exceptions.py +0 -33
- unrealon_driver/html_analyzer/__init__.py +0 -32
- unrealon_driver/html_analyzer/cleaner.py +0 -657
- unrealon_driver/html_analyzer/config.py +0 -64
- unrealon_driver/html_analyzer/manager.py +0 -247
- unrealon_driver/html_analyzer/models.py +0 -115
- unrealon_driver/html_analyzer/websocket_analyzer.py +0 -157
- unrealon_driver/models/__init__.py +0 -31
- unrealon_driver/models/websocket.py +0 -98
- unrealon_driver/parser/__init__.py +0 -36
- unrealon_driver/parser/cli_manager.py +0 -142
- unrealon_driver/parser/daemon_manager.py +0 -403
- unrealon_driver/parser/managers/__init__.py +0 -25
- unrealon_driver/parser/managers/config.py +0 -293
- unrealon_driver/parser/managers/error.py +0 -412
- unrealon_driver/parser/managers/result.py +0 -321
- unrealon_driver/parser/parser_manager.py +0 -458
- unrealon_driver/smart_logging/__init__.py +0 -24
- unrealon_driver/smart_logging/models.py +0 -44
- unrealon_driver/smart_logging/smart_logger.py +0 -406
- unrealon_driver/smart_logging/unified_logger.py +0 -525
- unrealon_driver/websocket/__init__.py +0 -31
- unrealon_driver/websocket/client.py +0 -249
- unrealon_driver/websocket/config.py +0 -188
- unrealon_driver/websocket/manager.py +0 -90
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recovery System
|
|
3
|
+
|
|
4
|
+
Automatic error recovery and healing mechanisms.
|
|
5
|
+
Following critical requirements - max 500 lines, functions < 20 lines.
|
|
6
|
+
|
|
7
|
+
Phase 2: Core Systems - Error Handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
17
|
+
|
|
18
|
+
from .error_context import ErrorContext, ErrorSeverity
|
|
19
|
+
from ..utils.time import utc_now
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RecoveryStrategy(str, Enum):
|
|
26
|
+
"""Recovery strategy types."""
|
|
27
|
+
RETRY = "retry"
|
|
28
|
+
FALLBACK = "fallback"
|
|
29
|
+
CIRCUIT_BREAK = "circuit_break"
|
|
30
|
+
GRACEFUL_DEGRADE = "graceful_degrade"
|
|
31
|
+
RESTART = "restart"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RecoveryAction(BaseModel):
|
|
35
|
+
"""Recovery action configuration."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
validate_assignment=True,
|
|
39
|
+
extra="forbid"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
strategy: RecoveryStrategy = Field(description="Recovery strategy to use")
|
|
43
|
+
max_attempts: int = Field(default=3, ge=1, le=10, description="Maximum recovery attempts")
|
|
44
|
+
delay_seconds: float = Field(default=1.0, ge=0.1, le=60.0, description="Delay between attempts")
|
|
45
|
+
timeout_seconds: float = Field(default=30.0, ge=1.0, le=300.0, description="Recovery timeout")
|
|
46
|
+
fallback_value: Optional[Any] = Field(default=None, description="Fallback value to return")
|
|
47
|
+
enabled: bool = Field(default=True, description="Whether recovery is enabled")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class RecoveryResult(BaseModel):
|
|
51
|
+
"""Result of recovery attempt."""
|
|
52
|
+
|
|
53
|
+
model_config = ConfigDict(
|
|
54
|
+
validate_assignment=True,
|
|
55
|
+
extra="forbid"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
success: bool = Field(description="Whether recovery succeeded")
|
|
59
|
+
strategy_used: RecoveryStrategy = Field(description="Strategy that was used")
|
|
60
|
+
attempts_made: int = Field(description="Number of recovery attempts")
|
|
61
|
+
duration_seconds: float = Field(description="Total recovery duration")
|
|
62
|
+
result: Optional[Any] = Field(default=None, description="Recovery result")
|
|
63
|
+
error_message: Optional[str] = Field(default=None, description="Error if recovery failed")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AutoRecovery:
|
|
67
|
+
"""
|
|
68
|
+
Automatic recovery system.
|
|
69
|
+
|
|
70
|
+
Provides intelligent error recovery based on error context
|
|
71
|
+
and configured recovery strategies.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
"""Initialize auto recovery system."""
|
|
76
|
+
self._recovery_actions: Dict[str, RecoveryAction] = {}
|
|
77
|
+
self._recovery_stats: Dict[str, Dict[str, int]] = {}
|
|
78
|
+
self.logger = logging.getLogger("auto_recovery")
|
|
79
|
+
|
|
80
|
+
def register_recovery_action(
|
|
81
|
+
self,
|
|
82
|
+
error_type: str,
|
|
83
|
+
action: RecoveryAction
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Register recovery action for error type.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
error_type: Exception class name
|
|
90
|
+
action: Recovery action configuration
|
|
91
|
+
"""
|
|
92
|
+
self._recovery_actions[error_type] = action
|
|
93
|
+
self._recovery_stats[error_type] = {
|
|
94
|
+
'attempts': 0,
|
|
95
|
+
'successes': 0,
|
|
96
|
+
'failures': 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.logger.info(f"Registered recovery action for {error_type}: {action.strategy}")
|
|
100
|
+
|
|
101
|
+
async def attempt_recovery(
|
|
102
|
+
self,
|
|
103
|
+
error_context: ErrorContext,
|
|
104
|
+
operation_func: Callable[..., Any],
|
|
105
|
+
*args,
|
|
106
|
+
**kwargs
|
|
107
|
+
) -> RecoveryResult:
|
|
108
|
+
"""
|
|
109
|
+
Attempt to recover from error.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
error_context: Context of the error
|
|
113
|
+
operation_func: Function to retry
|
|
114
|
+
*args, **kwargs: Function arguments
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
RecoveryResult with outcome
|
|
118
|
+
"""
|
|
119
|
+
start_time = utc_now()
|
|
120
|
+
error_type = error_context.error_type
|
|
121
|
+
|
|
122
|
+
# Get recovery action for this error type
|
|
123
|
+
action = self._recovery_actions.get(error_type)
|
|
124
|
+
if not action or not action.enabled:
|
|
125
|
+
self.logger.debug(f"No recovery action configured for {error_type}")
|
|
126
|
+
return RecoveryResult(
|
|
127
|
+
success=False,
|
|
128
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
129
|
+
attempts_made=0,
|
|
130
|
+
duration_seconds=0.0,
|
|
131
|
+
error_message="No recovery action configured"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Update stats
|
|
135
|
+
self._recovery_stats[error_type]['attempts'] += 1
|
|
136
|
+
|
|
137
|
+
# Attempt recovery based on strategy
|
|
138
|
+
try:
|
|
139
|
+
if action.strategy == RecoveryStrategy.RETRY:
|
|
140
|
+
result = await self._retry_recovery(action, operation_func, *args, **kwargs)
|
|
141
|
+
elif action.strategy == RecoveryStrategy.FALLBACK:
|
|
142
|
+
result = await self._fallback_recovery(action, operation_func, *args, **kwargs)
|
|
143
|
+
elif action.strategy == RecoveryStrategy.GRACEFUL_DEGRADE:
|
|
144
|
+
result = await self._graceful_degrade_recovery(action, operation_func, *args, **kwargs)
|
|
145
|
+
else:
|
|
146
|
+
result = RecoveryResult(
|
|
147
|
+
success=False,
|
|
148
|
+
strategy_used=action.strategy,
|
|
149
|
+
attempts_made=0,
|
|
150
|
+
duration_seconds=0.0,
|
|
151
|
+
error_message=f"Recovery strategy {action.strategy} not implemented"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Update stats
|
|
155
|
+
if result.success:
|
|
156
|
+
self._recovery_stats[error_type]['successes'] += 1
|
|
157
|
+
else:
|
|
158
|
+
self._recovery_stats[error_type]['failures'] += 1
|
|
159
|
+
|
|
160
|
+
# Calculate duration
|
|
161
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
162
|
+
result.duration_seconds = duration
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self.logger.error(f"Recovery attempt failed: {e}")
|
|
168
|
+
self._recovery_stats[error_type]['failures'] += 1
|
|
169
|
+
|
|
170
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
171
|
+
return RecoveryResult(
|
|
172
|
+
success=False,
|
|
173
|
+
strategy_used=action.strategy,
|
|
174
|
+
attempts_made=1,
|
|
175
|
+
duration_seconds=duration,
|
|
176
|
+
error_message=str(e)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def _retry_recovery(
|
|
180
|
+
self,
|
|
181
|
+
action: RecoveryAction,
|
|
182
|
+
operation_func: Callable[..., Any],
|
|
183
|
+
*args,
|
|
184
|
+
**kwargs
|
|
185
|
+
) -> RecoveryResult:
|
|
186
|
+
"""Attempt recovery using retry strategy."""
|
|
187
|
+
for attempt in range(action.max_attempts):
|
|
188
|
+
try:
|
|
189
|
+
self.logger.debug(f"Recovery retry attempt {attempt + 1}/{action.max_attempts}")
|
|
190
|
+
|
|
191
|
+
result = await asyncio.wait_for(
|
|
192
|
+
operation_func(*args, **kwargs),
|
|
193
|
+
timeout=action.timeout_seconds
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.logger.info(f"Recovery succeeded on attempt {attempt + 1}")
|
|
197
|
+
return RecoveryResult(
|
|
198
|
+
success=True,
|
|
199
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
200
|
+
attempts_made=attempt + 1,
|
|
201
|
+
duration_seconds=0.0, # Will be set by caller
|
|
202
|
+
result=result
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
if attempt < action.max_attempts - 1:
|
|
207
|
+
self.logger.warning(f"Recovery attempt {attempt + 1} failed: {e}")
|
|
208
|
+
await asyncio.sleep(action.delay_seconds)
|
|
209
|
+
else:
|
|
210
|
+
self.logger.error(f"All recovery attempts failed: {e}")
|
|
211
|
+
|
|
212
|
+
return RecoveryResult(
|
|
213
|
+
success=False,
|
|
214
|
+
strategy_used=RecoveryStrategy.RETRY,
|
|
215
|
+
attempts_made=action.max_attempts,
|
|
216
|
+
duration_seconds=0.0,
|
|
217
|
+
error_message="All retry attempts failed"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def _fallback_recovery(
|
|
221
|
+
self,
|
|
222
|
+
action: RecoveryAction,
|
|
223
|
+
operation_func: Callable[..., Any],
|
|
224
|
+
*args,
|
|
225
|
+
**kwargs
|
|
226
|
+
) -> RecoveryResult:
|
|
227
|
+
"""Attempt recovery using fallback strategy."""
|
|
228
|
+
self.logger.info("Using fallback recovery strategy")
|
|
229
|
+
|
|
230
|
+
return RecoveryResult(
|
|
231
|
+
success=True,
|
|
232
|
+
strategy_used=RecoveryStrategy.FALLBACK,
|
|
233
|
+
attempts_made=1,
|
|
234
|
+
duration_seconds=0.0,
|
|
235
|
+
result=action.fallback_value
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
async def _graceful_degrade_recovery(
|
|
239
|
+
self,
|
|
240
|
+
action: RecoveryAction,
|
|
241
|
+
operation_func: Callable[..., Any],
|
|
242
|
+
*args,
|
|
243
|
+
**kwargs
|
|
244
|
+
) -> RecoveryResult:
|
|
245
|
+
"""Attempt recovery using graceful degradation."""
|
|
246
|
+
self.logger.info("Using graceful degradation recovery strategy")
|
|
247
|
+
|
|
248
|
+
# Return a simplified/degraded version of the expected result
|
|
249
|
+
degraded_result = {
|
|
250
|
+
"status": "degraded",
|
|
251
|
+
"message": "Service operating in degraded mode",
|
|
252
|
+
"data": action.fallback_value
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return RecoveryResult(
|
|
256
|
+
success=True,
|
|
257
|
+
strategy_used=RecoveryStrategy.GRACEFUL_DEGRADE,
|
|
258
|
+
attempts_made=1,
|
|
259
|
+
duration_seconds=0.0,
|
|
260
|
+
result=degraded_result
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def get_recovery_stats(self) -> Dict[str, Dict[str, Any]]:
|
|
264
|
+
"""Get recovery statistics."""
|
|
265
|
+
stats = {}
|
|
266
|
+
|
|
267
|
+
for error_type, counts in self._recovery_stats.items():
|
|
268
|
+
total_attempts = counts['attempts']
|
|
269
|
+
success_rate = 0.0
|
|
270
|
+
|
|
271
|
+
if total_attempts > 0:
|
|
272
|
+
success_rate = (counts['successes'] / total_attempts) * 100.0
|
|
273
|
+
|
|
274
|
+
stats[error_type] = {
|
|
275
|
+
'total_attempts': total_attempts,
|
|
276
|
+
'successes': counts['successes'],
|
|
277
|
+
'failures': counts['failures'],
|
|
278
|
+
'success_rate_percent': round(success_rate, 2),
|
|
279
|
+
'recovery_action': self._recovery_actions.get(error_type, {})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return stats
|
|
283
|
+
|
|
284
|
+
def clear_stats(self) -> None:
|
|
285
|
+
"""Clear recovery statistics."""
|
|
286
|
+
for error_type in self._recovery_stats:
|
|
287
|
+
self._recovery_stats[error_type] = {
|
|
288
|
+
'attempts': 0,
|
|
289
|
+
'successes': 0,
|
|
290
|
+
'failures': 0
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
self.logger.info("Recovery statistics cleared")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Global auto recovery instance
|
|
297
|
+
_auto_recovery = AutoRecovery()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_auto_recovery() -> AutoRecovery:
|
|
301
|
+
"""Get global auto recovery instance."""
|
|
302
|
+
return _auto_recovery
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def recovery_handler(
|
|
306
|
+
error_type: str,
|
|
307
|
+
strategy: RecoveryStrategy = RecoveryStrategy.RETRY,
|
|
308
|
+
max_attempts: int = 3,
|
|
309
|
+
delay_seconds: float = 1.0,
|
|
310
|
+
fallback_value: Optional[Any] = None
|
|
311
|
+
):
|
|
312
|
+
"""
|
|
313
|
+
Decorator to add automatic recovery to functions.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
error_type: Exception class name to handle
|
|
317
|
+
strategy: Recovery strategy to use
|
|
318
|
+
max_attempts: Maximum recovery attempts
|
|
319
|
+
delay_seconds: Delay between attempts
|
|
320
|
+
fallback_value: Fallback value for fallback strategy
|
|
321
|
+
"""
|
|
322
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
323
|
+
# Register recovery action
|
|
324
|
+
action = RecoveryAction(
|
|
325
|
+
strategy=strategy,
|
|
326
|
+
max_attempts=max_attempts,
|
|
327
|
+
delay_seconds=delay_seconds,
|
|
328
|
+
fallback_value=fallback_value
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
_auto_recovery.register_recovery_action(error_type, action)
|
|
332
|
+
|
|
333
|
+
async def async_wrapper(*args, **kwargs):
|
|
334
|
+
try:
|
|
335
|
+
return await func(*args, **kwargs)
|
|
336
|
+
except Exception as e:
|
|
337
|
+
from .error_context import create_error_context
|
|
338
|
+
|
|
339
|
+
# Create error context
|
|
340
|
+
error_context = create_error_context(
|
|
341
|
+
error=e,
|
|
342
|
+
operation=func.__name__,
|
|
343
|
+
component=func.__module__
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Attempt recovery
|
|
347
|
+
recovery_result = await _auto_recovery.attempt_recovery(
|
|
348
|
+
error_context, func, *args, **kwargs
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if recovery_result.success:
|
|
352
|
+
return recovery_result.result
|
|
353
|
+
else:
|
|
354
|
+
# Re-raise original exception if recovery failed
|
|
355
|
+
raise
|
|
356
|
+
|
|
357
|
+
def sync_wrapper(*args, **kwargs):
|
|
358
|
+
# For sync functions, convert to async temporarily
|
|
359
|
+
import asyncio
|
|
360
|
+
try:
|
|
361
|
+
loop = asyncio.get_event_loop()
|
|
362
|
+
return loop.run_until_complete(async_wrapper(*args, **kwargs))
|
|
363
|
+
except RuntimeError:
|
|
364
|
+
return asyncio.run(async_wrapper(*args, **kwargs))
|
|
365
|
+
|
|
366
|
+
if asyncio.iscoroutinefunction(func):
|
|
367
|
+
return async_wrapper
|
|
368
|
+
else:
|
|
369
|
+
return sync_wrapper
|
|
370
|
+
|
|
371
|
+
return decorator
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retry System
|
|
3
|
+
|
|
4
|
+
Advanced retry logic with multiple backoff strategies.
|
|
5
|
+
Following critical requirements - max 500 lines, functions < 20 lines.
|
|
6
|
+
|
|
7
|
+
Phase 2: Core Systems - Error Handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import random
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Any, Callable, Optional, Type, Union, List
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
19
|
+
|
|
20
|
+
from ..exceptions.base import UnrealOnError
|
|
21
|
+
from ..utils.time import utc_now
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RetryStrategy(str, Enum):
|
|
28
|
+
"""Retry strategy types."""
|
|
29
|
+
EXPONENTIAL = "exponential"
|
|
30
|
+
LINEAR = "linear"
|
|
31
|
+
FIXED = "fixed"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RetryResult(BaseModel):
|
|
35
|
+
"""Result of retry operation."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
validate_assignment=True,
|
|
39
|
+
extra="forbid"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
success: bool = Field(description="Whether operation succeeded")
|
|
43
|
+
attempts: int = Field(description="Number of attempts made")
|
|
44
|
+
total_duration: float = Field(description="Total duration in seconds")
|
|
45
|
+
last_error: Optional[str] = Field(default=None, description="Last error message")
|
|
46
|
+
result: Optional[Any] = Field(default=None, description="Operation result if successful")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RetryConfig(BaseModel):
|
|
50
|
+
"""Configuration for retry behavior."""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(
|
|
53
|
+
validate_assignment=True,
|
|
54
|
+
extra="forbid"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
max_attempts: int = Field(default=3, ge=1, le=10, description="Maximum retry attempts")
|
|
58
|
+
strategy: RetryStrategy = Field(default=RetryStrategy.EXPONENTIAL, description="Backoff strategy")
|
|
59
|
+
base_delay: float = Field(default=1.0, ge=0.1, le=60.0, description="Base delay in seconds")
|
|
60
|
+
max_delay: float = Field(default=60.0, ge=1.0, le=300.0, description="Maximum delay in seconds")
|
|
61
|
+
jitter: bool = Field(default=True, description="Add random jitter to delays")
|
|
62
|
+
retryable_exceptions: List[str] = Field(
|
|
63
|
+
default_factory=lambda: ["ConnectionError", "TimeoutError", "HTTPError"],
|
|
64
|
+
description="Exception types that should trigger retry"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BackoffCalculator(ABC):
|
|
69
|
+
"""Abstract base for backoff calculation strategies."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: RetryConfig):
|
|
72
|
+
self.config = config
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
76
|
+
"""Calculate delay for given attempt number."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def add_jitter(self, delay: float) -> float:
|
|
80
|
+
"""Add random jitter to delay."""
|
|
81
|
+
if not self.config.jitter:
|
|
82
|
+
return delay
|
|
83
|
+
|
|
84
|
+
# Add ±25% jitter
|
|
85
|
+
jitter_range = delay * 0.25
|
|
86
|
+
jitter = random.uniform(-jitter_range, jitter_range)
|
|
87
|
+
return max(0.1, delay + jitter)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ExponentialBackoff(BackoffCalculator):
|
|
91
|
+
"""Exponential backoff strategy."""
|
|
92
|
+
|
|
93
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
94
|
+
"""Calculate exponential backoff delay."""
|
|
95
|
+
delay = self.config.base_delay * (2 ** (attempt - 1))
|
|
96
|
+
delay = min(delay, self.config.max_delay)
|
|
97
|
+
return self.add_jitter(delay)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LinearBackoff(BackoffCalculator):
|
|
101
|
+
"""Linear backoff strategy."""
|
|
102
|
+
|
|
103
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
104
|
+
"""Calculate linear backoff delay."""
|
|
105
|
+
delay = self.config.base_delay * attempt
|
|
106
|
+
delay = min(delay, self.config.max_delay)
|
|
107
|
+
return self.add_jitter(delay)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FixedBackoff(BackoffCalculator):
|
|
111
|
+
"""Fixed delay backoff strategy."""
|
|
112
|
+
|
|
113
|
+
def calculate_delay(self, attempt: int) -> float:
|
|
114
|
+
"""Calculate fixed backoff delay."""
|
|
115
|
+
return self.add_jitter(self.config.base_delay)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_backoff_calculator(config: RetryConfig) -> BackoffCalculator:
|
|
119
|
+
"""Factory function to create backoff calculator."""
|
|
120
|
+
if config.strategy == RetryStrategy.EXPONENTIAL:
|
|
121
|
+
return ExponentialBackoff(config)
|
|
122
|
+
elif config.strategy == RetryStrategy.LINEAR:
|
|
123
|
+
return LinearBackoff(config)
|
|
124
|
+
else:
|
|
125
|
+
return FixedBackoff(config)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_retryable_exception(
|
|
129
|
+
exception: Exception,
|
|
130
|
+
retryable_types: List[str]
|
|
131
|
+
) -> bool:
|
|
132
|
+
"""Check if exception is retryable."""
|
|
133
|
+
exception_name = exception.__class__.__name__
|
|
134
|
+
return exception_name in retryable_types
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def retry_async(
|
|
138
|
+
func: Callable[..., Any],
|
|
139
|
+
config: RetryConfig,
|
|
140
|
+
*args,
|
|
141
|
+
**kwargs
|
|
142
|
+
) -> RetryResult:
|
|
143
|
+
"""
|
|
144
|
+
Execute async function with retry logic.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
func: Async function to execute
|
|
148
|
+
config: Retry configuration
|
|
149
|
+
*args, **kwargs: Function arguments
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
RetryResult with operation outcome
|
|
153
|
+
"""
|
|
154
|
+
start_time = utc_now()
|
|
155
|
+
backoff = create_backoff_calculator(config)
|
|
156
|
+
last_error = None
|
|
157
|
+
|
|
158
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
159
|
+
try:
|
|
160
|
+
logger.debug(f"Attempt {attempt}/{config.max_attempts} for {func.__name__}")
|
|
161
|
+
|
|
162
|
+
result = await func(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
165
|
+
|
|
166
|
+
if attempt > 1:
|
|
167
|
+
logger.info(f"{func.__name__} succeeded on attempt {attempt}")
|
|
168
|
+
|
|
169
|
+
return RetryResult(
|
|
170
|
+
success=True,
|
|
171
|
+
attempts=attempt,
|
|
172
|
+
total_duration=duration,
|
|
173
|
+
result=result
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
last_error = str(e)
|
|
178
|
+
|
|
179
|
+
if not is_retryable_exception(e, config.retryable_exceptions):
|
|
180
|
+
logger.error(f"{func.__name__} failed with non-retryable error: {e}")
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
if attempt < config.max_attempts:
|
|
184
|
+
delay = backoff.calculate_delay(attempt)
|
|
185
|
+
logger.warning(
|
|
186
|
+
f"{func.__name__} attempt {attempt} failed: {e}. "
|
|
187
|
+
f"Retrying in {delay:.2f}s..."
|
|
188
|
+
)
|
|
189
|
+
await asyncio.sleep(delay)
|
|
190
|
+
else:
|
|
191
|
+
logger.error(f"{func.__name__} failed after {attempt} attempts: {e}")
|
|
192
|
+
|
|
193
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
194
|
+
|
|
195
|
+
return RetryResult(
|
|
196
|
+
success=False,
|
|
197
|
+
attempts=config.max_attempts,
|
|
198
|
+
total_duration=duration,
|
|
199
|
+
last_error=last_error
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def retry_sync(
|
|
204
|
+
func: Callable[..., Any],
|
|
205
|
+
config: RetryConfig,
|
|
206
|
+
*args,
|
|
207
|
+
**kwargs
|
|
208
|
+
) -> RetryResult:
|
|
209
|
+
"""
|
|
210
|
+
Execute sync function with retry logic.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
func: Sync function to execute
|
|
214
|
+
config: Retry configuration
|
|
215
|
+
*args, **kwargs: Function arguments
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
RetryResult with operation outcome
|
|
219
|
+
"""
|
|
220
|
+
import time
|
|
221
|
+
|
|
222
|
+
start_time = utc_now()
|
|
223
|
+
backoff = create_backoff_calculator(config)
|
|
224
|
+
last_error = None
|
|
225
|
+
|
|
226
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
227
|
+
try:
|
|
228
|
+
logger.debug(f"Attempt {attempt}/{config.max_attempts} for {func.__name__}")
|
|
229
|
+
|
|
230
|
+
result = func(*args, **kwargs)
|
|
231
|
+
|
|
232
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
233
|
+
|
|
234
|
+
if attempt > 1:
|
|
235
|
+
logger.info(f"{func.__name__} succeeded on attempt {attempt}")
|
|
236
|
+
|
|
237
|
+
return RetryResult(
|
|
238
|
+
success=True,
|
|
239
|
+
attempts=attempt,
|
|
240
|
+
total_duration=duration,
|
|
241
|
+
result=result
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
last_error = str(e)
|
|
246
|
+
|
|
247
|
+
if not is_retryable_exception(e, config.retryable_exceptions):
|
|
248
|
+
logger.error(f"{func.__name__} failed with non-retryable error: {e}")
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
if attempt < config.max_attempts:
|
|
252
|
+
delay = backoff.calculate_delay(attempt)
|
|
253
|
+
logger.warning(
|
|
254
|
+
f"{func.__name__} attempt {attempt} failed: {e}. "
|
|
255
|
+
f"Retrying in {delay:.2f}s..."
|
|
256
|
+
)
|
|
257
|
+
time.sleep(delay)
|
|
258
|
+
else:
|
|
259
|
+
logger.error(f"{func.__name__} failed after {attempt} attempts: {e}")
|
|
260
|
+
|
|
261
|
+
duration = (utc_now() - start_time).total_seconds()
|
|
262
|
+
|
|
263
|
+
return RetryResult(
|
|
264
|
+
success=False,
|
|
265
|
+
attempts=config.max_attempts,
|
|
266
|
+
total_duration=duration,
|
|
267
|
+
last_error=last_error
|
|
268
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UnrealOn Core Exceptions
|
|
3
|
+
|
|
4
|
+
Custom exception hierarchy for the UnrealOn system.
|
|
5
|
+
Provides specific exceptions for different error conditions
|
|
6
|
+
with proper error codes and context information.
|
|
7
|
+
|
|
8
|
+
Phase 1: Foundation exceptions with proper hierarchy
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .base import UnrealOnError, UnrealOnWarning
|
|
12
|
+
from .validation import ValidationError, ConfigurationError
|
|
13
|
+
from .communication import CommunicationError, WebSocketError, RPCError
|
|
14
|
+
from .driver import DriverError, DriverNotFoundError, DriverTimeoutError
|
|
15
|
+
from .task import TaskError, TaskTimeoutError, TaskValidationError
|
|
16
|
+
from .proxy import ProxyError, ProxyNotAvailableError, ProxyTimeoutError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Base exceptions
|
|
20
|
+
"UnrealOnError",
|
|
21
|
+
"UnrealOnWarning",
|
|
22
|
+
|
|
23
|
+
# Validation exceptions
|
|
24
|
+
"ValidationError",
|
|
25
|
+
"ConfigurationError",
|
|
26
|
+
|
|
27
|
+
# Communication exceptions
|
|
28
|
+
"CommunicationError",
|
|
29
|
+
"WebSocketError",
|
|
30
|
+
"RPCError",
|
|
31
|
+
|
|
32
|
+
# Driver exceptions
|
|
33
|
+
"DriverError",
|
|
34
|
+
"DriverNotFoundError",
|
|
35
|
+
"DriverTimeoutError",
|
|
36
|
+
|
|
37
|
+
# Task exceptions
|
|
38
|
+
"TaskError",
|
|
39
|
+
"TaskTimeoutError",
|
|
40
|
+
"TaskValidationError",
|
|
41
|
+
|
|
42
|
+
# Proxy exceptions
|
|
43
|
+
"ProxyError",
|
|
44
|
+
"ProxyNotAvailableError",
|
|
45
|
+
"ProxyTimeoutError",
|
|
46
|
+
]
|