# 批量任务受理 v1 状态:P3 已实现。`POST /v1/tasks:batch` 与 SDK `SubmitTasks` 提交一组独立任务,返回每项的受理结果。它不提供流程顺序、整批原子事务或业务效果恰好一次保证;步骤依赖由后续 Weave 管理。 ## 1. 请求与权限 请求头必须有 `Authorization: Bearer ` 与 `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,不会静默退化为其他语义。