222 lines
16 KiB
Markdown
222 lines
16 KiB
Markdown
# 调度计划接口 v1
|
||
|
||
状态:已实现五段 Cron、fixed-rate、不可变版本、暂停/恢复、三种漏跑与重叠策略、触发历史及独立 SDK。调度记录、任务创建、事件与进度同事务提交。当前实际验证与边界见 [阶段报告](../reports/phase-3-schedules.md),整体仍按 [开发计划](../08-development-plan.md) 推进。
|
||
|
||
## 1. 权限与生命周期
|
||
|
||
| 接口 | 权限 | SDK |
|
||
| --- | --- | --- |
|
||
| POST /v1/schedules | schedules:manage + tasks:submit + 模板队列范围 | CreateSchedule |
|
||
| GET /v1/schedules?queue_id=… | schedules:read + 队列范围 | ListSchedules |
|
||
| GET /v1/schedules/{id} | schedules:read + 计划队列范围 | GetSchedule |
|
||
| PATCH /v1/schedules/{id} | schedules:manage + tasks:submit + 计划队列范围 | PublishSchedule |
|
||
| POST /v1/schedules/{id}:pause | schedules:manage + 计划队列范围 | PauseSchedule |
|
||
| POST /v1/schedules/{id}:resume | schedules:manage + 计划队列范围 | ResumeSchedule |
|
||
| GET /v1/schedules/{id}/versions/{version} | schedules:read + 计划队列范围 | GetScheduleVersion |
|
||
| GET /v1/schedules/{id}/occurrences | schedules:read + 计划队列范围 | ListScheduleOccurrences |
|
||
|
||
计划终身绑定一个队列。发布完整新定义时可以改变时间、参数和策略,不能更换队列。计划属于命名空间;创建凭据后续到期/撤销不自动撤销已创建的计划。停止计划使用暂停接口,禁用应用或命名空间会阻止后台继续创建任务。每次管理与查询仍重新校验当前凭据。
|
||
|
||
计划暂停只停止产生新的任务,已经受理的任务继续执行。队列暂停只停止任务派发,不停止计划受理,可能形成积压。SERIAL 策略将 WAITING、LEASED、RUNNING、NEEDS_REVIEW 都视为未完成;未知效果未处理时不能开始下一轮。
|
||
|
||
请求使用 Bearer、application/json 和稳定的 Idempotency-Key,沿用 [任务接口](tasks.md) 的 512 KiB 请求、严格 JSON、命名空间隔离和精确数字规则。ID 为 32 位小写十六进制,revision/version 为十进制字符串,时间为 UTC RFC 3339 微秒精度。
|
||
|
||
## 2. 创建与定义字段
|
||
|
||
CreateScheduleRequest:name、definition 必填;paused 默认 false。name 为命名空间内唯一的 1–128 位 ASCII 名称,字符规则同任务处理器。definition 包含 rule、policy、task,三者均需明确提供。
|
||
|
||
| rule 字段 | 含义与边界 |
|
||
| --- | --- |
|
||
| kind | CRON 或 FIXED_RATE |
|
||
| timezone | 必填 IANA 名称,如 UTC、Asia/Shanghai;不接受 Local 或文件路径 |
|
||
| calendar_version | 可省略,v1 固定默认 `go1.27.1-b2d18a7c8fa8`;规范化后写入版本快照,其他未支持值拒绝 |
|
||
| start_at | 必填基准,包含边界;年份 2000–9998,最多微秒 |
|
||
| end_at | 可选排除边界,必须晚于 start_at;达到后不再产生新触发点 |
|
||
| cron | CRON 必填五段表达式,最多 256 字符;不接受秒段、@every 等描述符或内嵌 TZ= |
|
||
| interval_ms | FIXED_RATE 必填,1000–2592000000 毫秒;此时不能设置 cron |
|
||
|
||
新计划的第一个点不早于数据库创建时刻;start_at 在过去时只用作规则基准,不追溯生成计划存在前的任务。创建时必须还有未来触发点。新发布版本可以设置已经结束的规则,此时先处理旧积压,再结束后续调度。
|
||
|
||
Cron 段顺序为分钟、小时、月日、月、星期;支持 `*`、列表、范围、步长、月份/星期名称,星期为 0–6(周日为 0)。月日与星期均受限时按 OR 匹配,有通配段时按标准组合规则匹配。采用 [robfig/cron v3.0.1 的解析器](https://github.com/robfig/cron/tree/v3.0.1),Orbit 自行按时区偏移区间转换 UTC 触发点,不启动该库的内存调度器。
|
||
|
||
时区快照内置于服务二进制,不读取宿主机时区文件或 ZONEINFO。快照来自 Go 1.27.1 的 zoneinfo.zip,完整 SHA-256 为 `b2d18a7c8fa8142097a48c99609fb3c92db5ee98bc740294e57eab8ae9f94779`。升级时必须保留旧快照可读能力和 v1 默认语义,不能把旧计划静默换成新的时区规则。
|
||
|
||
- 夏令时缺失的本地时刻跳过;重复的本地时刻按两个不同 UTC 时间分别触发。
|
||
- 半小时跳变、整天跳过、非整点时区和世纪闰日都有测试。
|
||
- fixed-rate 始终按 `start_at + n × interval_ms` 的实际经过时间计算,不跟随上一任务的完成时间漂移。
|
||
- 无解日期规则、混合字段和无效时区在发布前拒绝。无后续触发点与计算错误分开处理,错误不能静默当作完成。
|
||
|
||
| policy 字段 | 要求 |
|
||
| --- | --- |
|
||
| misfire | 必填:SKIP、COALESCE、CATCH_UP |
|
||
| overlap | 必填:PARALLEL、SERIAL、SKIP |
|
||
| catchup_limit | 必填 1–100,每批最多生成任务数;大参数批次还受约 1 MiB 参数复制量限制 |
|
||
| catchup_window_ms | 必填 1000–2592000000,未处理触发点最多保留的时间窗口 |
|
||
| grace_ms | 必填 1000–60000,正常派发允许延迟;不能超过 catchup_window_ms |
|
||
|
||
所有任务策略在 task 内冻结,字段同 SubmitTaskRequest;payload 最大 256 KiB,数字文本保持精确。task 中禁止绝对 available_at / start_deadline_at,每次生成时立即可受理,task_timeout_ms 从本次实际创建时间计算。计划结束不取消已经生成的任务,也不缩短其任务时限。
|
||
|
||
生成任务受 [队列积压上限](queues.md) 约束。满额时保留未受理触发点,本批较早已生成部分仍提交;释放空间后按冻结的漏跑策略接续,超过补偿窗口的点仍按策略跳过。队列 paused 仅停止领取/开始,停止计划生成需使用计划暂停接口。
|
||
|
||
创建请求示例,ID 和未来时间替换为实际值:
|
||
|
||
```json
|
||
{
|
||
"name":"object-snapshot",
|
||
"paused":true,
|
||
"definition":{
|
||
"rule":{"kind":"FIXED_RATE","timezone":"UTC","start_at":"2026-10-01T00:00:00Z","interval_ms":60000},
|
||
"policy":{"misfire":"COALESCE","overlap":"SERIAL","catchup_limit":10,"catchup_window_ms":86400000,"grace_ms":5000},
|
||
"task":{"queue_id":"11111111111111111111111111111111","handler":"object.snapshot","handler_version":"v1","payload_version":"v1","payload":{"source_ref":"objects/sample","sequence":9007199254740993},"task_timeout_ms":30000,"retry":{"max_attempts":1,"attempt_timeout_ms":5000}}
|
||
}
|
||
}
|
||
```
|
||
|
||
```sh
|
||
curl "$ORBIT_URL/v1/schedules" \
|
||
-H "Authorization: Bearer $ORBIT_TOKEN" \
|
||
-H 'Content-Type: application/json' -H 'Idempotency-Key: snapshot-plan-001' \
|
||
--data-binary @schedule.json
|
||
```
|
||
|
||
将上方 JSON 保存为 schedule.json。首次返回 201,同键同内容返回 200;均不表示已有任务完成。响应示例:
|
||
|
||
```json
|
||
{
|
||
"request_id":"44444444444444444444444444444444",
|
||
"data":{
|
||
"deduplicated":false,
|
||
"schedule":{
|
||
"id":"22222222222222222222222222222222","namespace_id":"33333333333333333333333333333333",
|
||
"queue_id":"11111111111111111111111111111111","name":"object-snapshot",
|
||
"revision":"1","current_version":"1","processing_version":"1","paused":true,
|
||
"next_fire_at":"2026-10-01T00:00:00Z","check_at":"2026-10-01T00:00:00Z",
|
||
"created_at":"2026-09-17T08:00:00Z","updated_at":"2026-09-17T08:00:00Z",
|
||
"current":{
|
||
"version":"1","effective_at":"2026-09-17T08:00:00Z",
|
||
"definition":{
|
||
"rule":{"calendar_version":"go1.27.1-b2d18a7c8fa8","kind":"FIXED_RATE","timezone":"UTC","start_at":"2026-10-01T00:00:00Z","interval_ms":60000},
|
||
"policy":{"misfire":"COALESCE","overlap":"SERIAL","catchup_limit":10,"catchup_window_ms":86400000,"grace_ms":5000},
|
||
"task":{"queue_id":"11111111111111111111111111111111","handler":"object.snapshot","handler_version":"v1","payload_version":"v1","payload":{"source_ref":"objects/sample","sequence":9007199254740993},"task_timeout_ms":30000,"recovery_safety":"RECONCILE_ONLY","retry":{"max_attempts":1,"initial_delay_ms":0,"max_delay_ms":0,"attempt_timeout_ms":5000}}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
GetSchedule 的 data 直接为 schedule 对象。列表 Items 省略 current/大参数,其他字段相同。next_fire_at 是下一原计划时间,积压时可能在过去;check_at 是平台下次允许扫描时间,并不保证届时执行。next_fire_at 为空时,可能尚待初始化某个版本的首个触发点,也可能已经结束;即使 processing_version 等于 current_version,也必须结合 check_at 判断。check_at 非空表示仍有扫描工作,为空且处理到最新版本表示已无后续触发。
|
||
|
||
## 3. 漏跑、重叠与有限补偿
|
||
|
||
先处理超过 catchup_window_ms 的范围并记录 CATCHUP_WINDOW_EXCEEDED,再应用漏跑规则:
|
||
|
||
| 策略 | 行为 |
|
||
| --- | --- |
|
||
| SKIP | 早于 now − grace_ms 的点合为跳过范围;容差内的点仍可受理 |
|
||
| COALESCE | 若最早积压点超过 grace_ms,将当前版本内本轮所有已到期点合为一个任务 |
|
||
| CATCH_UP | 按原时间逐点创建,单批不超过 catchup_limit,剩余进度保留到下一批 |
|
||
| PARALLEL | 可以产生多条未完成任务,实际执行仍受队列与 Worker 并发许可限制 |
|
||
| SERIAL | 有任一先前计划任务未终结时保留游标等待;没有未完成任务时本批最多创建一条 |
|
||
| SKIP 重叠策略 | 有先前未终结任务时跳过本轮已到期范围;否则本批最多创建一条 |
|
||
|
||
SERIAL 等待也受时间窗口约束,过旧范围会被记录为跳过,避免无界补偿。NEEDS_REVIEW 不属于明确终态,会持续阻止串行后续任务;取消或失败不能假装外部效果已消失。
|
||
|
||
每节点每秒扫描最多 100 个候选计划,每个计划单独事务。同一计划相邻批次的数据库决策时间至少间隔一秒,check_at 持久化,多节点不会各自补跑一份。每批最多创建一个历史版本的任务,跨版本也不绕过批量上限;空版本每批最多推进 16 个。当前是有界运行基线,不是已验证的生产吞吐承诺。
|
||
|
||
每个触发身份由命名空间、计划 ID、原 UTC scheduled_at 唯一确定。暂停、重连和发布新版本都不会改变旧身份。跳过/合并记录的覆盖区间为 `[scheduled_at, covered_until)`,结束边界不包含;范围不会跨越版本生效时间,也不会为范围内每个点额外创建任务。
|
||
|
||
## 4. 发布、暂停与恢复
|
||
|
||
PATCH 是完整发布,必须包含 expected_revision(非零十进制字符串)、reason(非空,最多 1024 字符)、definition(完整新定义)。发布后 current_version 增加一,新版本以事务读取的数据库时间立即生效。
|
||
|
||
尚未处理且早于新版本生效边界的点仍按旧规则、旧模板和旧策略决定。后续原时间由新版本计算,已生成任务保持原快照。数据库触发器禁止覆盖已发布版本。GET versions/{version} 返回 version、effective_at、definition;version 必须是无前导零的非零十进制数。
|
||
|
||
暂停/恢复请求体相同:
|
||
|
||
```json
|
||
{"expected_revision":"1","reason":"start regular snapshots"}
|
||
```
|
||
|
||
成功返回 200,data 与创建回执结构相同。暂停不清空游标,恢复按冻结漏跑规则处理暂停期间的点。对已经处于目标暂停状态的计划换键操作返回 STATE_CONFLICT;原键原内容重报仍返回原回执。
|
||
|
||
- 管理操作、不可变版本、事件、审计和幂等回执同事务保存。后台触发持久化 occurrence、task.accepted、schedule.decided、schedule.advanced,记录执行节点。
|
||
- 每次扫描也更新 revision。遇到明确 STATE_CONFLICT,先 GET 新状态,再用新的命令身份和新 expected_revision 操作;网络结果不明时必须复用原键和原版本核对。
|
||
- 同键更改定义、reason 或 expected_revision 返回 IDEMPOTENCY_CONFLICT。原回执不表示计划当前仍是该状态。
|
||
- 计划名称不能修改,暂无删除或单次手动触发接口。WAITING 的已生成计划任务可用任务取消接口取消,计划本身继续运行;流程归属任务仍由后续流程控制管理。
|
||
|
||
## 5. 列表与触发记录
|
||
|
||
ListSchedules 需要 queue_id;计划列表和 occurrences 均可用 limit(默认 20,1–100)、cursor(最多 1024 字符)。未知、重复查询参数或错误游标返回 400。详情接口不接受查询参数。
|
||
|
||
计划按 `(created_at DESC,id DESC)`,触发按 `(scheduled_at DESC,id DESC)` 分页,不使用 offset。游标绑定命名空间、队列或计划及查询类型。读取不承诺跨页快照;空列表为 items: [],有下一页才返回 next_cursor。
|
||
|
||
```json
|
||
{
|
||
"request_id":"44444444444444444444444444444444",
|
||
"data":{"items":[{
|
||
"id":"55555555555555555555555555555555","schedule_id":"22222222222222222222222222222222",
|
||
"schedule_version":"1","scheduled_at":"2026-10-01T00:00:00Z","decision":"COALESCED",
|
||
"task_id":"66666666666666666666666666666666","covered_until":"2026-10-01T00:03:00.000001Z",
|
||
"reason":"MISFIRE_COALESCED","created_at":"2026-10-01T00:03:00Z"
|
||
}]}
|
||
}
|
||
```
|
||
|
||
FIRED 表示已创建单点任务;COALESCED 表示范围已合并到 task_id;SKIPPED 没有 task_id。它们都不是业务执行结果,继续 GetTask 查询,再通过 [尝试与证据接口](execution-history.md) 核对执行。
|
||
|
||
## 6. SDK 与完整示例
|
||
|
||
所有方法首参数为 context.Context,写操作需要稳定 key。ListSchedules 的参数为 `(ctx, queueID, SchedulePageRequest)`;ListScheduleOccurrences 为 `(ctx, scheduleID, SchedulePageRequest)`。
|
||
|
||
在已有服务和当前迁移基础上,创建专用开发凭据:
|
||
|
||
```sh
|
||
go run ./cmd/orbit-admin bootstrap \
|
||
--application schedule-demo --namespace default --environment development \
|
||
--queue snapshots --worker-access --schedule-access \
|
||
--output .local/schedule.credentials.json
|
||
cd sdk/go
|
||
ORBIT_URL=http://127.0.0.1:8080 \
|
||
ORBIT_CREDENTIALS_FILE=../../.local/schedule.credentials.json \
|
||
go run ./examples/schedules
|
||
```
|
||
|
||
前置数据库与目录步骤见 [SDK 接入](../sdk/README.md)。源码:[完整调度示例](../../sdk/go/examples/schedules/main.go)。示例创建暂停的每秒计划,恢复后让自动 Worker 返回对象输入快照,查询两次完成,再暂停并排空。计划本身有十秒结束边界,任务与历史保留;重复运行生成独立身份,不调用外部业务系统。
|
||
|
||
```text
|
||
schedule=<计划ID> occurrence=<触发ID> task=<任务ID> version=1 state=SUCCEEDED
|
||
schedule=<计划ID> occurrence=<触发ID> task=<任务ID> version=1 state=SUCCEEDED
|
||
paused=true completed_tasks=2 current_version=1
|
||
worker_drained=true running=0
|
||
```
|
||
|
||
调度或查询有延迟时可能观察到更多已产生的任务。异常时非零退出,Worker 尝试有界收尾;暂停不回滚已产生任务。接入函数可直接用以下片段发布 Cron 版本:
|
||
|
||
```go
|
||
current, err := client.GetSchedule(ctx, scheduleID)
|
||
if err != nil { return err }
|
||
definition := current.Current.Definition
|
||
definition.Rule.Kind = "CRON"
|
||
definition.Rule.Cron = "0 9 * * MON-FRI"
|
||
definition.Rule.IntervalMS = 0
|
||
definition.Rule.Timezone = "Asia/Shanghai"
|
||
updated, err := client.PublishSchedule(ctx, scheduleID, operationKey, orbit.PublishScheduleRequest{
|
||
ExpectedRevision: current.Revision,
|
||
Reason: "use weekday snapshot cadence",
|
||
Definition: definition,
|
||
})
|
||
if err != nil { return err }
|
||
fmt.Println(updated.Schedule.CurrentVersion)
|
||
```
|
||
|
||
保留原 calendar_version,并核对 start_at/end_at 是否仍覆盖期望时间。发布时不会取消旧版本已创建任务。
|
||
|
||
## 7. 错误与升级
|
||
|
||
错误信封同 [任务接口](tasks.md)。400 表示字段、时间、时区快照、游标或队列变更非法;401 表示凭据/应用/命名空间不可用;403 表示缺少动作或列表/创建队列范围;404 隐藏不存在或单资源越权;409 表示名称、状态、版本或幂等内容冲突;503 时写操作可能已经提交,应保留原身份核对。
|
||
|
||
迁移 000006 增加绑定队列、历史处理版本、扫描时间、窗口与容差,以及调度/任务来源索引。此前只有结构、没有创建计划接口;原人工行的 queue_id/check_at 为 NULL,不自动启用。迁移不会猜测任务模板对应的授权队列;此类行需由管理员核查后通过正式接口建立受控计划。
|
||
|
||
服务仍只依赖 MySQL,不需要外部 Cron、消息队列或协调服务。生产跨主机、数据库切换、在线迁移、海量积压与公平性测试尚待 P6;当前计划能力不代表完整生产验收已通过。
|