Files
orbit/docs/api/retries.md

118 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全重试 v1
状态:P3 的安全重试已实现。使用现有任务提交与 Worker 结果接口,无独立的自动重试路由;[Cron/固定间隔计划](schedules.md) 和 [运行中取消](cancellation.md) 已接入;[可靠结果订阅](subscriptions.md) 与业务Inbox已实现;独立任务人工核查与重试继续按 [计划](../08-development-plan.md) 开发。验证证据见 [本阶段报告](../reports/phase-3-safe-retry.md)。
## 1. 三种不同的重报
| 行为 | 身份与效果 |
| --- | --- |
| HTTP 受理结果不明,重报提交 | 保留原 Idempotency-Key 和内容,返回原任务,不增加尝试 |
| WS 控制结果不明,重报 start/finish | 保留尝试和 message_id,返回原回执,不再次调用处理器 |
| 执行失败或租约失效,平台安排重试 | 保留 Task.ID;到期后领取新 AttemptID/Epoch,重新消耗一次预算 |
HTTP 调用方自行用原身份核对受理结果,自动 Worker 管理第二类控制确认,平台根据持久化事实决定第三类。业务效果是否可重复执行,需要处理器自身保证,不能只靠连接重试。
## 2. 冻结策略与结果分类
提交时设置 `retry.max_attempts / initial_delay_ms / max_delay_ms / attempt_timeout_ms`、`recovery_safety`、任务总时限及可选最晚开始时间。默认 max_attempts=1、recovery_safety=RECONCILE_ONLY,不自动增加业务尝试。字段范围见 [任务接口](tasks.md)。
Worker 的 `task.finish` 新增可选布尔字段 `retryable`,省略为 false;只有 outcome=FAILED 才能为 true。SUCCEEDED/UNKNOWN 携带 true 返回 INVALID_ARGUMENT。该字段表示明确失败可安全再试,**不能用于把未知效果伪装成明确失败**。
以下均要求报告仍有当前有效授权;无授权的报告只保存证据:
| 当前事实 | 有剩余预算、截止允许时 | 不能重试时 |
| --- | --- | --- |
| SUCCEEDED | 完成任务,清除 effect_uncertain | 同左 |
| FAILED,retryable=false | 不安排重试 | 无历史不确定效果则 FAILED,否则 NEEDS_REVIEW |
| FAILED,retryable=true | 退避后 WAITING | 无历史不确定效果则 FAILED,否则 NEEDS_REVIEW |
| UNKNOWN 或已获开始授权后租约到期;RECONCILE_ONLY | NEEDS_REVIEW | NEEDS_REVIEW |
| UNKNOWN 或已获开始授权后租约到期;IDEMPOTENT | effect_uncertain=true,退避后 WAITING | NEEDS_REVIEW,保留不确定性 |
| 领取后未获开始授权即到期 | 退避后 WAITING,仍消耗已领取次数 | 无历史不确定效果则 FAILED,否则 NEEDS_REVIEW |
存在历史不确定效果时,只有 IDEMPOTENT 允许继续自动尝试。此声明要求跨进程、跨尝试、并发及迟到执行都具有业务幂等保障,例如持久化操作键、唯一约束、返回原结果及资源侧执行代次校验。Task.ID 在自动重试中稳定;AttemptID 会变化,不能作为跨尝试的业务幂等键。平台不会因为新尝试开始而保证旧进程已经停止。
`effect_uncertain` 是任务详情、摘要和派发任务中的可选布尔字段,false 时省略。只要曾发生尚未核实的授权执行,后来的明确失败、未开始即到期、总时限耗尽或取消都不会把该事实抹掉。当前有效尝试成功可自动清除;迟到成功证据不能清除。人工核查解除的管理接口尚未实现。
## 3. 退避、预算与持久化
消费第 n 次领取预算后,退避上限为:
```text
base = min(initial_delay_ms × 2^(n-1), max_delay_ms)
delay_ms ∈ [ceil(base/2), base]
available_at = 本次结束事务中的数据库时间 + delay_ms
```
抖动使用 AttemptID 生成确定值,同一尝试重复计算不改变时间;指数倍增先检查上限,避免溢出。返回 WAITING 表示下一次最早可领取时间,实际派发还受容量、队列暂停、流程门控、凭据、扫描与服务可用性影响,不承诺准点开始。
- 领取就消耗次数,包含未获开始授权的尝试。重连、ACK 重报和取消都不重置次数。
- 下一次 available_at 必须严格早于总截止和最晚开始时间;等于截止也不能安排重试。领取/开始仍重新核验数据库时间。
- attempt_timeout_ms 从获批开始计算;每次尝试重新计时,任务总截止保持不变。
- 尝试结果、retryable、任务状态、不确定标记、下一时间、许可释放、事件及结果操作审计/回执在同一事务提交。后台租约回收保存包含节点身份与决策的持久化事件。
- 等待重试期间不占执行许可,任务不带 result/completed_at;上一尝试的错误和结果保留在 [执行历史](execution-history.md)。NEEDS_REVIEW 也没有 completed_at。
- 重启后以数据库 available_at 继续领取,不依赖某一节点内存中的定时器。
`task.requeued` 等持久化事件记录 decision、delay_ms、next_available_at、attempt_count、max_attempts、effect_uncertain。decision 包括 RETRYABLE_FAILURE、IDEMPOTENT_UNKNOWN、UNSTARTED_LEASE_EXPIRED、ATTEMPT_LIMIT、DEADLINE_EXCEEDED、RECONCILIATION_REQUIRED 等。当前可靠事件订阅尚未开放。
取消接口支持独立任务和计划任务的 WAITING、LEASED、RUNNING 与 NEEDS_REVIEW。取消意图阻止后续尝试;运行中继续收集结果,普通取消错误或失联保持 UNKNOWN,不因 IDEMPOTENT 声明再次执行。此前未核实的效果不会被本次确认取消清除,详见 [取消状态与结果表](cancellation.md)。
## 4. SDK 接入
生产者在提交请求中设置有限策略,以下为函数片段:
```go
accepted, err := client.SubmitTask(ctx, operationKey, orbit.SubmitTaskRequest{
QueueID: queueID,
Handler: "text.normalize", HandlerVersion: "v1", PayloadVersion: "v1",
Payload: json.RawMessage(`{"text":" safe retry "}`),
RecoverySafety: orbit.RecoveryReconcileOnly,
TaskTimeoutMS: 60000,
Retry: orbit.RetryPolicy{
MaxAttempts: 3, InitialDelayMS: 500, MaxDelayMS: 2000,
AttemptTimeoutMS: 10000,
},
})
if err != nil { return err }
fmt.Println(accepted.Task.ID)
```
处理器在已确认可安全重试的失败分支返回:
```go
return nil, &orbit.WorkerFailure{
Code: "TEMPORARY_REJECTION",
Retryable: true,
Details: json.RawMessage(`{"reason_ref":"diagnostics/temporary"}`),
}
```
WorkerFailure 默认 Retryable=false;普通 error、panic、runtime.Goexit 和非法结果仍上报 UNKNOWN,由冻结的 recovery_safety 决定是否允许新尝试。不要把任意网络超时转成 Retryable=true。底层 SDK 对应 `WorkerFinish.Retryable`;其消息结构见 [Worker 契约](worker.md)。
## 5. 运行完整示例
源码:[examples/retry/main.go](../../sdk/go/examples/retry/main.go)。先运行当前迁移、服务,并按 [Worker 初始化步骤](../sdk/worker.md#1-可运行的文本规范化示例) 准备具有提交、读取、消费权限的专用凭据。从仓库根目录执行:
```sh
cd sdk/go
ORBIT_URL=http://127.0.0.1:8080 \
ORBIT_CREDENTIALS_FILE=../../.local/worker.credentials.json \
go run ./examples/retry
```
输入为 `{"text":" safe retry "}`。第一次处理在产生效果前模拟明确拒绝,第二次执行真正的文本规范化;重试等待由平台持久化安排。
```text
task=<任务ID> state=SUCCEEDED attempts=2 first_retryable=true result={"text":"SAFE RETRY"}
worker_drained=true started=2 confirmed=2 running=0
```
confirmed 统计已确认的尝试报告,包含第一次失败,不是成功任务数。示例通过尝试查询核对第一次 FAILED/retryable=true 与第二次 SUCCEEDED。每次运行生成独立操作和处理器身份,不连接外部业务系统;正常结束排空,历史保留。内存计数只用于注入一次故障,不能用作生产幂等存储。错误时非零退出并尝试有界收尾。
## 6. 回执与升级兼容
同 message_id 改变 retryable 或其他内容返回 IDEMPOTENCY_CONFLICT。保持同一结果而换 message_id 时,返回第一次已应用的结果回执;即使任务已在后续尝试成功,旧失败回执仍可能写着 WAITING。当前状态用 GetTask 查询;CommandResult.lease 仍只描述旧尝试当前是否有权执行。
迁移 000005 增加 effect_uncertain、尝试/证据 retryable 以及结果回执索引。已有 NEEDS_REVIEW 保守回填 effect_uncertain=true,保留原状态、版本和策略。已有结果摘要在 retryable 省略或 false 时保持兼容;true 属于不同语义。
本开发版本首次启用已冻结的多次尝试策略:旧的未结束任务若声明 max_attempts>1 / IDEMPOTENT,升级后也会按本页规则执行;不重写策略、不重置预算、不自动恢复已有 NEEDS_REVIEW。旧服务不识别新 Schema 版本,应停止旧节点后迁移并启动统一版本;尚未验证混合版本滚动升级或向旧二进制回退。SDK 尚未发布版本,自研 Worker 使用 retryable 前应升级服务端。