Files
zhenxun_bot/zhenxun/utils/decorator/retry.py
T
be86e0bb7f
检查bot是否运行正常 / bot check (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Sequential Lint and Type Check / ruff-call (push) Waiting to run
Sequential Lint and Type Check / pyright-call (push) Blocked by required conditions
Release Drafter / Update Release Draft (push) Waiting to run
Force Sync to Aliyun / sync (push) Waiting to run
Update Version / update-version (push) Waiting to run
♻️ refactor(scheduler): 重构定时任务系统并增强功能 (#2009)
* ♻️ refactor(scheduler): 重构定时任务系统并增强功能

- 【模型重命名】将 `ScheduleInfo` 模型及其数据库表重命名为 `ScheduledJob`,以提高语义清晰度。
- 【触发器抽象】引入 `Trigger` 工厂类,提供类型安全的 Cron、Interval 和 Date 触发器配置。
- 【执行策略】新增 `ExecutionPolicy` 模型,允许为定时任务定义重试策略、延迟、异常类型以及成功/失败回调。
- 【任务执行】重构任务执行逻辑,支持 NoneBot 的依赖注入,并根据 `ExecutionPolicy` 处理任务的重试和回调。
- 【临时任务】增加声明式和编程式的临时任务调度能力,支持非持久化任务在运行时动态创建和执行。
- 【管理命令】更新定时任务管理命令 (`schedule_admin`),使其适配新的 `ScheduledJob` 模型和参数验证逻辑。
- 【展示优化】改进定时任务列表和状态展示,使用新的触发器格式化逻辑和参数模型信息。
- 【重试装饰器】为 `Retry.api` 装饰器添加 `on_success` 回调,允许在任务成功执行后触发额外操作。

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-06 09:02:23 +08:00

275 lines
10 KiB
Python

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