Convex Cron Jobs 实战指南:用cronJobs()定时清理数据(附示例应用源码解析)
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
本指南围绕开源仓库 convex-backend 中的npm-packages/demos/cron-jobs示例应用展开,讲解如何在 Convex 中使用内置的 cron 定时任务能力:每间隔一段时间自动执行 mutation 或 action,例如每分钟清空一次消息表。读完本文,你将掌握cronJobs()的完整用法、五种调度方式(interval/hourly/daily/weekly/monthly/cron字符串)的参数规则,并理解从客户端配置到服务端执行的完整链路。
示例应用概览:每 60 秒清空一次消息表
npm-packages/demos/cron-jobs是一个基于 Vite + React 的最小示例,它构建在仓库中的 Convex tutorial 教程示例 之上,只多了一样东西——cron 定时任务。它的行为非常直观:
- 用户在聊天界面发送消息;
- 消息写入
messages表; - 一个每 1 分钟触发一次的 cron 任务将
messages表清空。
前端界面上也明确写出了提示:"Messages will be cleared every minute"(消息每分钟会被清空),见 App.tsx。也就是说,这是一个"用完即清"的演示:既能验证定时任务的触发,又能避免演示数据无限堆积。
快速运行
示例应用使用 pnpm workspace 管理依赖(convex以workspace:*形式引入,见 package.json)。在npm-packages/demos/cron-jobs目录下执行:
npm install npm run dev其中dev脚本实际执行的是:
convex dev --start 'vite --open'(见 package.json)。这条命令会同时做两件事:
- 启动 Convex 本地开发后端,监听
convex/目录下的函数文件,热重载部署; - 启动 Vite 开发服务器并自动打开浏览器。
当convex/crons.ts被部署后,cron 调度器即开始生效——按文档说明,interval 类任务从首次部署到 Convex 时开始计时(源码注释见 cron.ts)。
核心代码逐行拆解
1. 定义 cron 任务:convex/crons.ts
示例的定时任务定义在 crons.ts:
import { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; const crons = cronJobs(); crons.interval( "clear messages table", { minutes: 1 }, internal.messages.clearAll, ); export default crons;关键点:
cronJobs()从convex/server导入,返回一个Crons实例(cron.ts);- 第一个参数
"clear messages table"是唯一标识符(cron identifier),源码规定必须是可打印 ASCII 字符(/^[ -~]*$/),且同一个文件中不允许重复注册,否则会抛出Cron identifier registered twice: ...(cron.ts); - 第二个参数
{ minutes: 1 }是调度配置; - 第三个参数
internal.messages.clearAll是要执行的函数引用——这里使用的是internal 函数(下文详述); - 文件必须
export default crons,Convex 后端才会识别并注册这些定时任务。
2. 定时执行的函数:convex/messages.ts
被 cron 调用的clearAll定义在 messages.ts:
export const clearAll = internalMutation({ args: {}, handler: async (ctx) => { for (const message of await ctx.db.query("messages").collect()) { await ctx.db.delete("messages", message._id); } }, });同文件还定义了聊天功能本身需要的两个函数:
export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db.query("messages").collect(); }, }); export const send = mutation({ args: { body: v.string(), author: v.string(), }, handler: async (ctx, { body, author }) => { const message = { body, author }; await ctx.db.insert("messages", message); }, });这里有一个值得注意的设计:cron 调用的clearAll用的是internalMutation而非普通mutation。internal 函数不能被客户端直接调用,只能由服务端(其他函数、scheduler、cron)调用,因此用于清理类任务更安全——用户无法通过 HTTP 端点绕过前端逻辑直接触发它。这也解释了为什么crons.ts中引用的是internal.messages.clearAll而不是api.messages.clearAll。
3. 前端:src/App.tsx
App.tsx 使用convex/react的useQuery/useMutation订阅api.messages.list和api.messages.send:
const messages = useQuery(api.messages.list) || []; const sendMessage = useMutation(api.messages.send);每当 cron 清空表后,useQuery会自动收到新的查询结果,消息列表随之变为空——不需要任何手动刷新。这也是观察 cron 是否生效的最直接方式:发送几条消息,等一分钟,列表被清空。
深入convex/server的cronJobs()实现
cronJobs()的完整实现位于 npm-packages/convex/src/server/cron.ts。Crons类内部维护一个Record<string, CronJob>,每次调用schedule()都会做三件事:解析参数、校验标识符唯一性、把{ name, args, schedule }存入crons表(cron.ts)。
六种调度方式与参数校验
| 方法 | 调度配置 | 参数要求 |
|---|---|---|
crons.interval(id, schedule, fn) | { seconds }/{ minutes }/{ hours }三选一 | 必须且只能指定其中一个,值为正整数(cron.ts) |
crons.hourly(id, schedule?, fn) | { minuteUTC?: 0-59 },可整体省略 | 省略minuteUTC时由后端在整点内随机分摊,避免所有任务挤在整点(cron.ts) |
crons.daily(id, schedule, fn) | { hourUTC: 0-23, minuteUTC?: 0-59 } | hourUTC必填且为 UTC 小时(cron.ts) |
crons.weekly(id, schedule, fn) | { dayOfWeek: "monday"..."sunday", hourUTC, minuteUTC? } | 星期必须是英文小写全称(cron.ts) |
crons.monthly(id, schedule, fn) | { day: 1-31, hourUTC, minuteUTC? } | 注意:大于 28 的日期在部分月份不会触发,例如 30 号在二月不运行(cron.ts) |
crons.cron(id, cronString, fn) | 标准 5 段 cron 表达式 | 字段依次为:分钟(0-59)、小时(0-23)、日(1-31)、月(1-12)、星期(0-6,周日为 0),如"15 7 * * *"表示每天 7:15 UTC(cron.ts) |
所有字段都在注册时进行严格校验,非法值会直接抛出Error。例如:
- 间隔不是正整数 →
Interval must be an integer greater than 0; - 小时超出 0-23 →
Hour of day must be an integer from 0 to 23; - 星期名拼写错误 →
Day of week must be a string like "monday".
更多调度示例
// 每 30 秒运行一次 crons.interval("cleanup", { seconds: 30 }, api.jobs.cleanup); // 每小时的 30 分运行(UTC) crons.hourly("reset scores", { minuteUTC: 30 }, api.scores.reset); // 每天 17:30 UTC 运行 crons.daily("digest", { hourUTC: 17, minuteUTC: 30 }, api.emails.sendDailyDigest); // 每周二 17:30 UTC 运行 crons.weekly( "weekly email", { dayOfWeek: "tuesday", hourUTC: 17, minuteUTC: 30 }, api.emails.send, ); // 每月 1 号 17:30 UTC 运行 crons.monthly( "bill customers", { day: 1, hourUTC: 17, minuteUTC: 30 }, api.billing.billCustomers, ); // 标准 cron 表达式:每天 7:15 UTC crons.cron("backup", "15 7 * * *", api.jobs.backup);interval、daily、weekly、monthly、cron都支持在函数引用后追加位置参数(...args),这些参数会被parseArgs解析并序列化为 JSON 传给目标函数(cron.ts)。
服务端如何执行 cron 任务
前端 SDK 的crons.ts只是"配置",真正把配置变成周期性执行的是 Rust 后端。
- 定时任务的调度与执行逻辑位于 crates/application/src/cron_jobs/mod.rs,其中引用了
model::cron_jobs模块的compute_next_ts、stream_cron_jobs_to_run等核心函数,以及CronJob、CronJobState、CronJobStatus、CronNextRun等类型,负责计算每个任务的下一次执行时间戳并按序触发; - 触发时以
InertIdentity(无用户身份的"系统身份")执行目标 UDF,并受SCHEDULED_JOB_EXECUTION_PARALLELISM(定时任务执行并行度)等 knobs 控制(mod.rs); - 任务执行结果会记录
CronJobResult与CronJobLogLines,便于在 Convex 仪表盘中查看每次运行的日志。
也就是说,客户端crons.ts里写的{ minutes: 1 }这类配置,最终会被后端持久化为带类型标签的调度计划({ type: "interval", minutes: 1 }),再由compute_next_ts依据当前时间计算出下一次执行时刻并排队执行。
排查与注意事项
- 确认任务已注册:
crons.ts必须export default crons,且标识符不能重复,否则部署时报错; - 区分 internal 与 public:cron 可以调用 public 的
mutation/action,也可以调用internalMutation/internalAction;示例选择 internal 是为了防止用户直接触发清理逻辑; - UTC 时区:
hourUTC、minuteUTC以及 cron 表达式均以UTC为准,配置前需要把本地时区换算为 UTC,避免任务在错误的时间触发(cron.ts); - 每月大日期陷阱:
monthly的day若大于 28,并非每个月都会运行; - 首跳时间:interval 任务从部署时刻起算,因此间隔是相对部署时间的,而非对齐到整点。
小结
通过npm-packages/demos/cron-jobs这个极简示例,可以看到 Convex cron 的完整工作模式:convex/crons.ts集中声明调度计划 → SDK 校验并序列化 → Rust 后端cron_jobs模块按compute_next_ts计算的下一次时间触发 → 目标函数以系统身份执行并记录日志。从"每分钟清空消息表"的玩具示例出发,你完全可以把它扩展为数据清理、定时报表、邮件提醒、缓存刷新等生产级定时任务。
【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址: https://gitcode.com/gh_mirrors/co/convex-backend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考