Files
HeXi/hexi/core/scheduler.py
T

100 lines
3.9 KiB
Python
Raw Normal View History

"""定时任务的统一默认值与日志(`nonebot_plugin_apscheduler` 的薄封装)。
直接用 `scheduler.scheduled_job` / `scheduler.add_job` 时,各插件参数各写各的,
而 APScheduler 的原生默认值有两个坑:
1. **不设时区** → 用服务器本地时区。换机器、进容器、系统时区不是
Asia/Shanghai 时,`cron(hour=4)` 这类任务会整体错位 —— 而且错得很安静:
任务照常在跑,只是时间不对(`interval` 类任务不受影响)。
2. **`misfire_grace_time` 默认只有 1 秒** → 事件循环被卡一下(同步 IO、GC、
笔记本休眠唤醒)任务就被判"错过"直接丢弃,一天少跑几次很难发现。
本模块只做两件事:**补默认值** + **包一层日志**(起止 DEBUG、异常 ERROR 后吞掉,
不让坏任务刷爆 apscheduler 自己的日志)。调用方显式给的参数一律优先,
触发条件一个字都不改。
用法(跟原来一模一样,只是换个导入):
from hexi.core.scheduler import add_job, scheduled_job
@scheduled_job("cron", hour=4, id="temp_cleanup")
async def cleanup(): ...
add_job(fn, "interval", minutes=10, id="xxx", replace_existing=True)
动态删任务仍用 `nonebot_plugin_apscheduler.scheduler.remove_job(...)`。
"""
from __future__ import annotations
import functools
from collections.abc import Callable
from typing import Any
from nonebot import require
from nonebot.log import logger
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler # noqa: E402
#: 默认时区:显式钉死,不跟随服务器本地时区(见模块文档第 1 条)
DEFAULT_TIMEZONE = "Asia/Shanghai"
#: 默认宽限:宁可晚做也别不做(见模块文档第 2 条)
DEFAULT_MISFIRE_GRACE_TIME = 60
#: 补进 kwargs 的默认值;`coalesce` 在 APScheduler 里本来就是 True,
#: 写出来是为了表明"积压的任务只补跑一次"是有意为之
_DEFAULTS: dict[str, Any] = {
"timezone": DEFAULT_TIMEZONE,
"misfire_grace_time": DEFAULT_MISFIRE_GRACE_TIME,
"coalesce": True,
}
def _with_defaults(kwargs: dict[str, Any]) -> dict[str, Any]:
for key, value in _DEFAULTS.items():
kwargs.setdefault(key, value)
return kwargs
async def _run_logged(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""跑一个任务:起止 DEBUG、异常 ERROR 后吞掉(任务失败不该外抛给调度器)。"""
logger.debug(f"定时任务开始: {func.__name__}")
try:
result = await func(*args, **kwargs)
except Exception:
logger.exception(f"定时任务异常: {func.__name__}")
return None
logger.debug(f"定时任务完成: {func.__name__}")
return result
def scheduled_job(*args: Any, **kwargs: Any) -> Callable[..., Any]:
"""`scheduler.scheduled_job` + 统一默认值与日志(装饰器,用法不变)。"""
_with_defaults(kwargs)
def deco(func: Callable[..., Any]) -> Callable[..., Any]:
# functools.wraps 保住 __name__:APScheduler 不指定 id 时用函数名做任务 id
@functools.wraps(func)
async def wrapper(*fargs: Any, **fkwargs: Any) -> Any:
return await _run_logged(func, *fargs, **fkwargs)
return scheduler.scheduled_job(*args, **kwargs)(wrapper)
return deco
def add_job(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""`scheduler.add_job` + 统一默认值(动态增删任务用)。
注意包了一层:注册的是包装函数,`func.__name__` 由 `functools.wraps` 保住,
所以不显式传 `id` 时的默认任务 id 仍是原函数名。删除任务请用原函数的
id(或显式传 `id`)配合 `scheduler.remove_job(...)`。
"""
@functools.wraps(func)
async def wrapper(*fargs: Any, **fkwargs: Any) -> Any:
return await _run_logged(func, *fargs, **fkwargs)
return scheduler.add_job(wrapper, *args, **_with_defaults(kwargs))