跳到主要内容

持久化调度

持久化调度存储在 cron 模块之上扩展出数据库持久化的 调度:集群内每次触发只执行一次、运维可在线编辑触发器、错触(misfire) 策略、运行流水账与崩溃恢复。内存调度器继续服务进程内任务;存储引擎是一套 独立机制,面向必须跨重启存活、跨节点协调的任务。

默认关闭。启用后从主数据源加载调度,并挂载 sys/cron/schedulesys/cron/run 资源:

[vef.cron.store]
enabled = true
auto_migrate = true # 启动时创建 crn_schedule / crn_fire_request / crn_run

模型

引擎由两个概念驱动:

  • 任务处理器cron.JobHandler)是启动时以唯一名称注册的 Go 代码。 处理器是"做什么"。
  • 调度cron.Schedule,表 crn_schedule)是持久化的触发器:何时 触发哪个任务、带什么参数、受哪些策略约束。调度是数据——可在代码中或 管理 API 中创建,运行期可编辑。

每次触发都以运行cron.Run,表 crn_run)形式记入流水账。

注册任务处理器

vef.ProvideCronJobHandler(func(svc *ReportService) cron.JobHandler {
return cron.NewTypedJobHandler("daily-report",
func(ctx context.Context, params ReportParams) error {
return svc.Generate(ctx, params)
},
// 可选:启动时若同名调度不存在则播种默认调度;
// 运维修改永远不会被覆盖。
cron.WithDefaultSchedule(cron.ScheduleSpec{
Trigger: cron.Expr("0 2 * * *", "Asia/Shanghai"),
}),
)
})
API契约
cron.JobHandlerName() string + Execute(ctx, execution) error;每个任务名恰好一个处理器
cron.NewJobHandler(name, execute, opts...)适配函数;execute 接收完整的 cron.Execution
cron.NewTypedJobHandler[P](name, execute, opts...)运行前把调度参数解码为 P;解码失败直接把该运行记为 failed 而不调用函数
cron.WithDefaultSchedule(spec)随处理器携带默认调度;存储在启动时按缺失播种。spec 的 Name 回退为任务名
cron.DefaultScheduleProvider可选的处理器能力,用于携带默认调度;存储在启动时按缺失播种
cron.JobHandlerOptionNewJobHandlerNewTypedJobHandler 接收的 option 类型
cron.Execution运行的只读视图:RunIDScheduleIDScheduleNameJobNameScheduledAt(逻辑触发时间)、Params(原始 JSON)、BindParams(v)

每次触发至多被一个节点认领,但当调度设置了 Recover 时,崩溃的运行会重新 触发——投递是 at-least-once,处理器应当幂等。

触发器

cron.TriggerSpec 声明调度何时触发;用构造函数创建:

构造函数类型语义
cron.Expr(expr, timezone)cron在 IANA 时区中求值的 cron 表达式。支持 5 段、6 段(前导秒)与 @ 描述符(@daily@every 90m)。时区为空解析为 UTC ——持久化调度永远不依赖节点的进程本地时区("Local" 被拒绝)。内嵌 tzdata 保证无 zoneinfo 的部署也能加载时区
cron.Every(duration)interval固定频率,最小 1scron.MinInterval)。频率锚定在调度起点(StartsAt,否则创建时间),触发相位不受补偿触发和手动触发影响
cron.Once(at)once单次触发

触发器类型与常量

