- 后端
- Web框架
【免费下载链接】egg
🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode
导读
本文围绕 tegg 提供的Schedule装饰器定时任务能力展开,讲解如何在 Egg 应用中以声明式方式注册普通定时任务:包括interval(固定间隔)与cron(表达式)两种触发模式、worker与all两种执行策略、immediate/disable/env三个任务参数,以及其底层"agent 调度 + messenger 派发 + worker 执行"的实现原理。读完本文,你将能够在任意 Egg + tegg 项目中安全、规范地编写可灰度、可开关、可多环境控制的定时任务,并理解其与框架自带app/schedule定时任务的差异。
使用场景:普通定时任务
tegg 的Schedule装饰器支持普通定时任务,其核心特征是:在每一台部署机器上都会执行。例如一个应用在生产环境最少部署 2 台机器,那么该定时任务在这 2 台机器上都会运行调度逻辑(具体由worker/all模式决定每个 worker 是否执行)。
典型适用场景包括:每台机器各自的数据清理、本地缓存预热、按机器维度执行的心跳上报等。如果需要"全局只执行一次"的分布式任务,则不属于本文讨论的普通定时任务范畴。
快速上手
开启插件
tegg 的定时任务插件是egg 内置插件,默认开启。若需显式声明,可在config/plugin.ts(或config/plugin.js)中配置:
export default { teggSchedule: true, };目录约定与重要警告
:::warning ⚠️不要将代码写在app/schedule路径下。egg 框架默认会扫描该路径并自动注册为传统格式的定时任务,与 tegg 的装饰器方式会产生冲突。 :::
tegg 的定时任务控制器应放在app/port/schedule目录(或其他 tegg 约定的业务目录)下,通过Schedule装饰器声明。
控制器结构要求
使用Schedule装饰器标识一个类为定时任务控制器后,该类必须包含一个名称为subscribe的方法。框架调度定时任务执行时,会调用该注解类的subscribe方法。方法签名对应 tegg 定义的ScheduleSubscriber接口:
export interface ScheduleSubscriber { subscribe(data?: any): Promise<any>; }subscribe应为 async 方法。从 load_schedule.ts 的实现可见,传统方式下 generator function 形式的任务会被直接拒绝("schedule" generator function is not support, should use async function instead),装饰器方式同样建议遵循异步函数约定。
interval 模式:按固定间隔执行
普通定时任务可配置为interval模式,即每间隔指定的时间在每台机器上执行一次。间隔时间通过Schedule装饰器的scheduleData.interval参数设置:
interval传值数字类型时,单位为毫秒数,例如100表示每 100ms 执行一次;interval传值字符类型时,会通过ms工具转换为毫秒数,例如'5s'表示每 5 秒执行一次。
// 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, // 每 100ms 执行一次 // interval: '5s', // 每 5s 执行一次 }, }) export class IntervalScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }底层实现:interval 如何被解析
从 timer.ts 的实现可以看到,interval在getNextTick()中通过ms(this.scheduleConfig.interval)统一换算为毫秒;若同时配置了cron,则优先按 cron 计算下一次触发时间。换算后使用safeTimers进行调度:
protected safeTimeout(handler: () => void, delay: number, ...args: any[]): number | ReturnType<typeof setTimeout> { const fn = delay < safeTimers.maxInterval ? setTimeout : safeTimers.setTimeout; return fn(handler, delay, ...args) as number | ReturnType<typeof setTimeout>; }当间隔超过safeTimers.maxInterval(约 24.8 天)时,会退化为使用safe-timers避免 setTimeout 的 32 位毫秒溢出问题——因此超长间隔(如数月一次)也能正确执行。
cron 模式:按 cron 表达式执行
普通定时任务同样支持 cron 表达式模式,表达式规则由cron-parser解析。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: { // 每日 3 点执行一次 cron: '0 0 3 * * *', // 每 5 秒执行一次 // cron: '*/5 * * * * *', }, }) export class CronSubscriber { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }cron 解析与越界处理
在 timer.ts 的构造函数中,cron会通过cronParser.parseExpression(cron, cronOptions)预解析,解析失败会抛出带 key 信息的TypeError:
throw new TypeError(`[@eggjs/schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, { cause: err, });每次触发后,getNextTick()会循环调用cronInstance.next()找到下一个未来时间点;若表达式超出时间范围(Out of the timespan range),则记录日志并停止调度:
do { try { const nextInterval = this.cronInstance.next(); nextTick = nextInterval.getTime(); } catch (err) { this.logger.info(`[Timer] ${this.key} cron out of the timespan range, error: %s`, err); return; } } while (now >= nextTick); return nextTick - now;同时,cronOptions支持透传cron-parser的ParserOptions(如currentDate、startDate、endDate、tz时区等),可通过scheduleData.cronOptions配置。
工作模式:worker 与 all
普通定时任务一般使用worker模式,框架也提供all模式,二者差异如下:
worker模式:每台机器上只有一个 worker会执行该定时任务,每次执行时 worker 的选择是随机的;all模式:每台机器上的每个 worker 都会执行该定时任务。
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>({ type: ScheduleType.ALL, // 所有 worker 都会执行 scheduleData: { interval: 100, }, }) export class AllScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }两种模式的派发差异(源码视角)
ScheduleType在 tegg/core/types/src/schedule.ts 中定义:
export const ScheduleType = { WORKER: 'worker', ALL: 'all', } as const;其执行策略分别在 strategy/worker.ts 与 strategy/all.ts 中实现,二者都是TimerStrategy的子类,仅handler()的派发目标不同:
// worker.ts —— 随机派发给一个 worker export class WorkerStrategy extends TimerStrategy { handler(): void { this.sendOne(); } } // all.ts —— 派发给所有 worker export class AllStrategy extends TimerStrategy { handler(): void { this.sendAll(); } }sendOne/sendAll位于 strategy/base.ts,通过 agent 的 messenger 分别调用sendRandom('egg-schedule', info)与send('egg-schedule', info)完成消息广播,并生成唯一的 Job id:
getSeqId(): string { return `${Date.now()}${process.hrtime().join('')}${this.count}`; }worker 侧收到消息后(见 app.ts 的egg-schedule事件处理),会等待app.ready(),然后创建一个匿名上下文执行任务:
const ctx = this.#app.createAnonymousContext({ method: 'SCHEDULE', url: `/__schedule?path=${key}&${schedule.scheduleQueryString}`, }); await this.#app.ctxStorage.run(ctx, async () => { return await schedule.task(ctx, ...info.args); });执行完毕后,将success、workerId、rt(耗时毫秒)等结果通过sendToAgent('egg-schedule', ...)回传给 agent 收尾。
定时任务参数:immediate / disable / env
普通定时任务的Schedule装饰器支持第二个参数,用于指定定时任务运行参数:
immediate:配置为true时,该定时任务会在应用启动并 ready 后立刻执行一次(随后再按 interval/cron 周期执行);disable:配置为true时,该定时任务不会被启动;env:数组,仅在指定的环境下才启动该定时任务。
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>( { type: ScheduleType.WORKER, scheduleData: { interval: 100, }, }, { immediate: true, // 在应用启动并 ready 后立刻执行一次 // disable: true, // 为 true 时,定时任务不会被启动 env: ['devserver', 'test'], // 仅在线下环境运行 }, ) export class ParamScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }三个参数的底层行为
ScheduleOptions类型定义同样位于 tegg/core/types/src/schedule.ts:
export interface ScheduleOptions { // default is false immediate?: boolean; // default is false disable?: boolean; // if env has value, only run in this envs env?: EggEnvType[]; }disable:在 schedule.ts 的registerSchedule()中,schedule.disable为 true 的任务会被直接跳过注册,agent 启动时也不会打印其注册日志(见 app.ts 中对disable的过滤);worker 侧若意外收到已禁用任务的派发消息,也会记录disable警告后直接返回。env:环境过滤发生在任务加载阶段(load_schedule.ts),当env为数组且不包含当前app.config.env时,任务会被忽略并记录日志:
const env = app.config.env; const envList = schedule.schedule.env; if (Array.isArray(envList) && !envList.includes(env)) { app.coreLogger.info(`[@eggjs/schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); continue; }immediate:在 timer.ts 的start()中,配置了immediate的任务会通过setImmediate(() => this.handler())立即派发一次,否则进入#scheduleNext()等待第一个周期。
常用配置项速查
teggSchedule插件的完整配置(对应 config/config.default.ts)如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
schedule.type | 'worker' \| 'all' | - | 任务执行策略(与装饰器参数一致) |
schedule.interval | string \| number | - | 间隔,数字为毫秒,字符串经ms解析 |
schedule.cron | string | - | cron 表达式 |
schedule.cronOptions | CronOptions | - | 透传给cron-parser的解析选项 |
schedule.immediate | boolean | false | 启动 ready 后立即执行一次 |
schedule.disable | boolean | false | 是否禁用该任务 |
schedule.env | string[] | - | 仅在指定环境启动 |
schedule.directory | string[] | [] | 追加自定义定时任务目录(绝对路径) |
其中schedule.directory用于扩展默认的app/schedule加载目录,加载时会合并loadUnits下的app/schedule与config.schedule.directory中的目录(见 load_schedule.ts)。
此外,插件内置了独立的scheduleLogger(输出到egg-schedule.log,consoleLevel为NONE),任务注册、触发、执行成功/失败及耗时都会写入该日志,方便在生产环境排查定时任务问题。
执行链路小结
一次普通定时任务的完整执行链路可以概括为:
- 加载:agent 启动时,
Scheduler.init()调用loadSchedule()扫描app/schedule及schedule.directory目录(load_schedule.ts); - 注册:
Scheduler.registerSchedule()按type找到对应的策略类(worker→WorkerStrategy,all→AllStrategy),disable的任务被跳过(schedule.ts); - 调度:
TimerStrategy.start()依据interval/cron/immediate计算下一次触发时间(timer.ts); - 派发:到达触发点后,
sendOne()随机选择一个 worker(messenger.sendRandom)或sendAll()广播给所有 worker(base.ts); - 执行:worker 收到
egg-schedule消息,等待app.ready()后创建匿名上下文调用subscribe(),并将执行结果回传 agent 记录日志(app.ts)。
注意事项
- 不要将 tegg 定时任务代码放在
app/schedule目录下,避免与框架默认的定时任务扫描注册冲突; subscribe必须为 async 函数,且不要使用 generator function 形式;worker模式下每次执行由 agent 随机选择 worker,任务逻辑不应依赖"具体由哪台机器执行";- 生产环境多机部署时,普通定时任务会在每台机器上各执行一次(
worker模式每机一次、all模式每机每个 worker 一次),需要全局唯一执行时应采用分布式任务方案,而不是依赖本功能去重; - 任务执行耗时与调度间隔应合理设置,避免长时间阻塞型任务与高频率
interval相互叠加造成资源占用。
- 后端
- Web框架
【免费下载链接】egg
🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode
相关推荐
终极指南:如何快速重置Cursor试用限制并恢复AI编程助手功能
终极指南:如何快速重置Cursor试用限制并恢复AI编程助手功能 你是否在使用Cursor AI编程助手时遇到了"You've reached your tri
后端Web框架Cloudflare Agents 任务调度完全指南:从延迟执行到 Cron 定时任务的 Schedule 体系详解
Cloudflare Agents 任务调度完全指南:从延迟执行到 Cron 定时任务的 Schedule 体系详解 导读 本文围绕 Cloudflare Ag
AI AgentAgent 框架后端云原生MCP 服务实时通信Hermes Agent终极指南:如何打造你的个人AI智能助手
Hermes Agent终极指南:如何打造你的个人AI智能助手 你是否曾经幻想过拥有一个全天候待命的AI助手,能够帮你处理各种复杂任务,从代码调试到文档整理,从
AI Agent人工智能AI 应用工具调用Agent 记忆交互助手RAG任务调度MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考