Files
orbit/pulse/service/batch.go

128 lines
4.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
)
// MaxBatchTasks 限制一次请求的独立任务数;HTTP 总请求仍限制为512 KiB。
const MaxBatchTasks = 32
type BatchSubmitInput struct {
// Items 按输入顺序分别受理,不保证整批原子性。
Items []BatchSubmitItem `json:"items"`
}
type BatchSubmitItem struct {
// IdempotencyKey 必须由调用方持久化,与单项提交共用命名空间内的幂等身份。
IdempotencyKey string `json:"idempotency_key"`
// Task 与单项受理完全相同的输入,重报时保留原值。
Task SubmitInput `json:"task"`
}
type BatchItemStatus string
const (
BatchAccepted BatchItemStatus = "ACCEPTED"
BatchRejected BatchItemStatus = "REJECTED"
BatchUnknown BatchItemStatus = "UNKNOWN"
BatchNotAttempted BatchItemStatus = "NOT_ATTEMPTED"
)
// BatchTaskReceipt 是原始受理回执的精简视图,避免重复返回大参数。
type BatchTaskReceipt struct {
// TaskID 已持久化的任务标识,业务结果需另行查询或订阅。
TaskID core.ID `json:"task_id"`
// QueueID 原始回执所属的队列标识。
QueueID core.ID `json:"queue_id"`
// Revision 原始受理版本,十进制字符串,不代表当前任务版本。
Revision uint64 `json:"revision,string"`
// CreatedAt 原始受理时间,UTC微秒精度。
CreatedAt time.Time `json:"created_at"`
// Deduplicated 本项是否返回已有幂等回执。
Deduplicated bool `json:"deduplicated"`
}
type BatchItemError struct {
// Code 稳定错误码;UNKNOWN不能被视为确认回滚。
Code string `json:"code"`
// Message 对外说明,不包含数据库错误或任务输入。
Message string `json:"message"`
// RetryAfterMS 建议最小退避毫秒数,省略为未指定,不代表可更换幂等键。
RetryAfterMS uint32 `json:"retry_after_ms,omitempty"`
}
type BatchSubmitItemResult struct {
// Index 本次请求内从0开始的索引,结果始终与输入同序且数量相同。
Index int `json:"index"`
// Status 区分已受理、明确拒绝、结果未知及本次未尝试。
Status BatchItemStatus `json:"status"`
// Receipt 仅ACCEPTED包含原始受理回执。
Receipt *BatchTaskReceipt `json:"receipt,omitempty"`
// Error 仅非ACCEPTED包含稳定错误及退避提示。
Error *BatchItemError `json:"error,omitempty"`
}
type BatchSubmitResult struct {
// Items 每项独立结果,HTTP成功不能替代逐项检查。
Items []BatchSubmitItemResult `json:"items"`
}
// SubmitBatch 逐项重用单项事务;不建立整批幂等身份或跨项锁。
func (s *Service) SubmitBatch(ctx context.Context, token auth.Token, in BatchSubmitInput) (BatchSubmitResult, error) {
if len(in.Items) < 1 || len(in.Items) > MaxBatchTasks {
return BatchSubmitResult{}, fault.Invalid("items must contain 1 to 32 tasks")
}
keys := make(map[string]struct{}, len(in.Items))
for _, item := range in.Items {
if err := ValidateKey(item.IdempotencyKey); err != nil {
return BatchSubmitResult{}, err
}
if _, exists := keys[item.IdempotencyKey]; exists {
return BatchSubmitResult{}, fault.Invalid("item idempotency keys must be distinct within a batch")
}
keys[item.IdempotencyKey] = struct{}{}
}
// 即使全部任务参数无效,也必须先验证凭据和动作权限;每项写入会再次认证。
if err := s.store.AuthorizeSubmit(ctx, token); err != nil {
return BatchSubmitResult{}, err
}
out := BatchSubmitResult{Items: make([]BatchSubmitItemResult, len(in.Items))}
stopped := false
for i, item := range in.Items {
result := BatchSubmitItemResult{Index: i}
if stopped || ctx.Err() != nil {
result.Status = BatchNotAttempted
result.Error = &BatchItemError{Code: "BATCH_STOPPED", Message: "this item was not attempted in this request; retry using the original identity", RetryAfterMS: 1000}
} else {
accepted, err := s.Submit(ctx, token, item.IdempotencyKey, item.Task)
if err == nil {
result.Status = BatchAccepted
result.Receipt = &BatchTaskReceipt{TaskID: accepted.Task.ID, QueueID: accepted.Task.QueueID, Revision: accepted.Task.Revision, CreatedAt: accepted.Task.CreatedAt, Deduplicated: accepted.Deduplicated}
} else {
var public *fault.Error
if !errors.As(err, &public) || public.Code == "UNAVAILABLE" {
// 驱动超时、COMMIT响应丢失等不能证明未提交,停止继续占用数据库。
public = fault.Unavailable
result.Status, stopped = BatchUnknown, true
} else {
result.Status = BatchRejected
stopped = public.Code == "UNAUTHENTICATED"
}
result.Error = &BatchItemError{Code: public.Code, Message: public.Message}
switch public.Code {
case "QUEUE_FULL", "APPLICATION_FULL", "UNAVAILABLE":
result.Error.RetryAfterMS = 1000
}
}
}
out.Items[i] = result
}
return out, nil
}