Convex 调度实战:用 cronJobs 定时任务与 scheduler.runAfter 构建自毁消息应用
数据库后端【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址https://gitcode.com/gh_mirrors/co/convex-backend点击查看免费下载导读本文以 Convex 官方调度示例应用npm-packages/private-demos/cron为主线系统讲解 Convex 的两大调度能力由cronJobs()驱动的周期性定时任务interval/hourly/daily/weekly/monthly 及标准 cron 表达式以及由scheduler.runAfter驱动的一次性延时任务。读完本文你将掌握在 Convex 中声明式配置后台定时任务、在 mutation 内动态编排延时逻辑的完整套路并能结合后端crates/application/src/cron_jobs/mod.rs理解其执行器原理。一、示例应用总览一个会自我销毁的聊天室该示例是一个基于 React Vite 的迷你聊天应用演示了两类调度场景的典型组合周期性后台任务定时清理消息、重置高分、记录时间戳、发送邮件等全部声明在convex/crons.ts中由 Convex 后端自动触发无需任何外部 cron 服务一次性延时任务发送的每条消息会在 5 秒内逐秒更新倒计时文案倒计时结束自动删除——实现这个自毁消息效果完全依赖scheduler.runAfter见convex/sendExpiringMessage.ts。该应用基于 Convex 官方 tutorial 聊天室改造而来参见 npm-packages/demos/tutorial在原有发消息-展示消息流程之上叠加了调度逻辑是学习 Convex scheduling 的最小可运行样本。数据模型示例仅使用两张表convex/schema.tsexport default defineSchema({ messages: defineTable({ author: v.string(), body: v.string(), }), times: defineTable({ time: v.number(), }), });messages聊天消息自毁逻辑作用于此表times由recordTime定时任务每秒写入一条Date.now()时间戳供getTimes查询验证定时任务确实在按预期频率执行。二、运行示例应用README 给出的启动方式只有两步在npm-packages/private-demos/cron目录下just install-js npm run devjust install-js是仓库 Justfile 提供的 JavaScript 依赖安装入口用于安装本仓库 npm 工作区所需的全部依赖。npm run dev对应package.json中的脚本scripts: { dev: convex dev --start vite --open }convex dev会同时启动两件事一是本地 Convex 后端自动监听convex/目录的代码变更并热更新包括新增/修改 cron 配置二是通过--start vite --open拉起 Vite 开发服务器并自动打开浏览器页面。前端入口页面src/App.tsx用useQuery(api.listMessages.default)实时订阅消息列表用useMutation(api.sendMessage.default)发送新消息并展示每条消息的_creationTime。启动后可以观察两类现象来验证调度生效聊天页面发送任意消息5 秒内消息正文会逐秒追加倒计时并最终消失打开times表或后端日志可以看到recordTime每秒写入一条记录。三、周期性定时任务cronJobs 声明式配置全解析Convex 的定时任务不需要操作系统级 cron也不依赖任何外部调度器。你只需在convex/目录下创建crons.ts注意文件名是固定约定导出cronJobs()的实例后端便会自动注册并调度这些任务。示例 convex/crons.ts 完整覆盖了所有内置调度频率import { cronJobs } from convex/server; import { api } from ./_generated/api; const crons cronJobs(); crons.interval(15s task, { seconds: 10 }, api.sendEmail.default); crons.interval( clear presence data, { seconds: 10 }, api.clearPresence.default, { a: 12, }, ); crons.hourly( Clear at the top of the hour, { minuteUTC: 0, }, api.clearMessage.default, { n: 1 }, ); crons.daily( Daily high score reset, { hourUTC: 17, // (9:30am Pacific/10:30am Daylight Savings Pacific) minuteUTC: 30, // no timezone support yet }, api.clearHighScore.default, ); crons.weekly( Weekly re-engagement email, { dayOfWeek: tuesday, hourUTC: 17, minuteUTC: 30, }, api.sendEmail.default, ); crons.monthly( Clear a message once a month, { day: 1, hourUTC: 17, minuteUTC: 30, }, api.clearMessage.default, { n: 1 }, ); crons.cron(clear a message, 0 10 * * 2, api.clearMessage.default, { n: 1 }); crons.cron( fancier cron job!, 10-30/5 10 1-3 * *, api.clearMessage.default, { n: 1, }, ); crons.interval(record time, { seconds: 1 }, api.recordTime.default); export default crons;3.1 六种注册方式与参数语义方法用途调度参数示例触发频率crons.interval(name, { seconds }, fn, args?)固定间隔重复{ seconds: 10 }/{ seconds: 1 }每 10 秒 / 每 1 秒crons.hourly(name, { minuteUTC }, fn, args?)每小时整点{ minuteUTC: 0 }每小时第 0 分钟crons.daily(name, { hourUTC, minuteUTC }, fn, args?)每天指定时刻{ hourUTC: 17, minuteUTC: 30 }每天 UTC 17:30crons.weekly(name, { dayOfWeek, hourUTC, minuteUTC }, fn, args?)每周指定时刻{ dayOfWeek: tuesday, hourUTC: 17, minuteUTC: 30 }每周二 UTC 17:30crons.monthly(name, { day, hourUTC, minuteUTC }, fn, args?)每月指定时刻{ day: 1, hourUTC: 17, minuteUTC: 30 }每月 1 日 UTC 17:30crons.cron(name, 分 时 日 月 周, fn, args?)标准 cron 表达式0 10 * * 2/10-30/5 10 1-3 * *见表达式每个方法统一接收四个参数任务名称唯一标识字符串如15s task。修改名称会改变任务的持久化身份与任务的重建行为相关调度配置对象或 cron 表达式见上表目标函数通过api引用如api.sendEmail.default——即convex/sendEmail.ts中的default导出可选传给目标函数的参数如{ n: 1 }、{ a: 12 }由 Convex 的校验器validator在目标函数执行前进行参数校验。示例中对同一目标函数api.clearMessage.default注册了多个 cronhourly、monthly 与两条 cron 表达式任务说明同一函数可以被多个定时任务复用只需传不同参数。3.2 各任务在示例中的实际作用对照convex/目录下的函数实现recordTime每 1 秒recordTime.ts每次向times表插入Date.now()是最直观的调度是否在跑的探针sendEmail每 10 秒 / 每周二sendEmail.ts模拟发送邮件示例中为占位逻辑实际项目可替换为真实邮件服务调用clearPresence每 10 秒clearPresence.ts清理过期 presence 数据并演示了带参数调用{ a: 12 }clearMessage每小时 / 每月 / cron 表达式clearMessage.ts的defaultmutation 接收{ n: v.number() }通过db.query(messages).take(n)取前 n 条消息并逐一删除是定时清理的标准范式clearHighScore每天 UTC 17:30clearHighScore.ts打印清理日志演示了时区换算注释——hourUTC: 17, minuteUTC: 30对应太平洋时间 9:30/10:30。3.3 时区与表达式细节时区所有hourUTC/minuteUTC均以UTC为准。源码注释明确写着// no timezone support yet——目前没有时区支持开发者需自行完成本地时间到 UTC 的换算这是使用daily/weekly/monthly时必须注意的坑weekly的dayOfWeek取值如tuesday小写英文星期名示例中以tuesday演示标准 cron 表达式格式为分钟 小时 日 月 星期五个字段。0 10 * * 2表示每周二 10:00 UTC10-30/5 10 1-3 * *演示了范围10-30、步长/5与多值域1-3的组合即每月 1 至 3 日的 10:10–10:30 之间每 5 分钟触发一次。四、一次性延时任务scheduler.runAfter 与自毁消息与周期性 cron 不同scheduler是注入到 mutation 上下文中的调度句柄用于在运行时动态编排一次性任务。示例 convex/sendExpiringMessage.ts 给出了一个完整自洽的延时链式调度示例// snippet start self-destructing-message function formatMessage(body: string, secondsLeft: number) { return ${body} (This message will self-destruct in ${secondsLeft} seconds); } export default mutation( async ( { db, scheduler }, { body, author }: { body: string; author: string }, ) { const id await db.insert(messages, { body: formatMessage(body, 5), author, }); await scheduler.runAfter(1000, api.sendExpiringMessage.update, { messageId: id, body, secondsLeft: 4, }); }, ); export const update mutation({ args: { messageId: v.id(messages), body: v.string(), secondsLeft: v.number(), }, handler: async ({ db, scheduler }, { messageId, body, secondsLeft }) { if (secondsLeft 0) { await db.patch(messageId, { body: formatMessage(body, secondsLeft) }); await scheduler.runAfter(1000, api.sendExpiringMessage.update, { messageId, body, secondsLeft: secondsLeft - 1, }); } else { await db.delete(messageId); } }, }); // snippet end self-destructing-message其执行链路非常清晰是典型的递归调度模式用户发送消息defaultmutation 写入消息并附带(This message will self-destruct in 5 seconds)文案立即调用scheduler.runAfter(1000, api.sendExpiringMessage.update, { messageId, body, secondsLeft: 4 })安排 1 秒后执行update1 秒后update被触发若secondsLeft 0用新的倒计时文案db.patch更新消息并再次runAfter调度自己secondsLeft - 1形成每秒一次的链式倒计时当secondsLeft归零走else分支db.delete(messageId)消息自我销毁。4.1 scheduler.runAfter 的 API 语义签名scheduler.runAfter(delayMs, functionReference, args?)delayMs单位为毫秒示例中1000即 1 秒只能调度mutation不能调度 query因为被调度的函数会执行写操作由_generated/api的类型系统保证函数引用与参数类型的编译期校验scheduler同样可配合runAt(timestamp, ...)使用在指定时刻触发runAfter是其相对时间版本。4.2 基于 runAfter 的延时清理扩展clearMessage.ts还导出了一个scheduleClearDatamutation演示用runAfter实现N 秒后清理 N 条消息的延迟批量删除export const scheduleClearData mutation({ args: { n: v.optional(v.number()), }, handler: async ({ scheduler }, { n 10 }) { await scheduler.runAfter(n * 1000, api.clearMessage.default, { n: 1 }); }, });注意这里把延时值换算成毫秒n * 1000并在触发时复用同一文件中的default即批量删除前 n 条消息的 mutation。这种调度入口 清理执行的分离设计与 cron 部分共用目标函数体现了调度逻辑与业务逻辑解耦的实践。五、后端执行器原理CronJobExecutor 如何驱动这些任务定时任务在 Convex 中由后端进程内的执行器负责到期即跑。核心实现位于 crates/application/src/cron_jobs/mod.rs 的CronJobExecutorRT第 111 行起pub struct CronJobExecutorRT: Runtime { context: CronJobContextRT, running_job_ids: HashSetResolvedDocumentId, /// Some if theres at least one pending job. May be in the past! next_job_ready_time: OptionTimestamp, job_finished_tx: mpsc::SenderResolvedDocumentId, job_finished_rx: mpsc::ReceiverResolvedDocumentId, }从源码结构可以读出几个关键设计点持久化的任务状态任务元数据CronJob 定义、下一次运行时间、执行状态/结果都落在数据库中模型层由 crates/model/src/cron_jobs 的CronModel管理涉及CronJob、CronJobState、CronJobStatus、CronNextRun、CronJobResult、CronJobLogLines等类型下一次触发时间的计算通过model::cron_jobs::next_ts::compute_next_ts计算配合stream_cron_jobs_to_run拉取所有已到触发时间的任务执行器维护next_job_ready_time记录最近一个待执行任务的时间可能已经过期配合job_finished_tx/rx通道在任务完成后立即重新评估下一轮调度实现准点触发而非轮询任务去重running_job_ids: HashSetResolvedDocumentId保证同一任务不会并发重复执行失败重试与背压INITIAL_BACKOFF 500ms、MAX_BACKOFF 15s提供指数退避并行度由 knobs 中的SCHEDULED_JOB_EXECUTION_PARALLELISM控制遇到 OCC 冲突时按UDF_EXECUTOR_OCC_MAX_RETRIES重试日志截断CRON_LOG_MAX_RESULT_LENGTH与CRON_LOG_MAX_LOG_LINE_LENGTH均为 1000任务结果与日志行会被截断源码注释说明这些日志仅供 dashboard 展示使用与 ScheduledJobExecutor 的相似性源码注释指出这段代码与 ScheduledJobExecutor 非常相似将来可能重构合并——也就是说cronJobs与scheduler.runAfter/runAt最终都汇入同一套持久化调度 到期执行的基础设施。被触发的任务会经由ApplicationFunctionRunner以 mutation 的形式执行crates/application/src/application_function_runner并记录函数执行日志因此你在 dashboard 的函数日志中可以看到每次 cron 触发的记录。六、小结与上手建议通过这个示例Convex 调度体系的核心用法可以归纳为三条固定周期任务用crons.ts在convex/crons.ts中export default cronJobs()按需组合interval/hourly/daily/weekly/monthly/cron六种方式后端自动持久化调度无需自建 cron 服务运行时一次性任务用scheduler.runAfter/runAt在 mutation 上下文中动态安排延时执行配合链式自我调度即可实现倒计时、超时清理、延迟通知等场景调度与业务解耦多个 cron 可复用同一函数如示例反复调用api.clearMessage.default并传不同参数调度入口只负责何时触发业务函数只负责做什么。如需进一步阅读可在本仓库中对照示例应用源码目录npm-packages/private-demos/cron调度定义convex/crons.ts延时自毁实现convex/sendExpiringMessage.ts后端执行器crates/application/src/cron_jobs/mod.rs任务持久化模型crates/model/src/cron_jobs把crons.ts里的recordTime从 1 秒改成 10 秒、或新增一条0 8 * * *的早上定时任务观察 dashboard 中的函数执行记录即可快速验证你对这套调度体系的理解。赞分享数据库后端【免费下载链接】convex-backendThe open-source reactive database for app developers项目地址https://gitcode.com/gh_mirrors/co/convex-backend点击查看免费下载相关推荐Convex Scheduling 实战基于 convex-backend 实现 5 秒自毁消息示例应用Convex Scheduling 实战基于 convex backend 实现 5 秒自毁消息示例应用 导读 本文基于 convex backend 仓库中数据库后端Convex 函数编写与定时任务实战从 query/mutation 模板到 cronJobs 定时清理Convex 函数编写与定时任务实战从 query/mutation 模板到 cronJobs 定时清理 导读 本文以 convex backend 仓库中数据库后端Welcome UI贡献指南如何为开源设计系统提交组件PRWelcome UI贡献指南如何为开源设计系统提交组件PR Welcome UI是一个基于React、TypeScript和Tailwind CSS构建的可定数据库后端上一篇SAE-Res-Qwen3.5-9B-Base-W64K-L0_50入门指南从安装到首次特征提取的完整教程下一篇ChatGLM2-6B部署指南MindSpore环境配置与常见问题解决创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考