# 任务调度器使用说明 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:00(5 位 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:00–17: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` 默认为 3(APScheduler `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` 组件 |