API契约
cron.TriggerKind触发器类型的 string 判别值
cron.TriggerCroncron 表达式触发器类型
cron.TriggerInterval固定频率触发器类型
cron.TriggerOnce单次触发器类型
cron.DefaultTimezonecron 触发器未指定时区时的求值时区("UTC"
cron.MaxDurationMillisecondstime.Duration 能表示的最大整毫秒数;超过该值的频率/超时不支持 round-trip

触发器校验错误

错误触发条件
cron.ErrTriggerKindUnknown触发器类型不在 cronintervalonce 词汇表内
cron.ErrTriggerExprRequiredcron 触发器缺少表达式
cron.ErrTriggerExprInvalidcron 表达式无法解析
cron.ErrTriggerTimezoneInvalid指定的 IANA 时区无法加载
cron.ErrTriggerIntervalTooShort固定频率低于 cron.MinInterval
cron.ErrTriggerIntervalTooLong固定频率超过 MaxDurationMilliseconds
cron.ErrTriggerFireTimeRequired单次触发器缺少触发时间

不属于所选类型的字段会被拒绝(ErrTriggerFieldsConflict),无法解析的 表达式、无法加载的时区、低于 1 秒的间隔、缺失的触发时间同样被拒绝。

调度声明

cron.ScheduleSpec 声明要创建的调度,或更新的目标状态:

字段含义
Name唯一管理键;播种的默认调度回退为任务名
JobName要执行的已注册 JobHandler
Trigger何时触发(见上)
ParamsJSON 序列化后每次运行原样交给处理器
StartsAt / EndsAt可选触发窗口;StartsAt 同时是 interval 触发器的相位锚点
MisfirePolicyfire_now(默认)或 skip(见下)
ConcurrencyPolicyforbid(默认)或 allow(见下)
Recover重新触发未完成的运行(执行中途被遗弃,或因优雅关机被取消);要求处理器幂等
Timeout单次运行上限(必须是整毫秒);0 继承 vef.cron.store.run_timeout
Enabled初始/更新后的启用状态;nil 表示启用

调用方提供的时间(Trigger.AtStartsAtEndsAt)以绝对 Unix 毫秒 epoch 持久化(读回时为 UTC),因此任何时区构建的时刻读回后仍表示同一瞬间。

策略

错触(Misfire)

超过 vef.cron.store.misfire_threshold(默认 1m)才开始的触发计为 错触——停机、暂停中的调度或没有空闲执行槽。此时应用调度的 MisfirePolicy

策略行为
fire_now(默认)立即补跑一次,然后从现在恢复常规序列
skip跳到下一个未来触发,不补跑

无论哪种策略,永远不会执行的那些次数会以一条 missed 运行记入流水账, 覆盖整个缺口(missedCount 记录次数)。

并发

策略行为
forbid(默认)与同调度仍在执行的运行重叠的触发被抑制并记为 skipped。恢复请求保持等待直到活动运行结束
allow同一调度的运行可以重叠

策略常量

常量取值语义
cron.MisfireFireNowfire_now立即补跑一次,然后从“现在”恢复常规序列
cron.MisfireSkipskip跳到下一个未来触发,不补跑
cron.ConcurrencyForbidforbid抑制重叠触发并记为 skipped
cron.ConcurrencyAllowallow允许同一调度的运行重叠

暂停 / 恢复语义

Pause 清除运维所有的 isEnabled 标志;执行中的运行不受影响。触发游标在 暂停期间被刻意保留,因此 Resume 会把暂停缺口交给错触策略处理,而不是 静默丢弃:fire_now 下立即补跑一次,skip 下等待下一个常规触发。

手动触发

TriggerNow 持久化一条独立的立即触发请求(表 crn_fire_request)——单节点、 入流水账、遵守并发策略——且不移动常规触发游标。恢复重触发也走同一张请求表。 暂停中的调度拒绝并返回 ErrScheduleDisabled。当手动触发与常规触发落在同一 逻辑时刻时,常规触发优先,两者都按策略入流水账。

执行与恢复

  • 每个节点轮询到期调度(poll_interval,默认 5s,并自适应睡眠到最近的 已知触发——该间隔是其他节点新建调度的可见性延迟,不是触发精度),按事务 认领至多 batch_size 个触发,在本节点至多 max_concurrent 个槽位上 执行。
  • 执行器每 heartbeat_interval(默认 10s)为运行中的流水账行续心跳。 心跳陈旧超过 abandoned_after(默认 1m;必须至少是心跳间隔的两倍)的 running 行由恢复清扫在一个事务内接管并标记为 abandoned;设置了 Recover 的调度会将其作为全新运行重新触发。
  • 超过超时仍未结束的运行记为 failed;优雅停机时被中断的运行记为 canceled
  • 重塑调度(修改触发器/窗口)会重算下次触发但保留触发历史;重命名后流水账 经反规范化的名称保持关联。

运行流水账

cron.Run(表 crn_run)记录每次触发。行在调度删除后仍保留—— scheduleNamejobName 为此做了反规范化。

字段类型含义
idstring流水账行 ID
scheduleId / scheduleNamestring触发的调度
jobNamestring执行的处理器
scheduledAtUnixMsint64逻辑触发时间;补跑的实际开始晚于它。刻意不唯一:手动与恢复触发可能合法地共享同一时刻
claimedAtUnixMsint64节点认领触发的时间
statusstringrunningsucceededfailedmissedskippedabandonedcanceled
nodeIdstring执行节点;从未执行的行(missedskipped)为空
startedAtUnixMs / finishedAtUnixMsint64执行窗口
durationMsint64执行时长
heartbeatAtUnixMsint64执行器活性信号;陈旧即转为 abandoned
errorstring失败消息(截断);成功为空
missedCountint一条 missed 行覆盖的次数

运行状态

常量取值含义
cron.RunStatusstring生命周期状态类型
cron.RunRunningrunning已被认领、正在执行(或即将执行)
cron.RunSucceededsucceeded处理器返回 nil
cron.RunFailedfailed处理器返回错误、panic 或超时
cron.RunMissedmissed错触策略判定这些次数永远不会执行
cron.RunSkippedskippedConcurrencyForbid 抑制的触发
cron.RunAbandonedabandoned执行器心跳丢失
cron.RunCanceledcanceled优雅停机时中断

run_retention 会(每小时清扫)删除超窗的终态流水账行;0 表示永久保留—— 删除流水账严格 opt-in。

编程式管理

只要加载了 cron 模块,cron.ScheduleManager 就在 DI 中可用;存储关闭时 所有方法返回 ErrStoreDisabled。API 变更与编程式变更共享同一套校验与唤醒 路径。

方法契约
Create(ctx, spec)校验并持久化新调度;任务名必须在本节点注册;名称已占用返回 ErrScheduleExists
Update(ctx, name, spec)重塑指定调度,spec 携带不同的未占用名称时同时重命名;触发器/窗口变更重算下次触发
Delete(ctx, name)删除调度;流水账保留
Pause(ctx, name) / Resume(ctx, name)暂停语义
TriggerNow(ctx, name)手动触发
Get(ctx, name)返回指定调度,或 ErrScheduleNotFound
List(ctx, filter)匹配 ScheduleFilterJobNameEnabled *bool)的调度,按名称排序
ListRuns(ctx, filter)匹配 RunFilterScheduleNameJobNameStatuses、逻辑触发时间的 Since/UntilLimit —— 0 解析为 100,上限 1000)的流水账,最新在前

