feat: add durable relative task delays and outage recovery regression

This commit is contained in:
2026-09-17 23:43:57 +08:00
parent 522fe2ef3e
commit a664ec9370
34 changed files with 811 additions and 28 deletions

View File

@@ -10,6 +10,12 @@
- 覆盖:8个并发调用/20项领取、业务回滚、旧代次拒绝、HTTP回执丢失、三处MySQL COMMIT回执丢失及发送进程SIGKILL恢复。
- 边界:业务侧两张表均无外键,22个中文注释字段;固定发送预算,REVIEW保留原记录供业务管理核查,核心SDK不引入数据库依赖。
## 已可运行:相对延迟执行
- [完整命令、输入输出和失败处理](delayed-task.md),[SDK源码](../../sdk/go/examples/delayed-task/main.go)。
- delay_ms按数据库首次受理时间计时,实际Worker执行后查询开始时间;重报原请求仍返回原任务及时间。
- 自动失败重试单独使用retry策略,初始延迟不在每次尝试中重新计时。
## 已可运行:健康检查
- 源码:[SDK 健康检查](../../sdk/go/examples/health/main.go)。

View File

@@ -0,0 +1,59 @@
# 相对延迟与原请求恢复
源码:[delayed-task/main.go](../../sdk/go/examples/delayed-task/main.go)。示例提交等待1500毫秒的对象任务,启动自动Worker,核对实际执行历史,再在任务完成后重报原请求。处理器原样返回示例JSON,无外部副作用。
## 准备与运行
先完成[迁移和Worker凭据初始化](../sdk/worker.md#1-可运行的文本规范化示例),启动当前Orbit。凭据需要任务提交、读取及Worker连接/消费权限,队列须有可用额度。仓库根目录执行:
```sh
cd sdk/go
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
go run ./examples/delayed-task
```
也可用ORBIT_TOKEN和ORBIT_QUEUE_ID覆盖凭据文件中的对应值。示例总上下文20秒、任务总时限15秒、单次执行5秒、最多1次尝试;每次运行使用独立操作及处理器身份,不会消费其他处理器的任务。
预期输出:
```text
task=<任务ID> delay_ms=1500
state=SUCCEEDED attempts=1 no_early_start=true original_receipt=true worker_drained=true
```
检查依据是同一个数据库的CreatedAt、AvailableAt及尝试创建/开始时间,不把客户端墙钟或sleep结束当作执行授权。Worker正常排空后退出;任务、结果和审计保留。
## SDK调用
以下为函数中的调用片段,完整imports及错误处理见源码:
```go
request := orbit.SubmitTaskRequest{
QueueID: queueID,
Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1",
Payload: json.RawMessage(`{"object_ref":"objects/input"}`),
DelayMS: 5000,
TaskTimeoutMS: 60000,
Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 10000},
}
accepted, err := client.SubmitTask(ctx, operationKey, request)
if err != nil { return err }
fmt.Println(accepted.Task.ID, accepted.Task.AvailableAt)
```
`delay_ms`相对首次成功受理事务中的数据库时间计算,保存为`available_at`。同一幂等键重报仍提交原等待时长;不按当前时间重算available_at,不生成新键。响应是原受理回执,即使任务后来已经成功,回执中仍可能是WAITING;当前状态通过GetTask查询。
指定绝对时刻时使用AvailableAt并令DelayMS为0。非零delay_ms和available_at不能同时提供。等待时长必须小于task_timeout_ms;总时限包含等待时间。start_deadline_at不能早于计算后的available_at。所有时间规则及400/409错误见[任务接口](../api/tasks.md)。
服务端停机、队列暂停、没有兼容Worker或额度不足都可能让开始时间晚于available_at;到期后才可领取,不承诺准点执行。自动失败重试使用retry中的退避策略,不再次应用初始delay_ms。
## 失败与持久化边界
示例网络错误时非零退出,已打印任务ID可用于核对;不在同一次运行中换键重提。真实接入须在第一次请求前保存操作身份和请求,可使用[业务事务Outbox](task-outbox.md)。本例生成的新身份仅用于独立演示,重新运行不是上一次进程的恢复。
Outbox若使用delay_ms,等待从Orbit受理开始计算,不从本地业务事务或Outbox创建时间计算。需要固定业务时刻时冻结available_at;若发送时该时刻已超出接口容差,按拒绝结果核查,不静默改成“从现在起再等”。
旧服务不识别delay_ms。先更新服务端,再让调用方启用该字段;省略/零值保持旧请求摘要。计划模板拒绝非零delay_ms,时间由计划规则定义。人工重试的新开始时间使用其专门接口参数,不继承原任务的初始延迟。
`make integration`执行此例、真实HTTP/Worker延迟用例,以及重试退避期间全节点退出后的恢复测试;结果见[本轮报告](../reports/phase-3-delay-recovery.md)。

View File

@@ -172,3 +172,5 @@ send_count统计本地发送领取,与Pulse的执行尝试次数分别计数
本例的业务效果仅为本地SQL记录及平台受理,不提供外部服务恰好一次执行。业务Worker继续承担业务幂等;执行结果需要[可靠结果订阅与Inbox](result-inbox.md)等衔接。敏感请求和回执留在业务数据库,应纳入业务访问控制、审计、备份及保留策略;日志只输出操作ID和稳定状态,不输出token或请求正文。
REVIEW后的人工核查、证据审批及受控恢复由接入方的业务管理流程承担,示例不提供无证据清空错误/重置预算接口。不要直接修改SQL状态制造成功,也不要在无法确认的情况下换业务操作ID。生产MySQL故障切换、长期归档、跨部署迁移和容量仍按P6验收;本例不启动流程或发送Weave信号,相应Outbox命令类型随流程API演进。
相对延迟任务可在SubmitTaskRequest设置DelayMS;Outbox冻结并重发原时长,由Orbit首次受理事务计算执行时间。它不从本地业务创建时间起算;固定业务时刻应使用冻结AvailableAt,详见[延迟接入](delayed-task.md)。

View File

@@ -17,7 +17,7 @@ ORBIT_URL=http://127.0.0.1:8080 \
## 实际调用与结果
1. 提交两个完全相同业务键的任务,以及一个只相差尾部空格的任务。业务键含中文、加号、百分号及重音字符。
1. 使用不同提交身份创建两个完全相同业务键、format分别为text/json的任务,以及一个业务键只相差尾部空格的任务。业务键含中文、加号、百分号及重音字符。读取前两个任务确认各自输入保留,改参数没有覆盖原任务。
2. 用服务端回执中的CreatedAt设置包含起点、不包含终点的时间范围,避免依赖客户端时钟同步。
3. ListTasks按原键、WAITING状态和创建区间查询,每页1条,沿NextCursor遍历;仅匹配前两个任务。
4. 将created_before设为第二个任务的精确创建时间,确认它被排除,只剩第一个任务。
@@ -26,11 +26,13 @@ ORBIT_URL=http://127.0.0.1:8080 \
预期输出:
```text
matched=2 pages=2 before_excluded=true cancelled=3
matched=2 pages=2 before_excluded=true original_payload_preserved=true cancelled=3
```
重复运行创建新的独立样本。代码没有外部业务副作用,也不删除数据库历史。失败非零退出,可能留下已受理任务;用回执、业务键和时间范围查询后核对,各任务总时限60秒,当前示例没有Worker会进入未开始超时处理。
本例通过business_key关联不同参数的任务,不表示覆盖、自动取消或自动代替原工作,来源仍为DIRECT。需要重新执行原冻结参数时使用带证据的[人工重试](../api/task-recovery.md);不能用新任务身份绕过未知业务效果核查。
完整参数、游标兼容和异常说明见[查询契约](../api/task-queries.md)。翻页时保持所有筛选,发生INVALID_ARGUMENT不要丢弃过滤条件“兜底查全部”。状态变化的跨页结果不是快照,不能当作完整审计导出。
`make integration` 会在一次性MySQL中运行此示例及实际HTTP SDK回归,验证原键/尾部空格、时间边界、分页、变更条件拒绝,以及取消后创建时间不改变。