mcpField
C
convex-cron-jobs

convex-cron-jobs

github.com/waynesutton开发4052.8k 次安装4 周前更新
代码仓库

关于 convex-cron-jobs

面向 Convex 开发者的定时任务参考,说明如何配置周期 Cron 和一次性调度、批量处理及检查执行状态。重点包括仅调用 internal 函数、UTC 时间和任务重试等规则。

主要功能

  1. 01配置周期 Cron 与一次性任务
  2. 02使用 internal 函数作为任务目标
  3. 03分批处理并重新调度
  4. 04使用相对或绝对时间调度
  5. 05查看任务运行状态与日志
  6. 06处理时区、取消和重试

适用场景

  1. 01按固定间隔或日历时间运行工作
  2. 02在变更提交后安排后续工作
  3. 03分批处理大量记录
  4. 04排查未运行或时间不正确的 Cron

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-cron-jobs

安装后在 Claude Code 中输入/convex-cron-jobs确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
assets/
small-logo.svg
references/
cron-recipes.md
scheduling-patterns.md

详细介绍

Convex Cron 任务与调度

周期性工作写在 convex/crons.ts 中。一次性工作通过函数内部的 ctx.scheduler 进行调度。两者都必须调用 internal.* 函数,不能调用 api.*。

何时使用

  • 需要每隔 N 分钟运行,或在每天固定时间运行
  • 某个 mutation 需要在提交后启动后续工作
  • 某个任务涉及的行数超过单个 mutation 应处理的范围
  • Cron 显示在仪表板中,却从未运行,或运行时间不对
  • 需要取消、防抖或检查待处理任务

更多内容请参阅以下两个参考文件:

  • references/scheduling-patterns.md:需要了解带退避的重试、取消任务、读取 _scheduled_functions、防抖或本地时区时打开。
  • references/cron-recipes.md:需要简短且可直接复制的任务示例时打开,例如每日摘要邮件、会话过期、统计汇总、外部 API 同步。

crons.ts 骨架

一个文件,一个默认导出。对于“每隔 N”使用 crons.interval,对于日历时间使用 crons.cron。crons.daily、crons.hourly 和 crons.weekly 是已弃用的辅助函数。不要使用它们。

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

// Every hour
crons.interval(
  "expire sessions",
  { hours: 1 },
  internal.sessions.expireBatch,
  {},
);

// Every day at 09:00 UTC. Cron expressions are always UTC.
crons.cron("daily digest", "0 9 * * *", internal.digest.send, {});

export default crons;

每个条目都必须遵守以下规则:

  • 名称在文件中必须唯一。仪表板按此名称列出任务。
  • 从 ./_generated/api 导入 internal,即使目标函数定义在 crons.ts 中也是如此。
  • 参数是静态的,且必须符合目标函数的 args 验证器。
  • 间隔单位为 { seconds }、{ minutes } 或 { hours }。

Cron 表达式速查(分钟 小时 月份中的日期 月份 星期几):

表达式运行时间
*/15 * * * *每 15 分钟
0 * * * *每小时的 :00
0 0 * * *每天 UTC 00:00
0 8 * * 1每周一 UTC 08:00
0 0 1 * *每月 1 日
0 9-17 * * 1-5工作日 UTC 09:00 至 17:00,每小时运行

目标必须是内部函数

公共函数需要客户端、身份验证信息和不受信任的输入。Cron 和调度器调用不具备这些内容。公共目标会跳过你编写的身份验证检查,并让任何能访问此部署的人都能触发该任务。请使用 internalMutation、internalAction 或 internalQuery 注册目标。

一个分批处理的任务

Mutation 是一个受读写限制的事务。在循环中删除五万行会触及限制。获取固定大小的一批数据,然后使用 runAfter(0, ...) 重新调度自身,让任务链自行完成。

// convex/sessions.ts
import { internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

const BATCH = 100;

export const expireBatch = internalMutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    // Date.now() is fine in a mutation. Never call it in a query.
    const now = Date.now();
    const expired = await ctx.db
      .query("sessions")
      .withIndex("by_expiresAt", (q) => q.lt("expiresAt", now))
      .take(BATCH);

    await Promise.all(expired.map((s) => ctx.db.delete(s._id)));

    // A full batch means more may remain. Chain the next one.
    if (expired.length === BATCH) {
      await ctx.scheduler.runAfter(0, internal.sessions.expireBatch, {});
    }
    return null;
  },
});

