Files
orbit/docs/api/task-recovery.md

233 lines
12 KiB
Markdown
Raw Permalink 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.
# 人工核查、人工重试与任务操作审计
状态:P3 已实现服务端、Go SDK 和[可运行场景](../examples/recovery.md)。与[自动安全重试](retries.md)不同,人工操作需要专门权限、原因、证据引用和当前版本。验证范围见[阶段报告](../reports/phase-3-task-recovery.md)。
## 1. 公共规则
- HTTPS `Authorization: Bearer <token>`,写入带 `Content-Type: application/json` 与稳定 `Idempotency-Key`;查询不带幂等键。
- 权限分别是 `tasks:review`、`tasks:retry`、`tasks:audit:read`,均校验任务当前命名空间和队列范围。默认提交及 Worker 凭据不包含它们;开发初始化显式 `--recovery-access` 才添加。真实生产应分离执行与管理身份。
- ID 为32位小写十六进制;`expected_revision` 是非零十进制字符串。正文最多512 KiB,拒绝未知字段、重复键、大小写别名及非允许的null。
- 写操作幂等作用域:命名空间 + `task.review` / `task.retry` + 原任务ID + key。同键不同内容409;当前鉴权通过后优先返回原回执,不因任务已终结或额度已满重新执行。幂等保留约定与[任务受理](tasks.md)相同。
- 网络/提交结果不明时持久保留**原键、原版本和完整正文**核对;不要换键。回执是操作当时快照,最新状态用 `GetTask`。SDK不自动生成身份或自动重试。
- `reason`:非空白UTF-8,1–1024个Unicode字符,无NUL。`evidence_ref`:1–512个Unicode字符,无空白/控制字符;使用受控记录引用,禁止放入凭据秘密。平台只保存引用,不请求地址或自动判定证据真实性。
## 2. POST /v1/tasks/{task_id}:review
权限 `tasks:review`。仅 `NEEDS_REVIEW` 且版本匹配、无当前租约、无活跃尝试和未释放许可时允许。普通独立任务可核查;合法计划任务需同事务核验计划与触发记录归属。流程所属任务或有门控的任务拒绝,恢复须经对应上层控制器。
| 字段 | 类型/要求 | 含义 |
| --- | --- | --- |
| expected_revision | string,必填 | 当前待核查任务版本 |
| reason / evidence_ref | string,必填 | 上述原因和证据边界 |
| execution_settled | bool,必须显式true | 声明已核实全部历史执行已结束、不会继续产生效果 |
| outcome | string,必填 | 仅SUCCEEDED或FAILED |
| result | JSON,必填非null | 核查结论或业务结果引用,最多256 KiB,保留精确数值 |
先收集执行历史、迟到证据及外部系统凭证,核实所有旧执行和实际效果,再提交结论。仅“数据库租约已过期”不足以声明执行已经物理停止;平台无法杀死远程goroutine,也无法证明外部系统没有副作用。
请求示例,原状态为 NEEDS_REVIEW/revision=4:
```json
{
"expected_revision":"4",
"reason":"已核实旧执行全部结束,操作未完成",
"evidence_ref":"evidence:reconciliation-001",
"execution_settled":true,
"outcome":"FAILED",
"result":{"confirmed":true,"sequence":9007199254740993}
}
```
首次及去重均返回HTTP200;完整示意响应:
```json
{
"request_id": "44444444444444444444444444444444",
"data": {
"task": {
"id": "22222222222222222222222222222222",
"namespace_id": "33333333333333333333333333333333",
"queue_id": "11111111111111111111111111111111",
"state": "FAILED",
"revision": "5",
"handler": "object.prepare",
"handler_version": "v1",
"payload_version": "v1",
"payload": {
"object_ref": "objects/input",
"sequence": 9007199254740993
},
"business_key": "object:operation-001",
"available_at": "2026-09-17T00:00:00Z",
"deadline_at": "2026-09-17T00:01:00Z",
"recovery_safety": "RECONCILE_ONLY",
"retry": {
"max_attempts": 1,
"initial_delay_ms": 0,
"max_delay_ms": 0,
"attempt_timeout_ms": 10000
},
"attempt_count": 1,
"result": {
"confirmed": true,
"sequence": 9007199254740993
},
"completed_at": "2026-09-17T00:00:20Z",
"created_at": "2026-09-17T00:00:00Z",
"updated_at": "2026-09-17T00:00:20Z"
},
"audit_id": "55555555555555555555555555555555",
"deduplicated": false
}
}
```
事务内保存新终态/result、清除effect_uncertain、revision加一、设置completed_at,释放一次未终结额度;同时保存幂等回执、不可变审计及新的结果事件。失败整体回滚,不能只更新状态后补写审计。旧尝试及UNKNOWN结果不改写,迟到报告仍只追加证据;已有needs_review结果事件保持冻结,订阅会再收到更高版本的最终结果。串行计划可以在核查终结后推进后续触发。
SDK:`ReviewTask(ctx, taskID, key, orbit.ReviewTaskRequest)` 返回 `ReviewTaskResult`。
## 3. POST /v1/tasks/{task_id}:retry
权限 `tasks:retry`。只接受**没有上层所有者的独立任务**,原状态FAILED或CANCELLED,effect_uncertain=false,版本匹配,无租约/活跃尝试/未释放许可。NEEDS_REVIEW先核查,SUCCEEDED不能重试;计划任务和流程任务均拒绝直接生成后继。
| 字段 | 类型/要求 | 含义 |
| --- | --- | --- |
| expected_revision | string,必填 | 原终态任务的当前版本 |
| reason / evidence_ref | string,必填 | 重执行原因与安全依据 |
| safe_to_retry | bool,必须显式true | 声明已确认旧执行结束,按原业务身份重新执行安全 |
| task_timeout_ms | uint64,必填1–2592000000 | 新任务从受理起的总预算,最多30天;至少覆盖冻结的attempt_timeout_ms |
| available_at | time/null,可选 | 新任务最早可执行时间,默认当前数据库时间 |
| start_deadline_at | time/null,可选 | 新任务最晚开始时间,默认无独立限制,受新总截止时间约束 |
时间采用UTC RFC3339、微秒精度、年份2000–9998。available_at只允许最多1秒过去容差,必须早于新总截止;start_deadline_at必须晚于现在、不早于available_at、不晚于新总截止,详见[受理时间约束](tasks.md)。不继承原任务已经过期的绝对时间。
```json
{
"expected_revision":"5",
"reason":"已核实旧执行不会继续产生效果,允许重新执行",
"evidence_ref":"evidence:safe-retry-001",
"safe_to_retry":true,
"task_timeout_ms":60000
}
```
首次HTTP201,原键重报HTTP200且deduplicated=true。完整示意响应:
```json
{
"request_id": "77777777777777777777777777777777",
"data": {
"source_task_id": "22222222222222222222222222222222",
"task": {
"id": "66666666666666666666666666666666",
"namespace_id": "33333333333333333333333333333333",
"queue_id": "11111111111111111111111111111111",
"state": "WAITING",
"revision": "1",
"handler": "object.prepare",
"handler_version": "v1",
"payload_version": "v1",
"payload": {
"object_ref": "objects/input",
"sequence": 9007199254740993
},
"business_key": "object:operation-001",
"available_at": "2026-09-17T00:00:30Z",
"deadline_at": "2026-09-17T00:01:30Z",
"recovery_safety": "RECONCILE_ONLY",
"retry": {
"max_attempts": 1,
"initial_delay_ms": 0,
"max_delay_ms": 0,
"attempt_timeout_ms": 10000
},
"attempt_count": 0,
"created_at": "2026-09-17T00:00:30Z",
"updated_at": "2026-09-17T00:00:30Z",
"retry_of": "22222222222222222222222222222222"
},
"audit_id": "88888888888888888888888888888888",
"deduplicated": false
}
}
```
新任务保留原队列、处理器与输入版本、payload、business_key、concurrency_key、priority、recovery_safety和冻结重试策略;新ID、revision=1、attempt_count=0和新时间预算,`retry_of`指回原任务。原任务终态、版本、结果和尝试次数均保持不变。
每个原任务最多一个直接重试后继;不同键竞争时只有一个成功,其余409,继续人工重试应沿失败/取消的后继任务操作。同键重报返回同一子任务和audit_id。唯一索引 `(namespace_id,retry_of)` 兜底,关联存在与归属由事务校验,**无数据库外键**。
新任务占用原队列与应用额度,仍受背压/暂停/并发/限速/资源键控制;满额不创建子任务、不遗留幂等身份。人工声明不能替代业务幂等:业务操作身份仍须保持稳定,新taskID不能直接用作外部业务的去重身份。
`task.retry`审计记在原任务上,after包含retry_task_id;子任务的task.accepted事件包含retry_of和audit_id。任务详情、单项回执和Worker分配中的retry_of为新增可选字段;任务摘要列表没有该字段。
SDK:`RetryTask(ctx, taskID, key, orbit.RetryTaskRequest)` 返回 `RetryTaskResult`。
## 4. 查询不可变操作审计
权限 `tasks:audit:read`,与普通tasks:read分开;当前凭据与队列范围逐次校验,不返回其他任务的审计或秘密。
`GET /v1/tasks/{task_id}/operations?limit=20&cursor=...`:limit默认20,1–100,cursor最长1024;参数只能出现一次且不能传未知筛选。按(created_at DESC,id DESC)分页;游标绑定命名空间、任务和查询种类。不是跨页快照,新的并发操作需从首页重新查询。
```json
{
"request_id":"44444444444444444444444444444444",
"data":{
"items":[{
"id":"88888888888888888888888888888888",
"task_id":"22222222222222222222222222222222",
"operation_id":"99999999999999999999999999999999",
"actor_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"action":"task.retry",
"created_at":"2026-09-17T00:00:30Z"
}],
"next_cursor":"<服务端返回的不透明游标>"
}
}
```
`GET /v1/tasks/{task_id}/operations/{audit_id}`:200详情在上述摘要基础上增加reason、可选evidence_ref、before和after;不存在或不属该任务返回404。
```json
{
"request_id":"44444444444444444444444444444444",
"data":{
"id":"88888888888888888888888888888888",
"task_id":"22222222222222222222222222222222",
"operation_id":"99999999999999999999999999999999",
"actor_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"action":"task.retry",
"created_at":"2026-09-17T00:00:30Z",
"reason":"已核实旧执行不会继续产生效果,允许重新执行",
"evidence_ref":"evidence:safe-retry-001",
"before":{"state":"FAILED","revision":"5"},
"after":{
"source_task_id":"22222222222222222222222222222222",
"retry_task_id":"66666666666666666666666666666666",
"safe_to_retry":true,
"task_timeout_ms":60000
}
}
}
```
核查after包含state、revision、effect_uncertain=false、execution_settled=true及result_digest。摘要不复制完整业务输入;详情也不返回租约/凭据秘密。核查/重试的actor_id由实际认证凭据确定,不能在正文自报。审计可含受理、取消、执行结果等其他动作;查询操作记录不等于获得任务执行权。
SDK:`ListTaskOperations(ctx, taskID, HistoryPageRequest)`、`GetTaskOperation(ctx, taskID, auditID)`。
## 5. 错误与兼容
| HTTP/错误码 | 处理 |
| --- | --- |
| 400 INVALID_ARGUMENT | 修正字段、显式确认、预算、JSON或游标,不触及任务 |
| 401 UNAUTHENTICATED | 核对凭据到期/撤销与所属应用启用情况 |
| 403 PERMISSION_DENIED | 当前凭据未获该管理/审计权限 |
| 404 NOT_FOUND | 任务/操作不存在,或命名空间/队列/任务归属不匹配 |
| 409 STATE_CONFLICT | 状态/版本/所有者/门控不允许,或已有直接重试后继;先读当前状态和审计 |
| 409 IDEMPOTENCY_CONFLICT | 同操作身份正文变化,核对已保存原请求 |
| 429 QUEUE_FULL / APPLICATION_FULL | 新任务受理背压,释放额度后原请求重试;错误携带retry_after_ms提示 |
| 503 UNAVAILABLE 或网络错误 | 操作可能已提交,保持原身份核对 |
完整通用错误边界与请求大小限制见[任务接口](tasks.md)。新接口调用旧服务返回404,不静默改成重新SubmitTask。旧SDK忽略retry_of响应字段;使用additionalProperties=false的旧版严格Schema验证器需更新Schema。需要恢复接口与关联追踪时升级当前SDK。Worker协议仍为v1,retry_of是可选新增字段。
迁移000015不新增模型字段:明确中文retry_of语义,将相同列的历史非唯一索引替换为唯一索引,并增加禁止自关联CHECK。停止旧写入节点后迁移并统一升级;迁移遇到历史非法重复或自关联应失败,不能自动删除数据。跨版本混合负载和生产滚动发布尚未验收。