package outbox import ( "bytes" "context" "crypto/sha256" "encoding/json" "errors" "math/rand/v2" "time" orbit "git.yuntuo.hk/k2safe/orbit/sdk/go" ) // SendOnce 有界领取并发送一项;网络交换在数据库事务外,ErrIdle表示本次无到期项。 // 返回记录只包含本地发送状态;ACCEPTED不是业务执行成功,REVIEW必须人工核查原身份。 func (s *Store) SendOnce(parent context.Context, api *orbit.Client) (Record, error) { if api == nil { return Record{}, ErrInvalid } // 整个网络阶段从领取前即受单调时钟15秒约束,短于30秒租约;暂停进程后不会获得新发送预算。 work, stop := context.WithTimeout(parent, 15*time.Second) defer stop() r, err := s.claim(work) if err != nil { return Record{}, err } if r.State == "REVIEW" { return s.Status(parent, r.OperationID) } state, code, delay, receipt := s.deliver(work, api, r) // 网络超时不能阻止保存未知结果分类,收尾仍服从调用方总上下文且最多5秒。 if err = s.finish(parent, r, state, code, delay, receipt); err != nil { return Record{}, err } return s.Status(parent, r.OperationID) } func (s *Store) deliver(ctx context.Context, api *orbit.Client, r Record) (string, string, time.Duration, *orbit.SubmitTaskResult) { digest := sha256.Sum256(r.RequestJSON) if !bytes.Equal(digest[:], r.RequestDigest) || r.NamespaceID != s.namespace || r.QueueID != s.queue || r.IdempotencyKey != "outbox-task-"+r.OperationID { return "REVIEW", "INVALID_DOCUMENT", 0, nil } var in orbit.SubmitTaskRequest decoder := json.NewDecoder(bytes.NewReader(r.RequestJSON)) decoder.DisallowUnknownFields() if decoder.Decode(&in) != nil || in.QueueID != s.queue { return "REVIEW", "INVALID_DOCUMENT", 0, nil } // 先通过当前凭据读取固定队列;不靠配置声称命名空间正确,不向错误环境盲发命令。 queue, err := api.GetQueue(ctx, s.queue) if err != nil { state, code, delay := classify(err, r.SendCount) return state, code, delay, nil } if queue.ID != s.queue || queue.NamespaceID != s.namespace { return "REVIEW", "TARGET_MISMATCH", 0, nil } // 数据库时间决定剩余截止预算;额外查询不授予新租约,换代或临近截止时不再开始HTTP。 var remaining int64 err = s.db.QueryRowContext(ctx, `SELECT TIMESTAMPDIFF(MICROSECOND,UTC_TIMESTAMP(6),LEAST(wake_at,send_deadline_at)) FROM sender_outbox WHERE namespace_id=? AND queue_id=? AND operation_id=? AND state='SENDING' AND lease_epoch=?`, s.namespace, s.queue, r.OperationID, r.LeaseEpoch).Scan(&remaining) if err != nil || remaining <= 0 { return "PENDING", "UNCONFIRMED", time.Second, nil } send, stop := context.WithTimeout(ctx, min(10*time.Second, time.Duration(remaining)*time.Microsecond)) defer stop() result, err := api.SubmitTask(send, r.IdempotencyKey, in) if err != nil { state, code, delay := classify(err, r.SendCount) return state, code, delay, nil } if !validReceipt(r, result) { return "REVIEW", "INVALID_RECEIPT", 0, nil } return "ACCEPTED", "ACCEPTED", 0, &result } func validReceipt(r Record, receipt orbit.SubmitTaskResult) bool { t := receipt.Task return idPattern.MatchString(t.ID) && t.NamespaceID == r.NamespaceID && t.QueueID == r.QueueID && t.Revision > 0 } func classify(err error, count uint32) (state, code string, delay time.Duration) { state, code = "PENDING", "UNCONFIRMED" delay = min(time.Minute, time.Second*time.Duration(uint64(1)<