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/__init__.py +123 -0
- sdk/agent_protocol_client.py +180 -0
- sdk/agent_sdk.py +1549 -0
- sdk/chain_utils.py +278 -0
- sdk/cli.py +549 -0
- sdk/client.py +202 -0
- sdk/contract_adapter.py +489 -0
- sdk/exceptions.py +652 -0
- sdk/retry.py +509 -0
- sdk/signer.py +284 -0
- sdk/utils.py +163 -0
- trc_8004_sdk-0.1.0b1.dist-info/METADATA +411 -0
- trc_8004_sdk-0.1.0b1.dist-info/RECORD +16 -0
- trc_8004_sdk-0.1.0b1.dist-info/WHEEL +5 -0
- trc_8004_sdk-0.1.0b1.dist-info/entry_points.txt +2 -0
- trc_8004_sdk-0.1.0b1.dist-info/top_level.txt +1 -0
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
|