事件

两个主题都是尽力而为的运维通知,在任何事务之外经默认事件路由发布——用于 告警订阅;绝不要用它们驱动正确性(运行流水账才是持久事实)。

主题事件字段
vef.cron.run.failedcron.RunFailedEventrunIdscheduleNamejobNamescheduledAtUnixMsnodeIderror
vef.cron.run.abandonedcron.RunAbandonedEventrunIdscheduleNamejobNamescheduledAtUnixMsnodeId

事件构造器与常量

API契约
cron.EventTypeRunFailed主题常量 vef.cron.run.failed
cron.EventTypeRunAbandoned主题常量 vef.cron.run.abandoned
cron.NewRunFailedEvent(run *Run) *RunFailedEvent从流水账记录构造 run-failed 事件
cron.NewRunAbandonedEvent(run *Run) *RunAbandonedEvent从流水账记录构造 run-abandoned 事件

RPC 资源

存储启用后,两个管理资源挂载在 /api 下。存储关闭时资源不挂载任何操作—— 关闭的特性不暴露任何表面。变更类操作均记入审计。

sys/cron/schedule

操作权限输入输出
find_pagecron.schedule.queryScheduleSearch + 分页 metapage.Page[Schedule]
getcron.schedule.queryScheduleNameParamsScheduleDetail
list_jobscron.schedule.querystring[]
preview_firescron.schedule.queryPreviewFiresParamsFiresPreview
createcron.schedule.manage(审计)ScheduleParams创建后的 Schedule
updatecron.schedule.manage(审计)ScheduleParams更新后的 Schedule
deletecron.schedule.manage(审计)ScheduleNameParams成功
pausecron.schedule.manage(审计)ScheduleNameParams成功
resumecron.schedule.manage(审计)ScheduleNameParams成功
trigger_nowcron.schedule.manage(审计)ScheduleNameParams成功