这种方式之所以可行,是因为每次删除都会将该行从索引范围中移除。如果任务更新行,但不会将它们移出范围,请改用分页游标。参阅参考文件。

runAfter 与 runAt

// Relative: 5 minutes from now
const jobId = await ctx.scheduler.runAfter(
  5 * 60 * 1000,
  internal.reminders.send,
  { taskId: args.taskId },
);

// Absolute: a timestamp you already store (ms since epoch or a Date)
await ctx.scheduler.runAt(trial.endsAt, internal.billing.endTrial, {
  userId: trial.userId,
});
方法用途
runAfter(delayMs, fn, args)重试、后续任务、“十分钟后”
runAt(timestamp, fn, args)试用期结束、发送日期,以及任何有存储时间的事项

两者都会返回一个 Id<"_scheduled_functions">。如果之后可能需要取消任务,请将其存储在文档中。

请记住以下两点:

  • 在 mutation 中调度是事务性的。如果 mutation 抛出错误,就不会安排任何任务。在 action 中调度会立即生效,即使 action 随后失败也是如此。
  • 已调度的 mutation 只运行一次。已调度的 action 可能失败且不会重试,因此请为 action 添加重试逻辑,或使用重试组件。

在仪表板中查看运行记录

  • Schedules、Cron Jobs 标签页:显示 crons.ts 中的每个条目,以及上次运行和下次运行时间。
  • Schedules、Scheduled Functions 标签页:显示待处理的 runAfter 和 runAt 任务。
  • Logs,按函数名称筛选:显示每次执行、执行时长和任何抛出的错误。
  • 使用 CLI:npx convex logs 会输出相同的日志行。
  • 在代码中:ctx.db.system.get(jobId) 返回任务文档,其 state.kind 设置为 pending、inProgress、success、failed 或 canceled。

Cron 未触发时

  1. 文件名必须准确为 convex/crons.ts,并以 export default crons 结尾。
  2. npx convex dev 正在运行,且最近一次推送成功。Cron 变更只有在推送后才会生效。
  3. 目标是 internal.*,且参数与其验证器匹配。不匹配会在推送时失败。
  4. 表达式使用 UTC。比较之前,请先换算本地时间。
  5. 检查 Logs 中是否有抛出的错误。每次运行都会抛错的作业,看起来就像从未运行过。

常见错误

错误导致的问题应改为
crons.daily(...)已弃用的辅助函数crons.cron("...", "0 0 * * *", ...)
目标是 api.tasks.cleanup绕过身份验证,可公开调用注册为 internalMutation,使用 internal.tasks.cleanup
.collect() 后循环处理数千条记录超出事务限制使用 take(BATCH) 并重新安排
.withIndex("by_x").filter(...)筛选会扫描整个索引将范围条件放入 withIndex
在 internalQuery 中使用 Date.now()破坏缓存和响应性由调用方将 now 作为参数传入
将太平洋时间上午 9 点的 Cron 设为 "0 9 * * *"会在 UTC 上午 9 点运行使用 UTC,或每小时运行并检查本地时间
runAfter 未使用 await作业可能不会被安排始终对 ctx.scheduler.* 使用 await
两个 Cron 使用相同名称推送失败每个文件中的名称都必须唯一

检查清单

  • convex/crons.ts 只使用 crons.interval 和 crons.cron,并以 export default crons 结尾
  • 每个 Cron 和调度器目标都是 internal.*
  • 每个目标都有 args 和 returns 验证器
  • 处理大量行的作业会分批处理,并通过 runAfter(0, ...) 重新安排
  • 范围条件写在 withIndex 中,而不是 .filter 中
  • 查询中不使用 Date.now()
  • 每次调用 ctx.scheduler.* 都使用 await
  • Cron 小时使用 UTC 编写
  • 需要取消或防抖时,将作业 ID 存储在文档中
  • 已运行 npx convex dev,并在 Schedules 下看到作业

文档

评论

支持 Markdown