feat: add idempotent batch task submission and recovery examples

This commit is contained in:
2026-09-17 21:21:18 +08:00
parent a8a8dcfe1e
commit 571107f96d
27 changed files with 1375 additions and 24 deletions

View File

@@ -9,6 +9,7 @@
| `GET /health/live` | 已实现 | [健康检查](health.md) |
| `GET /health/ready` | 已实现,验证数据库、迁移记录及零外键要求 | [健康检查](health.md) |
| `/v1/tasks` 提交、查询、分页与待执行取消 | P1 已实现 | [任务接口](tasks.md) |
| `POST /v1/tasks:batch` | P3 已实现逐项受理、独立幂等、部分结果与补交 | [批量任务](batch-tasks.md) |
| `/v1/tasks/{task_id}/attempts` 与尝试详情 | P2 已实现 | [执行历史](execution-history.md) |
| `/v1/tasks/{task_id}/attempts/{attempt_id}/evidence` 与证据详情 | P2 已实现 | [执行历史](execution-history.md) |
| `/v1/queues` 创建、查询及 `:configure` 控制 | 已实现队列并发、开始速率、积压背压与暂停/恢复 | [队列接口与 SDK 场景](queues.md) |

160
docs/api/batch-tasks.md Normal file
View File

@@ -0,0 +1,160 @@
# 批量任务受理 v1
状态:P3 已实现。`POST /v1/tasks:batch` 与 SDK `SubmitTasks` 提交一组独立任务,返回每项的受理结果。它不提供流程顺序、整批原子事务或业务效果恰好一次保证;步骤依赖由后续 Weave 管理。
## 1. 请求与权限
请求头必须有 `Authorization: Bearer <token>` 与 `Content-Type: application/json`。**不传 `Idempotency-Key` 请求头**,传入会返回400;每项在正文使用自己的 `idempotency_key`。凭据必须具备 `tasks:submit`。预认证后,每项写入仍在自己的事务内重新校验凭据、动作、目标队列范围和命名空间归属。
| 字段 | 类型与约束 |
| --- | --- |
| items | 数组,1至32项,保持输入顺序处理 |
| items[].idempotency_key | 1至128个ASCII可打印非空白字符,同一批内不能重复;调用方发送前持久化 |
| items[].task | 与 [POST /v1/tasks](tasks.md#post-v1tasks) 完全相同的任务对象、默认值及归一化规则 |
总请求不超过512 KiB,每项payload仍不超过256 KiB。服务端请求上下文最多8秒;调用方可能更早取消。SDK不自动拆批或并发发送,32项是保护边界,不是吞吐承诺。不要通过增加批次大小规避背压。
示意请求(将队列ID替换为初始化结果):
```json
{
"items": [
{
"idempotency_key": "conversion-001",
"task": {
"queue_id": "11111111111111111111111111111111",
"handler": "file.convert",
"handler_version": "v1",
"payload_version": "v1",
"payload": {"source_ref": "objects/input-001.txt", "sequence": 9007199254740993}
}
},
{
"idempotency_key": "conversion-002",
"task": {
"queue_id": "11111111111111111111111111111111",
"handler": "file.convert",
"handler_version": "v1",
"payload_version": "v1",
"payload": {"source_ref": "objects/input-002.txt"}
}
}
]
}
```
保存为 `batch.json` 后调用:
```sh
curl -i "$ORBIT_URL/v1/tasks:batch" \
-H "Authorization: Bearer $ORBIT_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @batch.json
```
## 2. 响应与逐项状态
整批格式与初始权限检查通过后返回HTTP200,即使其中部分或全部项目被拒绝。必须读取 `data.items`,不能只依据HTTP状态判断受理成功。每个输入恰好对应一条结果,`index` 为**本次请求**内从0开始的索引。只补交子集时索引重新编号,稳定身份仍由原幂等键确定。
| status | 字段 | 本次请求的含义与处理 |
| --- | --- | --- |
| ACCEPTED | receipt,无error | 已确认受理事务提交或核对到原回执;任务执行结果另查或订阅 |
| REJECTED | error,无receipt | 本项明确拒绝,不因本次调用新增任务;需按错误原因处理,既有同键任务可能仍存在 |
| UNKNOWN | error.code=UNAVAILABLE | 无法确认提交结果,包括数据库/提交响应异常;必须用原键和原参数核对 |
| NOT_ATTEMPTED | error.code=BATCH_STOPPED | 本请求未尝试该项;之前其他请求可能已受理,仍须沿用原身份 |
例如第一项受理、第二项队列满额(ID和时间仅作示意):
```json
{
"request_id": "44444444444444444444444444444444",
"data": {
"items": [
{
"index": 0,
"status": "ACCEPTED",
"receipt": {
"task_id": "22222222222222222222222222222222",
"queue_id": "11111111111111111111111111111111",
"revision": "1",
"created_at": "2026-09-17T00:00:00Z",
"deduplicated": false
}
},
{
"index": 1,
"status": "REJECTED",
"error": {
"code": "QUEUE_FULL",
"message": "queue unfinished task limit reached; retry later using the original identity",
"retry_after_ms": 1000
}
}
]
}
}
```
`receipt` 是原始受理回执的精简视图,字段如下:
| 字段 | 类型与含义 |
| --- | --- |
| task_id / queue_id | 32位小写十六进制标识 |
| revision | 十进制字符串,原受理版本;任务已执行或取消后重报仍返回原版本 |
| created_at | 原任务创建时间,UTC RFC3339微秒精度 |
| deduplicated | bool,本项是否复用已有受理回执 |
回执不重复携带payload、结果或策略,32项返回大小有界。需要当前完整状态时调用 `GetTask`;取消、核查等后续操作应先查询当前版本。
## 3. 整批拒绝与部分失败
开始处理任何项目之前,完整读取并严格解析整个请求。以下情况整批返回错误信封,当前请求不写任务:
- HTTP400:空批/超过32项、无效或重复的项幂等键、传整批幂等头、未知字段/大小写别名/重复JSON键、字段类型或ID/时间编码错误、非法JSON/Unicode、非可空字段为null。
- HTTP413:总请求超过512 KiB;HTTP415:Content-Type不支持。
- HTTP401/403:初始凭据无效或无tasks:submit动作权限。
- HTTP503:预认证数据库不可用;此时还未进入逐项受理。
结构正确后,任务语义错误按项返回 `REJECTED/INVALID_ARGUMENT`,例如处理器为空、优先级超过9、有效时间范围不满足要求。队列不存在或跨命名空间为NOT_FOUND,缺少队列范围为PERMISSION_DENIED,同身份不同内容为IDEMPOTENCY_CONFLICT。队列或应用未终结上限分别为QUEUE_FULL、APPLICATION_FULL,按项提供1000毫秒退避提示;HTTP200不附整批Retry-After头。
处理遇到未知数据库结果后立即停止余项:当前项UNKNOWN,余项NOT_ATTEMPTED。中途凭据失效时当前项REJECTED/UNAUTHENTICATED,余项停止。上下文取消时,已确认提交的前项保留ACCEPTED,尚未开始的后项NOT_ATTEMPTED;在途项若无法确认提交则UNKNOWN。响应本身也可能丢失,不能依赖一定能收到这些状态。
一项队列越权、语义错误、幂等冲突或额度不足,不阻止后续独立项尝试。不在请求之间保存批次游标或整批快照,也不撤销此前已经受理的项目。每项任务、幂等记录、额度、事件与审计继续同事务提交;没有新增数据库表、外键或级联操作。
## 4. 幂等与补交
1. 在业务自己的事务中保存每项稳定键和完整请求内容,再发给平台。业务变更与平台受理仍有跨系统事务边界,后续事务Outbox示例单独交付。
2. 批量与单项提交使用相同的 `task.submit` 命名空间身份。批量受理后用同键调用SubmitTask会去重;反方向相同。换批次、重排或只补交子集不会改变身份。
3. 原回执核对先于当前积压额度检查,队列满额时仍可获取已受理项目的回执;当前凭据与队列授权仍须有效。
4. UNKNOWN、超时、连接断开或无法验证的响应:保留原键、原参数和原相对时限;不要重算available_at,不生成新键。不确定哪些项成功时可重报整批。
5. 明确额度不足时,退避并释放/等待配额后,可只补交被拒项目,仍用原身份。参数错误需要修正时先确认该操作从未受理,不能把“换键”当作解决幂等冲突的方法。
6. 幂等记录当前标记90天过期但尚无在线清理;生产清理窗口按后续保留策略执行,业务不得假定无限去重。
队列暂停仍允许受理。暂停、取消和任务执行与批量接口独立,提交顺序不保证执行顺序。业务效果去重仍由Worker/接收方负责。
## 5. Go SDK
```go
// items 来自业务库中的持久化待发记录。
result, err := client.SubmitTasks(ctx, orbit.SubmitTasksRequest{Items: items})
if err != nil {
// 网络/响应错误无法证明未提交;保留所有原身份,稍后核对。
return err
}
for _, item := range result.Items {
switch item.Status {
case orbit.BatchAccepted:
// 将task_id与对应的持久化操作关联;此处不代表业务执行成功。
fmt.Println(item.Index, item.Receipt.TaskID, item.Receipt.Deduplicated)
case orbit.BatchRejected:
// 根据item.Error.Code决定修正、人工处理或按RetryAfterMS退避。
fmt.Println(item.Index, item.Error.Code)
case orbit.BatchUnknown, orbit.BatchNotAttempted:
// 使用items[item.Index]的原键和原参数保留待核对状态。
}
}
```
SDK验证输入数量/身份/整体体积及返回数量、顺序、状态和回执归属;不自动重试、拆批或创建新键。`err == nil` 只表示取得可解释的逐项响应。局部错误为 `BatchItemError`,不是顶层APIError;整批HTTP错误仍为APIError,网络错误沿用标准库。独立SDK无数据库依赖。
完整运行命令、输入输出和收尾见 [批量场景](../examples/batch.md),源码见 [SDK示例](../../sdk/go/examples/batch/main.go),覆盖范围和实际证据见 [回归报告](../reports/phase-3-batch-tasks.md)。旧客户端继续使用单项接口;新SDK调用旧服务该路由会返回404,不会静默退化为其他语义。

View File

@@ -6,7 +6,7 @@ P2 进展:服务端执行内核、WSS 与独立自动 Worker SDK 已可产生
## 公共约定
业务接口需要 `Authorization: Bearer <token>`;写入需要 `Content-Type: application/json` 和稳定的 `Idempotency-Key`。凭据决定命名空间,请求体不能覆盖它。详见 [鉴权与初始化](authentication.md)。
业务接口需要 `Authorization: Bearer <token>`;单项写入需要 `Content-Type: application/json` 和稳定的 `Idempotency-Key`;[批量受理](batch-tasks.md) 在每项正文提供独立幂等键。凭据决定命名空间,请求体不能覆盖它。详见 [鉴权与初始化](authentication.md)。
请求体上限 512 KiB。拒绝未知字段、大小写别名、重复 JSON 键、无效 UTF-8、孤立 Unicode 代理项和超过 32 层的嵌套。除可空时间外,字段不能显式传 null。请求追踪 ID 由服务端生成,与幂等键分开。
@@ -190,4 +190,4 @@ SDK:`Client.CancelTask(ctx, taskID, key, request)`。此操作不承诺回滚
{"request_id":"44444444444444444444444444444444","error":{"code":"IDEMPOTENCY_CONFLICT","message":"idempotency key was used with different content"}}
```
有限自动重试已实现,具体分类、退避、回执和升级边界见 [安全重试](retries.md)。[运行中取消](cancellation.md) 已实现;批量受理和人工重试尚未实现。[执行历史](execution-history.md) 已提供尝试与证据的列表和详情。完整可运行程序见 [SDK 任务示例](../../sdk/go/examples/tasks/main.go),已验证异常场景见 [P1 回归报告](../reports/phase-1.md) 及 [取消回归](../reports/phase-3-cancellation.md)。SDK 忽略未知响应字段;更改已有字段语义需升级契约和回归。
有限自动重试已实现,具体分类、退避、回执和升级边界见 [安全重试](retries.md)。[运行中取消](cancellation.md) 已实现;[批量受理](batch-tasks.md) 已实现逐项结果与原身份补交;独立任务人工重试仍待实现。[执行历史](execution-history.md) 已提供尝试与证据的列表和详情。完整可运行程序见 [SDK 任务示例](../../sdk/go/examples/tasks/main.go),已验证异常场景见 [P1 回归报告](../reports/phase-1.md) 及 [取消回归](../reports/phase-3-cancellation.md)。SDK 忽略未知响应字段;更改已有字段语义需升级契约和回归。