定时任务 Jobs
Jobs 用于随应用运行的 Cron 定时任务,例如清理过期数据、生成日报或同步业务数据。任务文件负责计划,Service 负责业务;启动应用后,框架自动发现任务并等待未来计划点。
默认启用。一个应用进程和一个内置 Cluster 都无需 Redis;多个独立应用需要共同协调时,再显式配置共享 Jobs Redis。Jobs 不提供任务队列、自动重试、停机补跑、启动立即执行或手动触发(run-now)。
创建第一个任务
前提:已有能正常启动的 VextJS 应用。在默认目录创建:
只需必填的 cron 和 handler,无需补写全局 Jobs 配置。
- 执行项目已有的开发命令,例如
npm run dev。 - 等待必要初始化完成,检查 Jobs 日志中的 discovered、enabled 数量和协调模式。
- 等待下一个 5 秒计划点,查看
Heartbeat job executed和框架完成日志。
这里的六字段 Cron 包含秒。任务不会启动即执行;scheduledAt 是计划时间,实际开始可能稍晚。HTTP 已监听不等于 Jobs 已激活或 handler 已执行。构建后使用项目已有的 npm run build、npm start,框架从构建输出发现相应任务。
开发环境也会运行任务;不希望开发机执行业务时,使用后文的环境开关。
调用 Service 与协作取消
Job 可以直接调用已就绪的 app.services。下面两份文件演示完整的 Service 接入,不依赖外部接口或数据库:
这个演示 Service 只记录调用成功,不会删除订单。在真实项目中,用已有 order.cleanup() 的业务实现替换它;数据库操作、事务和业务幂等由 Service 负责。
先按服务类型生成执行 npm exec -- vext typegen,确保项目生成的类型包含 app.services.order,再创建:
此任务每天上海时间 03:00 调用 Service。首次运行仍等待未来计划点;验证接入时可暂用 */5 * * * * *,观察两条业务日志后改回业务计划。
入口检查一次 signal 只能取消尚未开始的工作。长任务应在循环及异步步骤之间持续检查,把 signal 传给支持取消的 I/O;驱动不支持时使用其原生取消能力。框架无法强行中断忽略取消的函数,也不能撤销已经发生的副作用。
Job 完整配置
例如,以下是替换上面任务文件的完整配置,不是在同一文件追加第二个 Job。它依赖已注册的 order Service:
leaseTtl 不是 handler 超时:Redis 模式执行期间会续租,任务可以运行超过 TTL。任务级值覆盖全局 Redis TTL;只在需要区别于默认租约策略时设置。
handler 上下文
cron 与时区
cron 表达式
支持 @yearly、@annually、@monthly、@weekly、@daily、@midnight、@hourly;@daily 表示有效时区的每天午夜。日历扩展由所安装 Croner 解析,例如 0 0 L * *(每月最后一天)、0 0 1W * *(最接近每月 1 日的工作日)、0 9 * * MON#2(每月第二个周一)。工作日表达式不包含节假日日历。
日期与星期同时填写时,默认 OR。 0 0 1 * MON 匹配每月 1 日或周一;0 0 1 * +MON 使用显式 + 表示同时满足。以 UTC 的 2026 年 1 月 2 日为起点,前者下次为 1 月 5 日,后者为 6 月 1 日。
时区优先级为 Job.timezone > jobs.timezone > UTC。Asia/Shanghai 的 09:00 对应 UTC 01:00;日志 ISO 时间中的 Z 表示 UTC。
夏令时可能使本地时刻缺失或重复,实际计划由所用 Croner 计算。当前依赖中,纽约 30 1 * * * 从 2026-11-01T05:29:59Z 计算得到 05:30Z,下一点为次日 06:30Z,不会当日再执行第二次 01:30。春季 30 2 * * * 从 2026-03-07T08:00Z 计算得到次日 07:30Z(当地 03:30),从 2026-03-08T07:00Z 计算则得到次日 06:30Z。涉及 DST 的业务应验证地区、起点和边界日期;需要固定绝对时间可选 UTC。
计划类别与未来点
只接受 Cron 字符串,拒绝 interval、Date 和 ISO 一次性日期,不提供毫秒固定间隔。启用任务在准备和实际激活时都必须有严格未来点;有限年份计划执行完最后一点后自然停止。关闭任务仍校验 Cron 语法,但无需未来点。
例如每分钟任务在 12:00:20 就绪,等待 12:01:00;正好在 12:01:00 就绪则等待 12:02:00。已经注册的 12:01 点稍晚到 12:01:02、仍未跨下一窗口时可执行;跨周期则跳过。启动、重启、故障恢复和 onReady 等待期间经过的历史点均不补跑。
发现、导出与名称
任务目录只需要真实的任务文件,例如:
默认递归扫描 .ts/.js/.mjs/.cjs/.mts/.cts。每个选中文件恰有一个默认 defineJob();普通 helper、类型、常量可以共存,业务辅助模块通常放在任务目录外。
忽略规则单独说明:下划线开头的文件名、声明文件、.test/.spec 模块不会被选中;下划线目录本身不自动忽略,.tsx/.jsx 也不在默认包含范围。
显式 name 优先,否则按相对任务目录的路径去扩展名、去末尾 /index、把路径分隔符转为点;根 index.ts 名为 index,order/index.ts 名为 order。不会把 kebab-case 转为 camelCase。名称允许 ASCII 字母、数字及 _ . : -,必须非空、全局唯一;关闭定义也参与重名检查。
一个文件不能通过多个命名 Job 注册多个任务,也不能返回任务数组或用工厂隐藏默认声明。普通辅助导出允许。JavaScript/ESM 同样默认导出;CommonJS 使用等价形式:
有限的相对默认转导出、默认别名及其他精确声明约束见 API 的定义规则。业务放在 handler 中,不要在模块顶层执行业务或私建定时器。
自定义目录与过滤
把配置片段合并到已有 src/config/default.ts:
dir 相对 src,此例发现 src/tasks/**,构建后对应输出根下的 tasks。不要写 src/tasks、绝对路径或 ..。include 替换默认包含,exclude 在内置忽略上追加排除。它们匹配实际文件名,不会把 **/*.ts 自动改成生产的 .js;需要同时匹配源码与输出时包含两者。include: [] 不发现任务。
按环境启停与全局配置
完整配置可以写成下面的配置片段,合并到项目现有配置;通常只需设置与默认值不同的字段:
开发机整体关闭:
只关闭一个任务,在其定义中设置 enabled: false。全局关闭跳过任务发现和模块导入;单任务关闭仍导入、校验定义、TTL 和名称。两者都不跳过配置合法性校验。配置按现有 profile 合并规则生效,开发覆盖不会自动关闭生产任务。
选择部署方式
例如一个内置 Cluster 开启 4 个 Worker:
不需要 Redis、Worker ID 或内部 IPC 配置。所有 Worker 完成必要初始化,Master 核对实际就绪集合的任务定义后,逐个激活 Jobs;每个 Worker 获得激活后才允许调度。Master 收齐有效激活确认后报告整体就绪,单个已激活 Worker 可能更早注册 Timer。任务清单不一致或启动失败时按错误排查,不会降级为各 Worker 独立执行。
多个独立副本共享 Redis
多个容器各自运行一个内置 Cluster,仍属于多个独立应用,需要共享协调时使用 Redis。把以下片段合并到每个副本的配置:
在各副本启动环境中设置指向同一实际共享服务的 VEXT_REDIS_URL,例如 redis://redis.internal:6379/0;redis.internal 是示意主机名,替换为实际地址。127.0.0.1 仅适合同一台机器的测试,不能表示不同主机共用 Redis。框架不因存在环境变量就自动开启 Redis;需要显式 jobs.redis 对象。已有环境 URL 时,也可使用:
连接优先级为 client > url > uri > VEXT_REDIS_URL > REDIS_URL。无效 client 不回退 URL;显式空 url 不会继续取环境变量。有启用任务但没有可用 Redis 地址/客户端,或连接初始化失败时拒绝启动。
默认 namespace 组合包名、VEXT_CONFIG(缺省 default)和 NODE_ENV(缺省 production),标准 CLI 提供 profile/运行模式;默认逻辑前缀为 vext:<namespace>:job:。自动值适合同一应用的普通副本,不需要到处配置。不同包名、profile 或运行模式默认隔离;如果有意让不同项目共享任务协调,可显式设置同一 namespace:
同一协调域中的副本须保持 Redis 目标、有效前缀、任务名、Cron、有效时区和 TTL 一致。移动任务文件、改变任务名/namespace/keyPrefix 会改变身份,混合部署可能两边都执行。Jobs 不自动复用 cache、Session 或限流的 Redis 配置。传入客户端、Redis Cluster、权限和旧键运维见进阶部署。
调整单个任务的租约
有效租约优先级为 Job.leaseTtl > jobs.redis.leaseTtl > 30000 ms。在已有任务定义中设置 leaseTtl: 60_000 即覆盖上例全局的 30 秒;未设置的任务继续继承。无需为调整 TTL 修改任务名或 Redis 前缀。
执行期间自动续租,长任务不必将 TTL 设成全部执行时长。Local/IPC 模式不使用 Redis TTL,但仍要求字段合法。TTL 不能用来强制结束任务;shutdown.timeout 是秒,表示整个应用共享的关闭预算,也不是每个 Job 各自的执行超时。
重叠、失败与关闭
同一健康协调域里,同名任务同一计划点最多取得一次执行资格。取得资格后崩溃可能漏掉这次工作,快速完成或租约过期不会把已消费点变成可重新执行的任务。内置 IPC 状态不跨新的 Master 持久化;网络分区、Redis 状态丢失或业务忽略取消也不能保证副作用恰好一次。关键业务使用唯一键、事务等幂等措施;可靠投递、历史和进度由业务存储或独立队列承担。
存在启用 Jobs 时,必要 onReady 失败会阻止启动并清理,避免任务调用未就绪依赖;无启用 Jobs 时保持原有日志隔离。观察性的 app:ready 回调不等同于必要初始化。开发中任务文件及已加载的相关依赖改变会冷重启,旧调度器先关闭,自定义任务目录也遵循此规则。
查看任务文档
应用已开启项目文档时,启动后直接访问 /docs 的 Jobs 分组;Jobs 文档源默认开启,不用再配置 openapi.docs.code.jobs: true。扫描范围默认逐字段继承 jobs.dir/include/exclude。
只关闭 Jobs 文档,将以下片段合并到现有配置:
可选地改变文档扫描目录,不改变实际任务发现或执行资格:
这个例子仅覆盖文档 dir,include/exclude 仍分别继承运行时配置。禁用任务可以展示,动态未知值保留未知,静态条目始终不证明已启动、已连接或已执行(runtimeVerified: false)。
摘要依次取 JSDoc 摘要、docs.summary、docs.description、description、默认摘要;描述取 JSDoc 描述、docs.description、description。标签合并顶层 tags、docs.tags 和 jobs。MCP 检查及 Recipe 同样只提供静态证据,生成任务必须显式提供 Cron;MCP 不运行 handler 或启动定时器。完整文档源字段见 API。
测试与观察
测试调度逻辑时,使用受控时钟而非等待真实时间:
helper 显式接收定义和初始时钟,只通过 tick() 驱动,不扫描项目任务、不创建真实 Timer;普通 createTestApp() 也不自动加载 Jobs。完整选项见测试 API。
然后用项目实际启动方式验证至少一个未来计划点。一个内置 Cluster 的 IPC 需要真实 Master/Worker 验证;独立副本共享 Redis 需要真实 Redis 集成验证,helper 不能替代它们。用真实 Redis 时使用真实计划时间,不用 1970 年的受控时钟申请资格。
启动日志包含发现/启用/禁用数量和 mode;执行日志包含 name、scheduledAt、PID/Worker、startedAt、duration、status 和 skipReason。nextRunAt 只是按当前配置计算的下一计划时间,不承诺必然执行。Redis 时钟偏差诊断给出 RTT 与偏差区间,不会修改系统时钟;各副本仍应保持时钟同步。