// Package events 定义可靠结果订阅契约;交付采用至少一次语义,接收方按事件ID去重。 package events import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "regexp" "slices" "strings" "time" "unicode/utf8" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" platform "git.yuntuo.hk/k2safe/orbit/platform/model" ) const LeaseDuration = 30 * time.Second const MaxSubscriptions = 64 var ( LeaseLost = fault.New("DELIVERY_LEASE_LOST", "delivery proof was superseded or is invalid") HistoryExpired = fault.New("EVENT_HISTORY_EXPIRED", "requested event history is no longer retained") SubscriptionLimit = fault.New("SUBSCRIPTION_LIMIT", "namespace subscription limit reached") ) func ResultType(s string) bool { switch s { case "task.succeeded", "task.failed", "task.cancelled", "task.needs_review": return true } return false } // TaskResult 是事件发生时冻结的快照,不从后续可变任务状态重新生成。 type TaskResult struct { SchemaVersion uint32 `json:"schema_version"` TaskID core.ID `json:"task_id"` QueueID core.ID `json:"queue_id"` State string `json:"state"` Revision uint64 `json:"revision,string"` Result json.RawMessage `json:"result,omitempty"` EffectUncertain bool `json:"effect_uncertain"` CurrentAttemptID *core.ID `json:"current_attempt_id,omitempty"` } type Event struct { ID core.ID `json:"id"` Sequence uint64 `json:"sequence,string"` Type string `json:"type"` AggregateKind string `json:"aggregate_kind"` AggregateID core.ID `json:"aggregate_id"` AggregateRevision uint64 `json:"aggregate_revision,string"` OccurredAt time.Time `json:"occurred_at"` // Details 是来源事件元信息,不包含租约秘密和任务输入。 Details json.RawMessage `json:"details"` Snapshot TaskResult `json:"snapshot"` } type CreateInput struct { Name string `json:"name"` QueueID core.ID `json:"queue_id"` Types []string `json:"types,omitempty"` // FromSequence 之后的结果纳入订阅;省略为创建时已提交的最新序号,0为从现存日志开始。 FromSequence *uint64 `json:"from_sequence,string,omitempty"` MaxPending uint32 `json:"max_pending,omitempty"` MaxAttempts uint32 `json:"max_attempts,omitempty"` } type Subscription struct { ID core.ID `json:"id"` QueueID core.ID `json:"queue_id"` CredentialID core.ID `json:"credential_id"` Name string `json:"name"` Revision uint64 `json:"revision,string"` Enabled bool `json:"enabled"` Types []string `json:"types"` CursorSequence uint64 `json:"cursor_sequence,string"` PendingCount uint32 `json:"pending_count"` MaxPending uint32 `json:"max_pending"` MaxAttempts uint32 `json:"max_attempts"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type SubscriptionResult struct { Subscription Subscription `json:"subscription"` Deduplicated bool `json:"deduplicated"` } type ControlInput struct { ExpectedRevision uint64 `json:"expected_revision,string"` Enabled bool `json:"enabled"` Reason string `json:"reason"` } type Delivery struct { ID core.ID `json:"id"` SubscriptionID core.ID `json:"subscription_id"` EventID core.ID `json:"event_id"` Sequence uint64 `json:"sequence,string"` Revision uint64 `json:"revision,string"` State platform.DeliveryState `json:"state"` AvailableAt time.Time `json:"available_at"` LeaseEpoch uint64 `json:"lease_epoch,string"` LeaseUntil *time.Time `json:"lease_until,omitempty"` Attempts uint32 `json:"attempts"` AttemptBudget uint32 `json:"attempt_budget"` AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"` } // ClaimInput 由服务端已认证的连接生成,ConnectionID不得直接使用客户端自报值。 type ClaimInput struct { ConnectionID core.ID Capacity uint32 } type Grant struct { Delivery Delivery `json:"delivery"` Event Event `json:"event"` // LeaseSecret 仅交付给当前连接;持久层、日志和审计仅保存摘要。 LeaseSecret string `json:"lease_secret"` } type AckInput struct { DeliveryID core.ID `json:"delivery_id"` Epoch uint64 `json:"epoch,string"` LeaseSecret string `json:"lease_secret"` } type AckResult struct { DeliveryID core.ID `json:"delivery_id"` EventID core.ID `json:"event_id"` Epoch uint64 `json:"epoch,string"` AcknowledgedAt time.Time `json:"acknowledged_at"` Deduplicated bool `json:"deduplicated"` } type RetryInput struct { ExpectedRevision uint64 `json:"expected_revision,string"` Reason string `json:"reason"` AdditionalAttempts uint32 `json:"additional_attempts"` } type RetryResult struct { Delivery Delivery `json:"delivery"` Deduplicated bool `json:"deduplicated"` } type Store interface { CreateSubscription(context.Context, auth.Token, string, CreateInput) (SubscriptionResult, error) GetSubscription(context.Context, auth.Token, core.ID) (Subscription, error) ControlSubscription(context.Context, auth.Token, core.ID, string, ControlInput) (SubscriptionResult, error) GetDelivery(context.Context, auth.Token, core.ID, core.ID) (Delivery, error) ClaimDelivery(context.Context, auth.Token, core.ID, ClaimInput) (*Grant, error) AckDelivery(context.Context, auth.Token, core.ID, AckInput) (AckResult, error) RetryDelivery(context.Context, auth.Token, core.ID, core.ID, string, RetryInput) (RetryResult, error) } type Service struct{ store Store } func New(s Store) *Service { return &Service{store: s} } var validName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`) func keyValid(key string) error { if len(key) < 1 || len(key) > 128 { return fault.Invalid("invalid Idempotency-Key") } for _, c := range key { if c < 33 || c > 126 { return fault.Invalid("invalid Idempotency-Key") } } return nil } func reasonValid(revision uint64, reason string) bool { return revision > 0 && utf8.ValidString(reason) && strings.TrimSpace(reason) != "" && utf8.RuneCountInString(reason) <= 1024 } func (s *Service) Create(ctx context.Context, t auth.Token, key string, in CreateInput) (SubscriptionResult, error) { if err := keyValid(key); err != nil { return SubscriptionResult{}, err } if len(in.Name) > 128 || !validName.MatchString(in.Name) || in.QueueID.Validate() != nil { return SubscriptionResult{}, fault.Invalid("invalid subscription name or queue_id") } if in.MaxPending == 0 { in.MaxPending = 100 } if in.MaxAttempts == 0 { in.MaxAttempts = 20 } if in.MaxPending > 1000 || in.MaxAttempts > 100 { return SubscriptionResult{}, fault.Invalid("subscription limits are invalid") } if len(in.Types) == 0 { in.Types = []string{"task.succeeded", "task.failed", "task.cancelled", "task.needs_review"} } if len(in.Types) > 4 { return SubscriptionResult{}, fault.Invalid("too many event types") } in.Types = slices.Clone(in.Types) slices.Sort(in.Types) for i, v := range in.Types { if !ResultType(v) || i > 0 && v == in.Types[i-1] { return SubscriptionResult{}, fault.Invalid("unsupported or repeated event type") } } return s.store.CreateSubscription(ctx, t, key, in) } func (s *Service) Get(ctx context.Context, t auth.Token, id core.ID) (Subscription, error) { if id.Validate() != nil { return Subscription{}, fault.Invalid("invalid subscription_id") } return s.store.GetSubscription(ctx, t, id) } func (s *Service) Control(ctx context.Context, t auth.Token, id core.ID, key string, in ControlInput) (SubscriptionResult, error) { if err := keyValid(key); err != nil { return SubscriptionResult{}, err } if id.Validate() != nil || !reasonValid(in.ExpectedRevision, in.Reason) { return SubscriptionResult{}, fault.Invalid("subscription_id, revision and reason are required") } return s.store.ControlSubscription(ctx, t, id, key, in) } func (s *Service) Delivery(ctx context.Context, t auth.Token, id, delivery core.ID) (Delivery, error) { if id.Validate() != nil || delivery.Validate() != nil { return Delivery{}, fault.Invalid("invalid subscription_id or delivery_id") } return s.store.GetDelivery(ctx, t, id, delivery) } func (s *Service) Claim(ctx context.Context, t auth.Token, id core.ID, in ClaimInput) (*Grant, error) { if id.Validate() != nil || in.ConnectionID.Validate() != nil || in.Capacity < 1 || in.Capacity > 32 { return nil, fault.Invalid("invalid delivery claim") } return s.store.ClaimDelivery(ctx, t, id, in) } func (s *Service) Ack(ctx context.Context, t auth.Token, id core.ID, in AckInput) (AckResult, error) { if id.Validate() != nil || in.DeliveryID.Validate() != nil || in.Epoch == 0 { return AckResult{}, fault.Invalid("invalid delivery confirmation") } if _, err := SecretDigest(in.LeaseSecret); err != nil { return AckResult{}, err } return s.store.AckDelivery(ctx, t, id, in) } func (s *Service) Retry(ctx context.Context, t auth.Token, id, delivery core.ID, key string, in RetryInput) (RetryResult, error) { if err := keyValid(key); err != nil { return RetryResult{}, err } if id.Validate() != nil || delivery.Validate() != nil || !reasonValid(in.ExpectedRevision, in.Reason) || in.AdditionalAttempts < 1 || in.AdditionalAttempts > 100 { return RetryResult{}, fault.Invalid("invalid delivery retry") } return s.store.RetryDelivery(ctx, t, id, delivery, key, in) } func SecretDigest(secret string) (core.Digest, error) { if len(secret) != 43 { return core.Digest{}, fault.Invalid("invalid delivery secret") } raw, err := base64.RawURLEncoding.Strict().DecodeString(secret) if err != nil || len(raw) != 32 { return core.Digest{}, fault.Invalid("invalid delivery secret") } return core.Digest(sha256.Sum256(raw)), nil } func NewSecret() (string, core.Digest, error) { var raw [32]byte if _, err := rand.Read(raw[:]); err != nil { return "", core.Digest{}, err } return base64.RawURLEncoding.EncodeToString(raw[:]), core.Digest(sha256.Sum256(raw[:])), nil }