100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""定时任务的统一默认值与日志(`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))
|