ScheduleSearchfind_page 查询过滤):

字段类型匹配说明
namestringcontains按调度名片段过滤
jobNamestringequals按任务名过滤
kindstringequals触发器类型:cronintervalonce
isEnabledboolequals按启用状态过滤

ScheduleNameParamsgetdeletepauseresumetrigger_now 使用):

字段类型必填说明
namestring调度的唯一名称

ScheduleParams(create/update;未知字段会被拒绝——参数结构体是严格 模式):

字段类型必填说明
namestringcreate 时:新调度的唯一名称;update 时:被寻址的调度
newNamestring仅 update:先按 name 寻址再重命名
jobNamestring要执行的已注册任务处理器;未注册返回 ErrJobNotRegistered
triggerTriggerParams触发器定义(见下)
params任意 JSON 值每次运行原样交给处理器
startsAtUnixMsint64(unix 毫秒)触发窗口起点;同时是 interval 触发器的相位锚点
endsAtUnixMsint64(unix 毫秒)触发窗口终点;必须晚于 startsAtUnixMs
misfirePolicystringfire_now(省略时默认)或 skip
concurrencyPolicystringforbid(省略时默认)或 allow
recoverbool重新触发未完成的运行(执行中途被遗弃,或因优雅关机被取消);处理器必须幂等
timeoutMsint64单次运行超时;0 继承 vef.cron.store.run_timeout;负值被拒绝
enabledbool省略表示启用

TriggerParams(只允许所选 kind 的字段;多余字段返回 ErrTriggerInvalid):

字段类型必填说明
kindstringcronintervalonce
exprstringcron 必填cron 表达式(5/6 段或 @ 描述符)
timezonestring否(仅 cron表达式求值的 IANA 时区;空表示 UTC"Local" 被拒绝
everyMsint64interval 必填固定频率(毫秒),最小 1000
atUnixMsint64(unix 毫秒)once 必填单次触发时间

ScheduleDetailget 响应):

字段类型说明
scheduleSchedule调度行(见下)
nextFiresUnixMsint64[]从现在起接下来(至多 5 个)精确触发时间的预览。过期游标按调度错触策略投影;暂停或已耗尽的调度返回空列表

Schedulegetcreateupdatefind_page 条目返回;省略标准 审计字段):

字段类型说明
namestring唯一管理键
jobNamestring调度触发的处理器
kindstring触发器类型
exprstringcron 表达式(cron 类型)
timezonestring求值时区(cron 类型)
everyMsint64固定频率(interval 类型)
fireAtUnixMsint64单次触发时间(once 类型);其他类型缺省
startsAtUnixMs / endsAtUnixMsint64触发窗口边界;无界时缺省
anchorAtUnixMsint64固定频率相位锚点(创建时间,或设置了 startsAtUnixMs 时为其值)
paramsJSON处理器参数,原样
misfirePolicystringfire_nowskip
concurrencyPolicystringforbidallow
recoverbool遗弃运行重触发标志
timeoutMsint64单次运行超时;0 继承配置默认
isEnabledbool运维所有的启用状态(pause 清除、resume 恢复)
nextFireAtUnixMsint64引擎将认领的下一次触发;触发器不再产生新次数(完成的单次、过期的窗口)时缺省——暂停会保留它
lastFireAtUnixMsint64最近一次被认领触发的逻辑时间;首次触发前缺省

