zhenxun_bot/zhenxun/utils/decorator/retry.py

275 lines
10 KiB
Python
Raw Normal View History

from collections.abc import Callable
from functools import partial, wraps
from typing import Any, Literal
from anyio import EndOfStream
from httpx import (
ConnectError,
HTTPStatusError,
RemoteProtocolError,
StreamError,
TimeoutException,
)
from nonebot.utils import is_coroutine_callable
from tenacity import (
RetryCallState,
retry,
retry_if_exception_type,
retry_if_result,
stop_after_attempt,
wait_exponential,
wait_fixed,
)
from zhenxun.services.log import logger
LOG_COMMAND = "RetryDecorator"
_SENTINEL = object()
def _log_before_sleep(log_name: str | None, retry_state: RetryCallState):
"""
tenacity 重试前的日志记录回调函数
"""
func_name = retry_state.fn.__name__ if retry_state.fn else "unknown_function"
log_context = f"函数 '{func_name}'"
if log_name:
log_context = f"操作 '{log_name}' ({log_context})"
reason = ""
if retry_state.outcome:
if exc := retry_state.outcome.exception():
reason = f"触发异常: {exc.__class__.__name__}({exc})"
else:
reason = f"不满足结果条件: result={retry_state.outcome.result()}"
wait_time = (
getattr(retry_state.next_action, "sleep", 0) if retry_state.next_action else 0
)
logger.warning(
f"{log_context}{retry_state.attempt_number} 次重试... "
f"等待 {wait_time:.2f} 秒. {reason}",
LOG_COMMAND,
)
class Retry:
@staticmethod
def simple(
stop_max_attempt: int = 3,
wait_fixed_seconds: int = 2,
exception: tuple[type[Exception], ...] = (),
*,
log_name: str | None = None,
on_failure: Callable[[Exception], Any] | None = None,
return_on_failure: Any = _SENTINEL,
):
"""
一个简单的用于通用网络请求的重试装饰器预设
参数:
stop_max_attempt: 最大重试次数
wait_fixed_seconds: 固定等待策略的等待秒数
exception: 额外需要重试的异常类型元组
log_name: 用于日志记录的操作名称
on_failure: (可选) 所有重试失败后的回调
return_on_failure: (可选) 所有重试失败后的返回值
"""
return Retry.api(
stop_max_attempt=stop_max_attempt,
wait_fixed_seconds=wait_fixed_seconds,
exception=exception,
strategy="fixed",
log_name=log_name,
on_failure=on_failure,
return_on_failure=return_on_failure,
)
@staticmethod
def download(
stop_max_attempt: int = 3,
exception: tuple[type[Exception], ...] = (),
*,
wait_exp_multiplier: int = 2,
wait_exp_max: int = 15,
log_name: str | None = None,
on_failure: Callable[[Exception], Any] | None = None,
return_on_failure: Any = _SENTINEL,
):
"""
一个适用于文件下载的重试装饰器预设使用指数退避策略
参数:
stop_max_attempt: 最大重试次数
exception: 额外需要重试的异常类型元组
wait_exp_multiplier: 指数退避的乘数
wait_exp_max: 指数退避的最大等待时间
log_name: 用于日志记录的操作名称
on_failure: (可选) 所有重试失败后的回调
return_on_failure: (可选) 所有重试失败后的返回值
"""
return Retry.api(
stop_max_attempt=stop_max_attempt,
exception=exception,
strategy="exponential",
wait_exp_multiplier=wait_exp_multiplier,
wait_exp_max=wait_exp_max,
log_name=log_name,
on_failure=on_failure,
return_on_failure=return_on_failure,
)
@staticmethod
def api(
stop_max_attempt: int = 3,
wait_fixed_seconds: int = 1,
exception: tuple[type[Exception], ...] = (),
*,
strategy: Literal["fixed", "exponential"] = "fixed",
retry_on_result: Callable[[Any], bool] | None = None,
wait_exp_multiplier: int = 1,
wait_exp_max: int = 10,
log_name: str | None = None,
on_success: Callable[[Any], Any] | None = None,
on_failure: Callable[[Exception], Any] | None = None,
return_on_failure: Any = _SENTINEL,
):
"""
通用可配置的API调用重试装饰器
参数:
stop_max_attempt: 最大重试次数
wait_fixed_seconds: 固定等待策略的等待秒数
exception: 额外需要重试的异常类型元组
strategy: 重试等待策略, 'fixed' (固定) 'exponential' (指数退避)
retry_on_result: 一个回调函数接收函数返回值如果返回 True则触发重试
例如 `lambda r: r.status_code != 200`
wait_exp_multiplier: 指数退避的乘数
wait_exp_max: 指数退避的最大等待时间
log_name: 用于日志记录的操作名称方便区分不同的重试场景
on_success: (可选) 当函数成功执行且未触发重试
会调用此函数并将函数的返回值作为参数传入
on_failure: (可选) 当所有重试都失败后在抛出异常或返回默认值之前
会调用此函数并将最终的异常实例作为参数传入
return_on_failure: (可选) 如果设置了此参数当所有重试失败后
将不再抛出异常而是返回此参数指定的值
"""
base_exceptions = (
TimeoutException,
ConnectError,
HTTPStatusError,
StreamError,
RemoteProtocolError,
EndOfStream,
*exception,
)
def decorator(func: Callable) -> Callable:
if strategy == "exponential":
wait_strategy = wait_exponential(
multiplier=wait_exp_multiplier, max=wait_exp_max
)
else:
wait_strategy = wait_fixed(wait_fixed_seconds)
retry_conditions = retry_if_exception_type(base_exceptions)
if retry_on_result:
retry_conditions |= retry_if_result(retry_on_result)
log_callback = partial(_log_before_sleep, log_name)
tenacity_retry_decorator = retry(
stop=stop_after_attempt(stop_max_attempt),
wait=wait_strategy,
retry=retry_conditions,
before_sleep=log_callback,
reraise=True,
)
decorated_func = tenacity_retry_decorator(func)
if return_on_failure is _SENTINEL:
if is_coroutine_callable(func):
@wraps(func)
async def async_success_wrapper(*args, **kwargs):
result = await decorated_func(*args, **kwargs)
if on_success:
if is_coroutine_callable(on_success):
await on_success(result)
else:
on_success(result)
return result
return async_success_wrapper
else:
@wraps(func)
def sync_success_wrapper(*args, **kwargs):
result = decorated_func(*args, **kwargs)
if on_success:
if is_coroutine_callable(on_success):
logger.error(
f"不能在同步函数 '{func.__name__}' 中调用异步的 "
f"on_success 回调。",
LOG_COMMAND,
)
else:
on_success(result)
return result
return sync_success_wrapper
if is_coroutine_callable(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
try:
result = await decorated_func(*args, **kwargs)
if on_success:
if is_coroutine_callable(on_success):
await on_success(result)
else:
on_success(result)
return result
except Exception as e:
if on_failure:
if is_coroutine_callable(on_failure):
await on_failure(e)
else:
on_failure(e)
return return_on_failure
return async_wrapper
else:
@wraps(func)
def sync_wrapper(*args, **kwargs):
try:
result = decorated_func(*args, **kwargs)
if on_success:
if is_coroutine_callable(on_success):
logger.error(
f"不能在同步函数 '{func.__name__}' 中调用异步的 "
f"on_success 回调。",
LOG_COMMAND,
)
else:
on_success(result)
return result
except Exception as e:
if on_failure:
if is_coroutine_callable(on_failure):
logger.error(
f"不能在同步函数 '{func.__name__}' 中调用异步的 "
f"on_failure 回调。",
LOG_COMMAND,
)
else:
on_failure(e)
return return_on_failure
return sync_wrapper
return decorator