12 KiB
人工核查、人工重试与任务操作审计
状态:P3 已实现服务端、Go SDK 和可运行场景。与自动安全重试不同,人工操作需要专门权限、原因、证据引用和当前版本。验证范围见阶段报告。
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;当前鉴权通过后优先返回原回执,不因任务已终结或额度已满重新执行。幂等保留约定与任务受理相同。 - 网络/提交结果不明时持久保留原键、原版本和完整正文核对;不要换键。回执是操作当时快照,最新状态用
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:
{
"expected_revision":"4",
"reason":"已核实旧执行全部结束,操作未完成",
"evidence_ref":"evidence:reconciliation-001",
"execution_settled":true,
"outcome":"FAILED",
"result":{"confirmed":true,"sequence":9007199254740993}
}
首次及去重均返回HTTP200;完整示意响应:
{
"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、不晚于新总截止,详见受理时间约束。不继承原任务已经过期的绝对时间。
{
"expected_revision":"5",
"reason":"已核实旧执行不会继续产生效果,允许重新执行",
"evidence_ref":"evidence:safe-retry-001",
"safe_to_retry":true,
"task_timeout_ms":60000
}
首次HTTP201,原键重报HTTP200且deduplicated=true。完整示意响应:
{
"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)分页;游标绑定命名空间、任务和查询种类。不是跨页快照,新的并发操作需从首页重新查询。
{
"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。
{
"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 或网络错误 | 操作可能已提交,保持原身份核对 |
完整通用错误边界与请求大小限制见任务接口。新接口调用旧服务返回404,不静默改成重新SubmitTask。旧SDK忽略retry_of响应字段;使用additionalProperties=false的旧版严格Schema验证器需更新Schema。需要恢复接口与关联追踪时升级当前SDK。Worker协议仍为v1,retry_of是可选新增字段。
迁移000015不新增模型字段:明确中文retry_of语义,将相同列的历史非唯一索引替换为唯一索引,并增加禁止自关联CHECK。停止旧写入节点后迁移并统一升级;迁移遇到历史非法重复或自关联应失败,不能自动删除数据。跨版本混合负载和生产滚动发布尚未验收。