list_jobs 返回本节点注册的任务名——调度编辑器任务选择器的候选词汇。 异构部署下各节点注册集可能不同;返回的是应答节点的视图。

PreviewFiresParamspreview_fires —— 在编辑器阶段用真实解析器验证 未保存的触发器;拒绝的恰好是保存会拒绝的):

字段类型必填说明
triggerTriggerParams要投影的未保存触发器
startsAtUnixMsint64(unix 毫秒)投影所用的窗口起点
endsAtUnixMsint64(unix 毫秒)窗口终点;必须晚于起点

FiresPreview 响应:

字段类型说明
nextFiresUnixMsint64[]触发器从现在起的触发时间(至多 5 个);窗口内不再产生次数时为空

sys/cron/run

只读流水账视图:分页视图用于浏览,单条视图用于查看完整错误文本。默认按 认领时间倒序。

操作权限输入输出
find_pagecron.run.queryRunSearch + 分页 metapage.Page[Run]
find_onecron.run.queryRunSearch一条 Run

RunSearch(查询过滤):

字段类型匹配说明
idstringequals寻址单条流水账行——find_one 没有其他方式指名记录
scheduleNamestringequals按调度过滤
jobNamestringequals按任务过滤
statusstringequals运行状态之一
nodeIdstringequals按执行节点过滤
scheduledAtFromUnixMsint64逻辑触发时间下界
scheduledAtToUnixMsint64逻辑触发时间上界

Run 响应字段即运行流水账字段加上创建审计字段。

错误码

Cron API 错误使用响应码 27002799,以 HTTP 200 承载、失败由响应体 code 表达。

错误含义
2700ErrScheduleNotFound调度不存在
2701ErrScheduleExists调度名已占用
2702ErrScheduleDisabled对暂停中的调度手动触发
2703ErrTriggerInvalid(reason)触发器校验失败(字段冲突、坏表达式、坏时区、间隔过短、缺触发时间)
2704ErrJobNotRegistered调度引用了本节点未注册的任务名
2705ErrStoreDisabledvef.cron.store.enabled = false 时调用存储操作
2706ErrScheduleInvalid(reason)非触发器的声明故障(名称、窗口、超时、参数、策略词汇)

错误码常量

常量含义
cron.ErrCodeScheduleNotFound2700调度不存在
cron.ErrCodeScheduleExists2701调度名已占用
cron.ErrCodeScheduleDisabled2702对暂停中的调度手动触发
cron.ErrCodeTriggerInvalid2703触发器校验失败
cron.ErrCodeJobNotRegistered2704调度引用了本节点未注册的任务名
cron.ErrCodeStoreDisabled2705vef.cron.store.enabled = false 时调用存储操作
cron.ErrCodeScheduleInvalid2706非触发器的声明故障(名称、窗口、超时、参数、策略词汇)

配置

[vef.cron.store]
enabled = false # 总开关;关闭时不触碰任何表
auto_migrate = false # 启动时执行 cron DDL 迁移
poll_interval = "5s" # 调度表重读上限(可见性延迟,不是触发精度)
batch_size = 32 # 每个轮询周期认领的调度数
max_concurrent = 16 # 每节点并发运行数
misfire_threshold = "1m" # 触发晚到多久后应用错触策略
heartbeat_interval = "10s" # 执行器对运行中行的活性节律
abandoned_after = "1m" # 心跳陈旧窗口;必须 ≥ 2 × heartbeat_interval
run_timeout = "0s" # 默认单次运行上限;0 表示不限
run_retention = "0s" # 流水账保留;0 表示永久保留

启动校验拒绝负的时长、比心跳间隔两倍更紧的 abandoned_after、负的 batch_size 以及负的 max_concurrent (否则健康执行器会被判死亡)。

下一步

面向进程内工作的内存调度器见 Cron 定时任务。要对失败或被遗弃的 运行做告警,请通过事件总线订阅上述事件。