Files
orbit/docs/api/retries.md

9.4 KiB
Raw Blame History

安全重试 v1

状态:P3 的安全重试已实现。使用现有任务提交与 Worker 结果接口,无独立的自动重试路由;Cron/固定间隔计划 和 运行中取消 已接入;可靠结果订阅 与业务Inbox已实现;独立任务人工核查与重试 已实现,剩余能力继续按 计划 开发。验证证据见 本阶段报告。

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,不自动增加业务尝试。字段范围见 任务接口。

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 次领取预算后,退避上限为:

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;上一尝试的错误和结果保留在 执行历史。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 等。可靠结果订阅已开放规定的结果类型;内部重排事件不自动成为外部可订阅类型。

提交参数delay_ms只控制首次受理的最早执行时间;它与这里按每次失败计算的退避分别定义。真实进程回归已覆盖退避期间唯一节点退出、两个新节点竞争恢复,以及停机期间总时限耗尽;冻结策略、已用次数和未知效果均保留,见验证记录。

取消接口支持独立任务和计划任务的 WAITING、LEASED、RUNNING 与 NEEDS_REVIEW。取消意图阻止后续尝试;运行中继续收集结果,普通取消错误或失联保持 UNKNOWN,不因 IDEMPOTENT 声明再次执行。此前未核实的效果不会被本次确认取消清除,详见 取消状态与结果表。

4. SDK 接入

生产者在提交请求中设置有限策略,以下为函数片段:

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)

处理器在已确认可安全重试的失败分支返回:

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 契约。

5. 运行完整示例

源码:examples/retry/main.go。先运行当前迁移、服务,并按 Worker 初始化步骤 准备具有提交、读取、消费权限的专用凭据。从仓库根目录执行:

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 "}。第一次处理在产生效果前模拟明确拒绝,第二次执行真正的文本规范化;重试等待由平台持久化安排。

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 前应升级服务端。