// 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 { CreatedAt time.Time `json:"created_at,omitzero"` 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 { AuthorizeEvents(context.Context, auth.Token) error BindConsumer(context.Context, auth.Token, core.ID) (ConsumerBinding, error) ListSubscriptions(context.Context, auth.Token, core.ID, PageInput) (SubscriptionPage, error) ListDeliveries(context.Context, auth.Token, core.ID, PageInput) (DeliveryPage, error) GetDeliveryDetail(context.Context, auth.Token, core.ID, core.ID) (DeliveryDetail, error) 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 }