package orbit import ( "context" "errors" "time" ) // MaxBatchTasks 是单次批量受理的项数上限,总请求还受512 KiB限制。 const MaxBatchTasks = 32 type SubmitTasksRequest struct { // Items 1至32项独立任务,总请求不超过512 KiB,不保证整批原子性。 Items []SubmitTasksItem `json:"items"` } type SubmitTasksItem struct { // IdempotencyKey 调用方持久化的稳定键,与SubmitTask共用幂等身份;批内不得重复。 IdempotencyKey string `json:"idempotency_key"` // Task 原始任务参数,重报必须保留原值。 Task SubmitTaskRequest `json:"task"` } type BatchItemStatus string const ( BatchAccepted BatchItemStatus = "ACCEPTED" BatchRejected BatchItemStatus = "REJECTED" BatchUnknown BatchItemStatus = "UNKNOWN" BatchNotAttempted BatchItemStatus = "NOT_ATTEMPTED" ) type BatchTaskReceipt struct { // TaskID 已受理任务标识,实际结果需要查询或订阅。 TaskID string `json:"task_id"` // QueueID 任务所属队列。 QueueID string `json:"queue_id"` // Revision 原始受理版本,JSON为十进制字符串,不代表当前版本。 Revision uint64 `json:"revision,string"` // CreatedAt 原始受理时间,UTC微秒精度。 CreatedAt time.Time `json:"created_at"` // Deduplicated 是否复用相同身份的已有回执。 Deduplicated bool `json:"deduplicated"` } type BatchItemError struct { // Code 稳定错误码;UNKNOWN结果使用UNAVAILABLE,不证明回滚。 Code string `json:"code"` // Message 不包含数据库错误或输入的对外说明。 Message string `json:"message"` // RetryAfterMS 建议最小退避毫秒数,零为未指定,SDK不自动重试。 RetryAfterMS uint32 `json:"retry_after_ms,omitempty"` } type SubmitTasksItemResult struct { // Index 本次请求内从0开始的索引,与Items输入同序。 Index int `json:"index"` // Status 每项独立状态;HTTP成功不表示全部任务已受理。 Status BatchItemStatus `json:"status"` // Receipt 仅ACCEPTED存在,省略大参数以保证回执大小有界。 Receipt *BatchTaskReceipt `json:"receipt,omitempty"` // Error 仅非ACCEPTED存在,调用方需分别处理明确拒绝、未知及未尝试。 Error *BatchItemError `json:"error,omitempty"` } type SubmitTasksResult struct { // Items 与输入数量和顺序一致的逐项结果。 Items []SubmitTasksItemResult `json:"items"` } // SubmitTasks 逐项提交独立任务。返回error时整批可能已部分或全部受理,必须保留原键及参数核对。 // error为nil时仍需检查每项Status。此方法不会自动重试、拆批、生成新键或启动并发请求。 func (c *Client) SubmitTasks(ctx context.Context, in SubmitTasksRequest) (SubmitTasksResult, error) { if len(in.Items) < 1 || len(in.Items) > MaxBatchTasks { return SubmitTasksResult{}, errors.New("items must contain 1 to 32 tasks") } keys := make(map[string]struct{}, len(in.Items)) for _, item := range in.Items { if err := validateIdempotencyKey(item.IdempotencyKey); err != nil { return SubmitTasksResult{}, err } if _, exists := keys[item.IdempotencyKey]; exists { return SubmitTasksResult{}, errors.New("item idempotency keys must be distinct within a batch") } keys[item.IdempotencyKey] = struct{}{} } var out SubmitTasksResult if err := c.callJSON(ctx, "POST", "/v1/tasks:batch", "", in, &out); err != nil { return SubmitTasksResult{}, err } // 拒绝不完整或错误关联的回执,避免调用方将截断的成功响应误认为全部已受理。 if len(out.Items) != len(in.Items) { return SubmitTasksResult{}, errors.New("invalid Orbit batch result count") } for i, item := range out.Items { valid := item.Index == i switch item.Status { case BatchAccepted: r := item.Receipt valid = valid && item.Error == nil && r != nil && validID(r.TaskID) && r.QueueID == in.Items[i].Task.QueueID && validID(r.QueueID) && r.Revision > 0 && !r.CreatedAt.IsZero() case BatchRejected, BatchUnknown, BatchNotAttempted: e := item.Error valid = valid && item.Receipt == nil && e != nil && len(e.Code) > 0 && len(e.Code) <= 64 && len(e.Message) > 0 && len(e.Message) <= 1024 && e.RetryAfterMS <= 3600000 if valid && item.Status == BatchUnknown { valid = e.Code == "UNAVAILABLE" } if valid && item.Status == BatchNotAttempted { valid = e.Code == "BATCH_STOPPED" } if valid && item.Status == BatchRejected { valid = e.Code != "UNAVAILABLE" && e.Code != "BATCH_STOPPED" } default: valid = false } if !valid { return SubmitTasksResult{}, errors.New("invalid Orbit batch item result; retry using original identities") } } return out, nil }