Egg 定时任务调度实战指南:基于 Schedule 装饰器的 Worker/All 双模式实现
【免费下载链接】egg🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg
导读
本文面向使用 Egg + tegg 技术栈的开发者,完整讲解基于Schedule装饰器实现定时任务的完整流程:从插件启用、interval间隔模式与cron表达式模式的配置语法,到worker/all两种执行模式的行为差异,再到immediate、disable、env等任务运行参数的使用方法。读完本文,你将能够用装饰器方式在 Egg 应用中快速注册定时任务,并理解任务在 agent 与 worker 之间的调度与分发原理。
核心概念与适用场景
Egg 的定时任务(Schedule)用于支持定期执行的自动化逻辑,例如数据清洗、缓存预热、报表生成、定时推送等。文档明确指出其核心使用场景是:
Supports regular scheduled tasks that will execute on every deployed machine.
即常规定时任务会在每一台部署的机器上执行。例如生产环境通常至少部署 2 台机器,那么该定时任务会在两台机器上都运行。这一特性与后文要讲到的worker/all模式共同决定了任务的实际执行次数:
- 若按“每台机器上仅一个进程执行”,适合
worker模式(与单机执行次数无关,每台机器各执行一次); - 若按“每台机器上所有 worker 进程都执行”,适合
all模式(每台机器上 N 个 worker 就执行 N 次)。
与 tegg 体系内的其他装饰器(如@Inject、@SingletonProto)一致,Schedule装饰器同样遵循“类即控制器”的约定:被装饰的类必须包含一个名为subscribe的方法,框架在调度任务时会调用该类的subscribe方法。
使用前提:切勿把代码放到 app/schedule 目录
文档给出了一个高优先级警告:
⚠️ Do not place your code in the
app/schedulepath, because Egg scans this path by default to register scheduled tasks, which will conflict with the decorator-based approach.
即:不要把使用装饰器的定时任务代码放在app/schedule目录下。原因在于 Egg 框架默认会扫描app/schedule路径并自动注册定时任务,这与 tegg 装饰器方式注册会产生冲突。从源码可以印证这一点:load_schedule.ts 中的loadSchedule函数会把app.loader.getLoadUnits().map((unit) => path.join(unit.path, 'app/schedule'))作为默认扫描目录之一,同时叠加app.config.schedule.directory配置的额外目录。
因此,本文示例统一将定时任务类放在app/port/schedule/目录下(如app/port/schedule/Demo.ts),这也是 tegg 推荐的端口(port)目录约定。
启用插件
schedule是 Egg 内置插件,默认启用。在config/plugin.ts中显式声明即可:
export default { teggSchedule: true, };从插件入口 index.ts 可以看到,@eggjs/schedule插件通过definePluginFactory定义,enable: true,无需额外安装:
export default definePluginFactory({ name: 'schedule', enable: true, path: import.meta.dirname, }) as EggPluginFactory;插件同时导出了ScheduleWorker与Scheduler两个核心类,分别用于 worker 侧的任务注册与 agent 侧的调度管理。
两种触发模式
Interval 间隔模式
interval模式表示每个机器按固定间隔执行一次任务,间隔通过Schedule装饰器第一个参数的scheduleData.interval设置:
- number 类型:单位为毫秒,例如
100表示每 100ms 执行一次; - string 类型:会使用 ms)将其转换为毫秒,例如
'5s'表示每 5 秒执行一次,同样支持'1m'、'2h'等写法。
示例代码:
// app/port/schedule/Demo.ts import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>({ type: ScheduleType.WORKER, scheduleData: { interval: 100, // Execute every 100ms // interval: '5s', // Execute every 5s }, }) export class IntervalScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }在底层实现中,TimerStrategy.getNextTick()(见 timer.ts)对 interval 模式直接调用ms(this.scheduleConfig.interval)换算成下一次执行的毫秒数,随后通过safeTimeout触发下一次调度,形成固定频率的执行循环。
Cron 表达式模式
cron模式按照 cron 表达式规则执行任务,表达式语法遵循 cron-parser 中通过cronParser.parseExpression(cron, cronOptions)解析)。Egg 的 cron 表达式支持6 位字段(包含秒),各字段含义如下:
* * * * * * ┬ ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ | │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) │ │ │ │ └───── month (1 - 12) │ │ │ └────────── day of month (1 - 31) │ │ └─────────────── hour (0 - 23) │ └──────────────────── minute (0 - 59) └───────────────────────── second (0 - 59, optional)以下示例在每台机器上每天凌晨 3 点执行一次:
// app/port/schedule/CronDemo.ts import { Inject, Logger } from 'egg'; import { CronParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<CronParams>({ type: ScheduleType.WORKER, scheduleData: { // Execute once daily at 3 AM cron: '0 0 3 * * *', // Execute every 5 seconds // cron: '*/5 * * * * *', }, }) export class CronSubscriber { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }从类型定义(schedule.ts)可以看到CronParams还支持cronOptions字段,用于向 cron-parser 传递currentDate、startDate、endDate、tz(时区)等解析选项。在 timer.ts 的 cron 分支中,框架会循环调用cronInstance.next()不断向后推进时间,直到找到一个晚于当前时刻的触发点,再据此计算nextTick;若表达式超出endDate时间范围,则打印日志并停止调度。
两种执行模式:worker 与 all
常规定时任务一般使用worker模式,即每台机器上只有一个 worker 进程会执行该任务;框架同时提供all模式,用于需要每台机器上所有 worker 都执行的场景:
worker模式:每台机器上仅有一个 worker 执行,每次执行时由 agent随机选择哪个 worker;all模式:每台机器上的每个 worker 都会执行该任务。
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>({ type: ScheduleType.ALL, // All workers will execute scheduleData: { interval: 100, }, }) export class AllScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }这一行为差异在底层由两套策略类实现(见 agent.ts):
WorkerStrategy(worker.ts):handler()调用this.sendOne(),在 base.ts 中通过this.agent.messenger.sendRandom('egg-schedule', info)将任务随机发送给某一个 worker;AllStrategy(all.ts):handler()调用this.sendAll(),在 base.ts 中通过this.agent.messenger.send('egg-schedule', info)广播给所有 worker。
每次触发时,agent 都会为任务生成一个唯一 job id(getSeqId(),由时间戳、高精度计时与计数拼接而成),并记录[Job#${id}] ${key} triggered, send random/all by agent日志。worker 侧收到egg-schedule消息后(见 app.ts),会等待应用 ready、创建匿名 Context 并执行schedule.task(ctx, ...info.args),随后把执行结果(成功与否、耗时rt、错误信息)通过messenger.sendToAgent回传给 agent 完成闭环。
任务运行参数(ScheduleOptions)
Schedule装饰器还支持第二个参数用于指定任务运行参数(对应类型定义中的ScheduleOptions,见 schedule.ts):
| 参数 | 类型 | 说明 |
|---|---|---|
immediate | boolean | 为true时,应用启动并 ready 后立即执行一次任务(默认false) |
disable | boolean | 为true时,定时任务不启动(默认false) |
env | string[] | 指定只在特定环境(如devserver、test、prod)下才启动该任务 |
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>( { type: ScheduleType.WORKER, scheduleData: { interval: 100, }, }, { immediate: true, // Execute once immediately after app starts and becomes ready // disable: true, // When true, the scheduled task will not start env: ['devserver', 'test'], // Only run in offline environments }, ) export class ParamScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }这些参数在框架各阶段生效:
env过滤发生在加载阶段:load_schedule.ts 中,若配置了env数组且当前app.config.env不在其中,则直接忽略该任务并打印ignore schedule ... due to schedule.env not match日志。tegg 侧对应 ScheduleMetadata.shouldRegister() 的实现;disable过滤发生在注册阶段:schedule.ts 的registerSchedule中,schedule.disable为true的任务不会创建对应策略实例,worker 侧收到消息后也会再次校验并打印disable日志;immediate立即执行发生在启动阶段:timer.ts 的start()中,若开启immediate则通过setImmediate(() => this.handler())立即触发一次,否则进入scheduleNext()等待下一次触发点。
另外,TimerStrategy构造函数(timer.ts)会断言interval、cron、immediate三者至少存在其一,否则抛出错误提示,避免写出无法触发的空任务。
装饰器到插件执行链的完整打通
作为补充,可以看看装饰器侧如何与@eggjs/schedule插件衔接。Schedule装饰器本身(Schedule.ts)所做的工作包括:
- 通过
ScheduleInfoUtil将isSchedule标记、调度参数(ScheduleParams)与运行选项(ScheduleOptions)写入类元数据; - 将该类注册为
SingletonProto单例 Bean(accessLevel: AccessLevel.PUBLIC); - 通过
PrototypeUtil.setFilePath记录类的源文件路径,供后续importResolve归一化 key。
tegg 插件侧(tegg/plugin/schedule)的SchedulePrototypeHook与ScheduleWorkerLoadUnitHook会在 Bean 原型加载阶段感知到带isSchedule标记的类,由ScheduleManager(ScheduleManager.ts)将其包装为{ schedule, task, key }结构并注册进 worker 侧的scheduleWorker.registerSchedule(),最终落入与文件扫描方式相同的ScheduleWorker注册表(schedule_worker.ts)。这正是装饰器任务与app/schedule文件任务能在同一套 agent 调度链路中共存的原因,也再次印证了“两条注册路径必须避免重复声明”的警告。
配置参考与日志
@eggjs/schedule插件的默认配置(config.default.ts)如下:
- 注册了一个独立的
scheduleLogger自定义日志器,consoleLevel: 'NONE'(控制台不输出),日志写入egg-schedule.log文件; schedule.directory默认为空数组,可配置为自定义额外扫描目录的完整路径(与默认的app/schedule目录叠加)。
因此,排障时可以重点关注egg-schedule.log中的以下关键日志:
register schedule <key>:任务成功注册;[Job#<id>] <key> triggered, send random/all by agent:agent 已触发任务;[Job#<id>] <key> executing by app:worker 开始执行;[Job#<id>] <key> execute succeed/failed, used <rt>ms:任务执行结果与耗时。
借助这些日志,可以快速确认任务是未注册(env/disable过滤)、未触发(cron 表达式问题)还是执行失败(业务异常),从而完成从配置到运行的全链路排障。
小结
本文完整覆盖了 Egg tegg 定时任务的装饰器使用方式:开启teggSchedule插件 → 在app/port/schedule/目录编写带@Schedule装饰器与subscribe方法的类 → 按需选择interval(毫秒/时间字符串)或cron(6 段表达式)触发模式 → 通过type决定worker(每机单 worker 随机执行)或all(每机全 worker 执行)→ 通过第二参数immediate、disable、env控制任务运行时机。在此基础上,本文结合 plugins/schedule 与 tegg/core/schedule-decorator、tegg/core/types 的源码,说明了 agent 调度、worker 分发、参数过滤与日志埋点等底层机制,帮助你既会写、也能排查。
【免费下载链接】egg🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考