Files
2026-07-13 12:34:57 +08:00

272 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务调度器使用说明
MyBoot 内置基于 [APScheduler](https://apscheduler.readthedocs.io/) 的任务调度器,支持 Cron、固定间隔与一次性任务。应用启动时,若已注册任务且调度器已启用,会自动启动调度。
## 1. 快速开始(推荐)
定时任务**必须**定义在 `@component` 装饰的类中,由自动配置在组件注册时扫描并加入 `app.scheduler`
```python
from myboot.core.decorators import component, cron, interval, once
from myboot.core.config import get_config
@component()
class DataSyncJobs:
"""定时任务组件"""
@cron("0 2 * * *") # 每天 02:005 位 Cron
def sync_daily(self):
print("每日同步")
@interval(minutes=30) # 每 30 分钟
def health_check(self):
print("健康检查")
@once("2025-12-31 23:59:59") # 指定时刻执行一次
def year_end_task(self):
print("年末任务")
@cron("0 */5 * * *", enabled=get_config("jobs.report.enabled", True))
def report(self):
"""enabled 可从配置读取,为 False 时不注册"""
print("报表任务")
```
**要求与约定:**
| 项 | 说明 |
|----|------|
| 类装饰器 | 必须使用 `@component()`,不支持模块级函数或 `@service` 类中的 `@cron` |
| 方法可见性 | 以 `_` 开头的私有方法不会被扫描 |
| 依赖注入 | 可在组件 `__init__` 中注入 `@service`,在任务方法内使用 |
| 包扫描 | 任务类需位于自动发现包内(默认 `app`),见项目启动与 `auto_discover` 配置 |
应用生命周期中,存在已注册任务且调度器启用时,会在启动钩子阶段调用 `scheduler.start()`,关闭时 `scheduler.stop()`
## 2. 配置
`config.yaml` 或环境变量中配置(环境变量嵌套键使用双下划线 `__`,详见 [配置管理使用说明](./configuration.md))。
```yaml
scheduler:
enabled: true # 是否允许启动调度器
timezone: "Asia/Shanghai" # 任务触发时区(建议显式设置)
max_workers: 10 # 线程池大小
on_all_workers: false # 多 worker 时是否在非 primary 进程也启用
```
| 配置项 | 环境变量示例 | 默认值 | 说明 |
|--------|----------------|--------|------|
| `scheduler.enabled` | `SCHEDULER__ENABLED=false` | `true` | 为 `false` 时调度器不启动 |
| `scheduler.timezone` | `SCHEDULER__TIMEZONE=Asia/Shanghai` | `UTC` | 所有触发器使用的时区 |
| `scheduler.max_workers` | `SCHEDULER__MAX_WORKERS=20` | `10` | 并发执行任务的上限 |
| `scheduler.on_all_workers` | `SCHEDULER__ON_ALL_WORKERS=true` | `false` | 见下文「多 Worker」 |
`config.py` 中默认片段:
```yaml
scheduler:
enabled: true
timezone: "UTC"
max_workers: 10
```
## 3. Cron 表达式
解析逻辑见 `myboot/core/scheduler.py``_parse_cron`
1. 优先使用 `CronTrigger.from_crontab`**5 位**Unix/APScheduler 风格)
2. 解析失败时回退为手动构造 `CronTrigger`(支持 **5 位****6 位**
### 3.1 五位格式(推荐)
顺序:**分 · 时 · 日 · 月 · 周**
```
分 时 日 月 周
│ │ │ │ └── 星期(0=周一 … 6=周日,见下表)
│ │ │ └────── 月(1-12 或 *)
│ │ └────────── 日(1-31 或 *)
│ └────────────── 时(0-23)
└────────────────── 分(0-59)
```
**星期字段(APScheduler `from_crontab`)与 Linux crontab 不同:**
| 值 | APScheduler(本项目 5 位默认路径) | 传统 Linux crontab |
|----|-----------------------------------|---------------------|
| 0 | 周一 | 周日 |
| 1 | 周二 | 周一 |
| 2 | 周三 | 周二 |
| … | … | … |
| 6 | 周日 | 周六 |
> 使用 5 位表达式时,请按 **APScheduler 星期编号**理解,避免与系统 `crontab` 的「周日=0」混淆。
**常用示例(5 位,时区以 `scheduler.timezone` 为准):**
| 表达式 | 含义 |
|--------|------|
| `0 * * * *` | 每小时整点 |
| `0 2 * * *` | 每天 02:00 |
| `0 23 * * 1` | 每周二 23:00(周字段 `1` = 周二) |
| `30 8 * * 0` | 每周一 08:30 |
| `*/15 * * * *` | 每 15 分钟 |
| `0 9-17 * * 1-5` | 工作日 09:0017:00 每小时整点 |
### 3.2 六位格式(兼容旧写法)
顺序:**秒 · 分 · 时 · 日 · 月 · 周**
仅在 `from_crontab` 无法解析时走手动分支,例如:
| 表达式 | 含义 |
|--------|------|
| `0 0 * * * *` | 每小时整点(秒=0,分=0) |
| `0 */5 * * * *` | 每 5 分钟 |
| `0 0 2 * * *` | 每天 02:00:00 |
### 3.3 字段支持
支持 APScheduler 常见写法:`*``,``-``/` 及范围。复杂表达式以 [APScheduler Cron 文档](https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html) 为准。
## 4. 装饰器 API
### 4.1 `@cron`
```python
@cron(cron_expression: str, enabled: bool | None = None, **kwargs)
```
- `cron_expression`5 位或 6 位 Cron 字符串
- `enabled``False` 时跳过注册;`None` 默认启用
- `**kwargs`:传给 APScheduler `add_job` 的额外参数(如 `name``max_instances`
### 4.2 `@interval`
```python
@interval(seconds=None, minutes=None, hours=None, enabled=None, **kwargs)
```
三者至少指定其一,内部统一换算为秒。例如 `@interval(minutes=5)` 每 5 分钟执行。
### 4.3 `@once`
```python
@once(run_date: str, enabled=None, **kwargs)
```
`run_date` 支持格式:
- `YYYY-MM-DD HH:MM:SS`
- `YYYY-MM-DD HH:MM`
- `YYYY-MM-DD`(当天 00:00:00
时间为 **naive**,由调度器全局时区 `scheduler.timezone` 解释。
## 5. 多 Worker 与进程模型
多进程部署(`server.workers > 1`)时:
- 默认**仅 primary worker**`MYBOOT_IS_PRIMARY_WORKER=1`)启用调度器,避免重复执行
- 设置 `scheduler.on_all_workers: true` 可在每个 worker 都运行调度(一般仅特殊场景需要)
任务在**线程池**中执行;`max_instances` 默认为 3APScheduler `job_defaults`),同一任务并发实例数受此限制。
## 6. 编程式 API
除装饰器外,可通过 `app.scheduler``get_scheduler()` 动态管理任务。
```python
from myboot.core.scheduler import get_scheduler
scheduler = get_scheduler() # 注意:与 Application 可能不是同一实例,推荐用 app.scheduler
job_id = scheduler.add_cron_job(func=my_func, cron="0 2 * * *")
scheduler.add_interval_job(func=my_func, interval=60)
scheduler.add_date_job(func=my_func, run_date="2025-12-31 23:59:59")
scheduler.remove_job(job_id)
info = scheduler.get_job_info(job_id)
all_jobs = scheduler.list_all_jobs()
```
| 方法 | 说明 |
|------|------|
| `add_cron_job(func, cron, job_id=None, **kwargs)` | 添加 Cron 任务 |
| `add_interval_job(func, interval, job_id=None, **kwargs)` | `interval` 单位为秒 |
| `add_date_job(func, run_date, job_id=None, **kwargs)` | 一次性任务 |
| `remove_job(job_id)` | 移除任务 |
| `get_job_info(job_id)` | 任务类型、下次执行时间等 |
| `list_all_jobs()` | 所有任务摘要 |
| `start()` / `stop()` | 启停(应用生命周期通常自动处理) |
| `is_enabled()` / `is_running()` / `has_jobs()` | 状态查询 |
| `get_config()` | 当前调度器配置摘要 |
未指定 `job_id` 时,默认生成 `cron_{模块名}.{限定名}`(如
`cron_app.jobs.SyncJobs.daily_sync`),`interval_`/`date_` 前缀同理。
限定名包含类名,因此不同类中的同名方法不会冲突(0.2.0 起,issue #14
此前为 `cron_{函数名}`,同名方法会因 ID 冲突导致启动失败)。
注册时即校验 ID 唯一性:显式传入重复的 `job_id` 会抛出 `SchedulerError`
自动生成的 ID 重复(同一函数注册多次)会追加 8 位随机后缀并记录 warning。
### 6.1 `ScheduledJob` 类
继承 `myboot.jobs.scheduled_job.ScheduledJob` 可实现带重试、状态跟踪的任务,并通过 `add_scheduled_job` 注册:
```python
from myboot.jobs.scheduled_job import ScheduledJob
class CleanupJob(ScheduledJob):
def run(self):
# 业务逻辑
return "ok"
job = CleanupJob(name="cleanup", trigger="0 3 * * *") # 或 trigger={'type': 'cron', 'cron': '...'}
app.scheduler.add_scheduled_job(job)
```
`trigger` 可为 Cron 字符串或字典:`cron` / `interval`seconds/minutes/hours/days/ `date``run_date`)。
## 7. 运维与排查
```python
# 应用内
print(app.scheduler.get_config())
for job in app.scheduler.list_all_jobs():
print(job["job_id"], job.get("type"), job.get("next_run_time"))
# 健康检查等可结合 application 状态中的 scheduler 字段
```
日志:调度器绑定 logger 名 `scheduler`,注册任务时会输出 `已添加 Cron 任务` 等信息。
**任务未执行时检查:**
1. `scheduler.enabled` 是否为 `true`
2. 多 worker 下当前进程是否为 primary(或已开启 `on_all_workers`
3. 装饰器 `enabled=False` 是否被跳过
4. Cron 位数与星期编号是否符合上文约定
5. `timezone` 是否与预期一致
6. 应用启动时 `has_jobs()` 是否为真(无任务不会 `start()`
## 8. 最佳实践
1. **统一用 5 位 Cron**,减少与 6 位混用带来的理解成本。
2. **显式设置 `scheduler.timezone`**(如 `Asia/Shanghai`),避免默认 UTC 造成「时间差 8 小时」。
3. **任务逻辑保持幂等**;错过触发时 APScheduler 有 `misfire_grace_time`(默认 30 秒),但仍可能补跑。
4. **长耗时任务**注意 `max_workers``max_instances`,避免占满线程池。
5. **配置开关**用 `enabled=get_config('jobs.xxx.enabled', True)`,便于按环境关闭任务。
6. **IO 密集或阻塞操作**在任务内自行控制超时与异常,避免拖垮调度线程。
## 9. 相关文档与代码
| 资源 | 说明 |
|------|------|
| [scheduler_refactor_analysis.md](./scheduler_refactor_analysis.md) | APScheduler 重构与能力对照(偏设计) |
| [dependency-injection.md](./dependency-injection.md) | 含定时任务的 `@component` 示例 |
| `myboot/core/scheduler.py` | 调度器实现 |
| `myboot/core/decorators.py` | `@cron` / `@interval` / `@once` |
| `examples/convention_app.py` | 完整示例 `ScheduledJobs` 组件 |