trc-8004-sdk 0.1.0b1__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.
sdk/retry.py ADDED
@@ -0,0 +1,509 @@
1
+ """
2
+ TRC-8004 SDK 重试机制模块
3
+
4
+ 提供可配置的重试策略,支持指数退避和自定义重试条件。
5
+
6
+ Classes:
7
+ RetryConfig: 重试配置数据类
8
+ RetryContext: 重试上下文管理器
9
+
10
+ Functions:
11
+ calculate_delay: 计算重试延迟
12
+ is_retryable: 判断异常是否可重试
13
+ retry: 同步重试装饰器
14
+ retry_async: 异步重试装饰器
15
+
16
+ Predefined Configs:
17
+ DEFAULT_RETRY_CONFIG: 默认配置(3 次重试,1s 基础延迟)
18
+ AGGRESSIVE_RETRY_CONFIG: 激进配置(5 次重试,0.5s 基础延迟)
19
+ CONSERVATIVE_RETRY_CONFIG: 保守配置(2 次重试,2s 基础延迟)
20
+ NO_RETRY_CONFIG: 不重试
21
+
22
+ Example:
23
+ >>> from sdk.retry import retry, AGGRESSIVE_RETRY_CONFIG
24
+ >>> @retry(config=AGGRESSIVE_RETRY_CONFIG)
25
+ ... def flaky_operation():
26
+ ... # 可能失败的操作
27
+ ... pass
28
+
29
+ Note:
30
+ - 默认只对网络相关异常进行重试
31
+ - 使用指数退避 + 随机抖动避免惊群效应
32
+ - 可通过 RetryConfig 自定义重试行为
33
+ """
34
+
35
+ import asyncio
36
+ import logging
37
+ import random
38
+ import time
39
+ from dataclasses import dataclass, field
40
+ from functools import wraps
41
+ from typing import Callable, Optional, Tuple, Type, TypeVar, Union
42
+
43
+ from .exceptions import RetryExhaustedError, NetworkError, RPCError, TimeoutError
44
+
45
+ logger = logging.getLogger("trc8004.retry")
46
+
47
+ T = TypeVar("T")
48
+
49
+
50
+ @dataclass
51
+ class RetryConfig:
52
+ """
53
+ 重试配置数据类。
54
+
55
+ 定义重试行为的所有参数。
56
+
57
+ Attributes:
58
+ max_attempts: 最大尝试次数(包括首次尝试)
59
+ base_delay: 基础延迟时间(秒)
60
+ max_delay: 最大延迟时间(秒)
61
+ exponential_base: 指数退避基数
62
+ jitter: 是否添加随机抖动
63
+ jitter_factor: 抖动因子(0-1),表示延迟的随机波动范围
64
+ retryable_exceptions: 可重试的异常类型元组
65
+ retry_on_status_codes: 可重试的 HTTP 状态码元组
66
+
67
+ Example:
68
+ >>> config = RetryConfig(
69
+ ... max_attempts=5,
70
+ ... base_delay=0.5,
71
+ ... max_delay=30.0,
72
+ ... jitter=True,
73
+ ... )
74
+
75
+ Note:
76
+ - 延迟计算公式: delay = base_delay * (exponential_base ^ (attempt - 1))
77
+ - 抖动范围: delay ± (delay * jitter_factor)
78
+ """
79
+
80
+ max_attempts: int = 3
81
+ """最大重试次数(包括首次尝试)"""
82
+
83
+ base_delay: float = 1.0
84
+ """基础延迟时间(秒)"""
85
+
86
+ max_delay: float = 30.0
87
+ """最大延迟时间(秒)"""
88
+
89
+ exponential_base: float = 2.0
90
+ """指数退避基数"""
91
+
92
+ jitter: bool = True
93
+ """是否添加随机抖动"""
94
+
95
+ jitter_factor: float = 0.1
96
+ """抖动因子(0-1)"""
97
+
98
+ retryable_exceptions: Tuple[Type[Exception], ...] = field(
99
+ default_factory=lambda: (
100
+ NetworkError,
101
+ RPCError,
102
+ TimeoutError,
103
+ ConnectionError,
104
+ OSError,
105
+ )
106
+ )
107
+ """可重试的异常类型"""
108
+
109
+ retry_on_status_codes: Tuple[int, ...] = (429, 500, 502, 503, 504)
110
+ """可重试的 HTTP 状态码"""
111
+
112
+
113
+ # ============ 预定义配置 ============
114
+
115
+ DEFAULT_RETRY_CONFIG = RetryConfig()
116
+ """默认重试配置:3 次尝试,1s 基础延迟,指数退避"""
117
+
118
+ AGGRESSIVE_RETRY_CONFIG = RetryConfig(
119
+ max_attempts=5,
120
+ base_delay=0.5,
121
+ max_delay=60.0,
122
+ exponential_base=2.0,
123
+ )
124
+ """激进重试配置:5 次尝试,0.5s 基础延迟,适用于关键操作"""
125
+
126
+ CONSERVATIVE_RETRY_CONFIG = RetryConfig(
127
+ max_attempts=2,
128
+ base_delay=2.0,
129
+ max_delay=10.0,
130
+ exponential_base=1.5,
131
+ )
132
+ """保守重试配置:2 次尝试,2s 基础延迟,适用于非关键操作"""
133
+
134
+ NO_RETRY_CONFIG = RetryConfig(max_attempts=1)
135
+ """不重试配置:仅尝试一次"""
136
+
137
+
138
+ def calculate_delay(attempt: int, config: RetryConfig) -> float:
139
+ """
140
+ 计算第 N 次重试的延迟时间。
141
+
142
+ 使用指数退避算法,可选添加随机抖动。
143
+
144
+ Args:
145
+ attempt: 当前尝试次数(从 1 开始)
146
+ config: 重试配置
147
+
148
+ Returns:
149
+ 延迟时间(秒),第一次尝试返回 0
150
+
151
+ Example:
152
+ >>> config = RetryConfig(base_delay=1.0, exponential_base=2.0, jitter=False)
153
+ >>> calculate_delay(1, config) # 第一次尝试
154
+ 0.0
155
+ >>> calculate_delay(2, config) # 第一次重试
156
+ 1.0
157
+ >>> calculate_delay(3, config) # 第二次重试
158
+ 2.0
159
+
160
+ Note:
161
+ - 延迟公式: base_delay * (exponential_base ^ (attempt - 2))
162
+ - 抖动范围: delay ± (delay * jitter_factor)
163
+ - 延迟不会超过 max_delay
164
+ """
165
+ if attempt <= 1:
166
+ return 0.0
167
+
168
+ # 指数退避
169
+ delay = config.base_delay * (config.exponential_base ** (attempt - 2))
170
+
171
+ # 限制最大延迟
172
+ delay = min(delay, config.max_delay)
173
+
174
+ # 添加抖动
175
+ if config.jitter:
176
+ jitter_range = delay * config.jitter_factor
177
+ delay += random.uniform(-jitter_range, jitter_range)
178
+
179
+ return max(0.0, delay)
180
+
181
+
182
+ def is_retryable(exception: Exception, config: RetryConfig) -> bool:
183
+ """
184
+ 判断异常是否可重试。
185
+
186
+ 检查异常类型和 HTTP 状态码。
187
+
188
+ Args:
189
+ exception: 捕获的异常
190
+ config: 重试配置
191
+
192
+ Returns:
193
+ 是否应该重试
194
+
195
+ Example:
196
+ >>> from sdk.exceptions import NetworkError
197
+ >>> is_retryable(NetworkError("timeout"), DEFAULT_RETRY_CONFIG)
198
+ True
199
+ >>> is_retryable(ValueError("invalid"), DEFAULT_RETRY_CONFIG)
200
+ False
201
+
202
+ Note:
203
+ - 检查异常是否是 retryable_exceptions 中的类型
204
+ - 检查 HTTP 响应状态码是否在 retry_on_status_codes 中
205
+ """
206
+ # 检查异常类型
207
+ if isinstance(exception, config.retryable_exceptions):
208
+ return True
209
+
210
+ # 检查 HTTP 状态码
211
+ if hasattr(exception, "response"):
212
+ response = getattr(exception, "response", None)
213
+ if response is not None and hasattr(response, "status_code"):
214
+ return response.status_code in config.retry_on_status_codes
215
+
216
+ return False
217
+
218
+
219
+ def retry(
220
+ config: Optional[RetryConfig] = None,
221
+ operation_name: Optional[str] = None,
222
+ ) -> Callable:
223
+ """
224
+ 同步重试装饰器。
225
+
226
+ 自动重试被装饰的函数,直到成功或达到最大重试次数。
227
+
228
+ Args:
229
+ config: 重试配置,默认使用 DEFAULT_RETRY_CONFIG
230
+ operation_name: 操作名称,用于日志记录
231
+
232
+ Returns:
233
+ 装饰器函数
234
+
235
+ Raises:
236
+ RetryExhaustedError: 重试次数耗尽
237
+ Exception: 不可重试的异常会直接抛出
238
+
239
+ Example:
240
+ >>> @retry(config=AGGRESSIVE_RETRY_CONFIG, operation_name="register_agent")
241
+ ... def register_agent():
242
+ ... # 可能失败的操作
243
+ ... pass
244
+ >>>
245
+ >>> # 使用默认配置
246
+ >>> @retry()
247
+ ... def another_operation():
248
+ ... pass
249
+
250
+ Note:
251
+ - 只对 config.retryable_exceptions 中的异常进行重试
252
+ - 每次重试前会等待 calculate_delay 计算的时间
253
+ - 日志会记录每次重试的信息
254
+ """
255
+ if config is None:
256
+ config = DEFAULT_RETRY_CONFIG
257
+
258
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
259
+ op_name = operation_name or func.__name__
260
+
261
+ @wraps(func)
262
+ def wrapper(*args, **kwargs) -> T:
263
+ last_exception: Optional[Exception] = None
264
+
265
+ for attempt in range(1, config.max_attempts + 1):
266
+ try:
267
+ return func(*args, **kwargs)
268
+ except Exception as e:
269
+ last_exception = e
270
+
271
+ if not is_retryable(e, config):
272
+ logger.debug(
273
+ "Non-retryable exception in %s: %s",
274
+ op_name,
275
+ type(e).__name__,
276
+ )
277
+ raise
278
+
279
+ if attempt >= config.max_attempts:
280
+ logger.warning(
281
+ "Retry exhausted for %s after %d attempts: %s",
282
+ op_name,
283
+ attempt,
284
+ str(e),
285
+ )
286
+ raise RetryExhaustedError(op_name, attempt, e) from e
287
+
288
+ delay = calculate_delay(attempt + 1, config)
289
+ logger.info(
290
+ "Retrying %s (attempt %d/%d) after %.2fs: %s",
291
+ op_name,
292
+ attempt,
293
+ config.max_attempts,
294
+ delay,
295
+ str(e),
296
+ )
297
+ time.sleep(delay)
298
+
299
+ # 不应该到达这里
300
+ raise RetryExhaustedError(op_name, config.max_attempts, last_exception)
301
+
302
+ return wrapper
303
+
304
+ return decorator
305
+
306
+
307
+ def retry_async(
308
+ config: Optional[RetryConfig] = None,
309
+ operation_name: Optional[str] = None,
310
+ ) -> Callable:
311
+ """
312
+ 异步重试装饰器。
313
+
314
+ 与 retry 相同,但用于异步函数。
315
+
316
+ Args:
317
+ config: 重试配置
318
+ operation_name: 操作名称
319
+
320
+ Returns:
321
+ 异步装饰器函数
322
+
323
+ Example:
324
+ >>> @retry_async(config=DEFAULT_RETRY_CONFIG)
325
+ ... async def async_operation():
326
+ ... # 异步操作
327
+ ... pass
328
+
329
+ Note:
330
+ 使用 asyncio.sleep 进行异步等待。
331
+ """
332
+ if config is None:
333
+ config = DEFAULT_RETRY_CONFIG
334
+
335
+ def decorator(func: Callable[..., T]) -> Callable[..., T]:
336
+ op_name = operation_name or func.__name__
337
+
338
+ @wraps(func)
339
+ async def wrapper(*args, **kwargs) -> T:
340
+ last_exception: Optional[Exception] = None
341
+
342
+ for attempt in range(1, config.max_attempts + 1):
343
+ try:
344
+ return await func(*args, **kwargs)
345
+ except Exception as e:
346
+ last_exception = e
347
+
348
+ if not is_retryable(e, config):
349
+ raise
350
+
351
+ if attempt >= config.max_attempts:
352
+ raise RetryExhaustedError(op_name, attempt, e) from e
353
+
354
+ delay = calculate_delay(attempt + 1, config)
355
+ logger.info(
356
+ "Retrying %s (attempt %d/%d) after %.2fs",
357
+ op_name,
358
+ attempt,
359
+ config.max_attempts,
360
+ delay,
361
+ )
362
+ await asyncio.sleep(delay)
363
+
364
+ raise RetryExhaustedError(op_name, config.max_attempts, last_exception)
365
+
366
+ return wrapper
367
+
368
+ return decorator
369
+
370
+
371
+ class RetryContext:
372
+ """
373
+ 重试上下文管理器。
374
+
375
+ 用于手动控制重试逻辑,适用于需要更细粒度控制的场景。
376
+
377
+ Attributes:
378
+ config: 重试配置
379
+ operation: 操作名称
380
+ attempt: 当前尝试次数
381
+ last_exception: 最后一次异常
382
+
383
+ Args:
384
+ config: 重试配置,默认使用 DEFAULT_RETRY_CONFIG
385
+ operation: 操作名称,用于日志和错误消息
386
+
387
+ Example:
388
+ >>> with RetryContext(config=DEFAULT_RETRY_CONFIG, operation="send_tx") as ctx:
389
+ ... while ctx.should_retry():
390
+ ... ctx.next_attempt()
391
+ ... try:
392
+ ... result = do_something()
393
+ ... ctx.success()
394
+ ... break
395
+ ... except Exception as e:
396
+ ... ctx.failed(e)
397
+
398
+ Note:
399
+ - 必须调用 next_attempt() 开始每次尝试
400
+ - 成功时调用 success(),失败时调用 failed(exception)
401
+ - 重试耗尽时会自动抛出 RetryExhaustedError
402
+ """
403
+
404
+ def __init__(
405
+ self,
406
+ config: Optional[RetryConfig] = None,
407
+ operation: str = "operation",
408
+ ) -> None:
409
+ """
410
+ 初始化重试上下文。
411
+
412
+ Args:
413
+ config: 重试配置
414
+ operation: 操作名称
415
+ """
416
+ self.config = config or DEFAULT_RETRY_CONFIG
417
+ self.operation = operation
418
+ self.attempt = 0
419
+ self.last_exception: Optional[Exception] = None
420
+ self._succeeded = False
421
+
422
+ def __enter__(self) -> "RetryContext":
423
+ """进入上下文。"""
424
+ return self
425
+
426
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
427
+ """
428
+ 退出上下文。
429
+
430
+ 如果操作未成功且重试耗尽,抛出 RetryExhaustedError。
431
+ """
432
+ if exc_type is not None and not self._succeeded:
433
+ if self.attempt >= self.config.max_attempts:
434
+ raise RetryExhaustedError(
435
+ self.operation,
436
+ self.attempt,
437
+ self.last_exception or exc_val,
438
+ )
439
+ return False
440
+
441
+ def should_retry(self) -> bool:
442
+ """
443
+ 检查是否应该继续重试。
444
+
445
+ Returns:
446
+ 如果未成功且未达到最大尝试次数,返回 True
447
+ """
448
+ if self._succeeded:
449
+ return False
450
+ return self.attempt < self.config.max_attempts
451
+
452
+ def next_attempt(self) -> int:
453
+ """
454
+ 开始下一次尝试。
455
+
456
+ 如果不是第一次尝试,会等待计算的延迟时间。
457
+
458
+ Returns:
459
+ 当前尝试次数
460
+ """
461
+ self.attempt += 1
462
+
463
+ if self.attempt > 1:
464
+ delay = calculate_delay(self.attempt, self.config)
465
+ if delay > 0:
466
+ time.sleep(delay)
467
+
468
+ return self.attempt
469
+
470
+ def failed(self, exception: Exception) -> None:
471
+ """
472
+ 标记当前尝试失败。
473
+
474
+ 如果异常不可重试或重试耗尽,会立即抛出异常。
475
+
476
+ Args:
477
+ exception: 当前尝试的异常
478
+
479
+ Raises:
480
+ exception: 如果异常不可重试
481
+ RetryExhaustedError: 如果重试耗尽
482
+ """
483
+ self.last_exception = exception
484
+
485
+ if not is_retryable(exception, self.config):
486
+ raise exception
487
+
488
+ if self.attempt >= self.config.max_attempts:
489
+ raise RetryExhaustedError(
490
+ self.operation,
491
+ self.attempt,
492
+ exception,
493
+ ) from exception
494
+
495
+ logger.info(
496
+ "Attempt %d/%d failed for %s: %s",
497
+ self.attempt,
498
+ self.config.max_attempts,
499
+ self.operation,
500
+ str(exception),
501
+ )
502
+
503
+ def success(self) -> None:
504
+ """
505
+ 标记操作成功。
506
+
507
+ 调用后 should_retry() 将返回 False。
508
+ """
509
+ self._succeeded = True