Coverage for qreward / utils / retry.py: 97.14%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 19:32 +0000

1import asyncio 

2import functools 

3import random 

4import time 

5from collections.abc import Callable 

6from typing import ( 

7 Any, 

8 Union, 

9 Iterable, 

10) 

11 

12from qreward.types import RetryPredicate 

13 

14 

15def retry( 

16 *, 

17 max_retries: int = 3, 

18 delay: float = 1.0, 

19 backoff_factor: float = 2.0, 

20 jitter: bool = True, 

21 retry_on: Union[ 

22 type[Exception], 

23 Iterable[type[Exception]], 

24 RetryPredicate, 

25 ] = Exception, 

26 check_exception: RetryPredicate | None = None, 

27): 

28 """重试装饰器, 支持同步和异步函数。 

29 

30 参数: 

31 max_retries: 最大重试次数(不包括首次调用) 

32 delay: 初始延迟时间(单位: 秒) 

33 backoff_factor: 指数退避因子 

34 jitter: 是否添加随机抖动(0~1倍delay的随机值) 

35 retry_on: 指定要重试的异常类型或异常判断函数 

36 check_exception: 自定义异常判断函数, 接收异常实例, 返回是否重试 

37 

38 示例: 

39 @retry( 

40 max_retries=3, 

41 delay=0.1, 

42 retry_on=(ValueError, ConnectionError), 

43 ) 

44 async def fetch_data(): 

45 ... 

46 

47 @retry(check_exception=lambda e: isinstance(e, ValueError) 

48 and "retry" in str(e)) 

49 def unreliable_func(): 

50 ... 

51 """ 

52 

53 def should_retry(exception: Exception) -> bool: 

54 # 检查 retry_on 

55 if callable(retry_on): 

56 if not retry_on(exception): 

57 return False 

58 elif not isinstance( 

59 exception, 

60 tuple(retry_on) if isinstance(retry_on, (list, tuple)) 

61 else (retry_on,), 

62 ): 

63 return False 

64 

65 # 检查 check_exception 

66 if check_exception is not None: 

67 return check_exception(exception) 

68 

69 return True 

70 

71 def exponential_backoff(attempt: int) -> float: 

72 # 计算指数退避时间 

73 exp_delay = delay * (backoff_factor ** (attempt - 1)) 

74 if jitter: 

75 exp_delay += random.uniform(0, delay) 

76 return exp_delay 

77 

78 def decorator(func: Callable[..., Any]) -> Callable[..., Any]: 

79 is_coroutine = asyncio.iscoroutinefunction(func) 

80 

81 @functools.wraps(func) 

82 async def async_wrapper(*args, **kwargs): 

83 last_exc = None 

84 for attempt in range(1, max_retries + 2): 84 ↛ 93line 84 didn't jump to line 93 because the loop on line 84 didn't complete

85 try: 

86 return await func(*args, **kwargs) 

87 except Exception as e: 

88 last_exc = e 

89 if attempt >= max_retries + 1 or not should_retry(e): 

90 break 

91 wait_time = exponential_backoff(attempt) 

92 await asyncio.sleep(wait_time) 

93 raise last_exc 

94 

95 @functools.wraps(func) 

96 def sync_wrapper(*args, **kwargs): 

97 last_exc = None 

98 for attempt in range(1, max_retries + 2): 98 ↛ 107line 98 didn't jump to line 107 because the loop on line 98 didn't complete

99 try: 

100 return func(*args, **kwargs) 

101 except Exception as e: 

102 last_exc = e 

103 if attempt >= max_retries + 1 or not should_retry(e): 

104 break 

105 wait_time = exponential_backoff(attempt) 

106 time.sleep(wait_time) 

107 raise last_exc 

108 

109 # 根据函数类型返回对应的包装器 

110 return async_wrapper if is_coroutine else sync_wrapper 

111 

112 return decorator