161 lines
8.9 KiB
Markdown
161 lines
8.9 KiB
Markdown
# 批量任务受理 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示例](../../examples/batch/main.go),覆盖范围和实际证据见 [回归报告](../reports/phase-3-batch-tasks.md)。旧客户端继续使用单项接口;新SDK调用旧服务该路由会返回404,不会静默退化为其他语义。
|