package service import ( "bytes" "context" "regexp" "strings" "time" "unicode" "unicode/utf8" "git.yuntuo.hk/k2safe/orbit/internal/fault" "git.yuntuo.hk/k2safe/orbit/internal/jsonvalue" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" pulse "git.yuntuo.hk/k2safe/orbit/pulse/model" ) type Service struct{ store Store } func New(store Store) *Service { return &Service{store: store} } var namePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:-]*$`) func ValidName(s string, max int) bool { return len(s) > 0 && len(s) <= max && namePattern.MatchString(s) } func ValidateKey(key string) error { if len(key) < 1 || len(key) > 128 { return fault.Invalid("Idempotency-Key must contain 1 to 128 printable ASCII characters") } for _, c := range key { if c < 33 || c > 126 { return fault.Invalid("Idempotency-Key must contain printable ASCII characters without spaces") } } return nil } func NormalizeSubmit(in SubmitInput) (SubmitInput, error) { if in.QueueID.Validate() != nil { return in, fault.Invalid("queue_id is invalid") } if !ValidName(in.Handler, 128) || !ValidName(in.HandlerVersion, 64) || !ValidName(in.PayloadVersion, 32) { return in, fault.Invalid("handler and version names are required ASCII identifiers") } if len(in.Payload) == 0 || len(in.Payload) > 256<<10 || bytes.Equal(bytes.TrimSpace(in.Payload), []byte("null")) { return in, fault.Invalid("payload must be a non-null JSON value up to 256 KiB") } if _, err := jsonvalue.Canonical(in.Payload); err != nil { return in, fault.Invalid("payload is invalid, duplicated, too deep or outside numeric limits") } if !utf8.ValidString(in.BusinessKey) || utf8.RuneCountInString(in.BusinessKey) > 191 || strings.ContainsAny(in.BusinessKey, "\x00\r\n") { return in, fault.Invalid("business_key must be at most 191 characters without control newlines") } if err := ValidateConcurrencyKey(in.ConcurrencyKey); err != nil { return in, err } if in.Priority < 0 || in.Priority > 9 { return in, fault.Invalid("priority must be between 0 and 9") } if in.TaskTimeoutMS == 0 { in.TaskTimeoutMS = 24 * 60 * 60 * 1000 } if in.TaskTimeoutMS > 30*24*60*60*1000 { return in, fault.Invalid("task_timeout_ms exceeds 30 days") } if in.DelayMS >= in.TaskTimeoutMS || (in.DelayMS != 0 && in.AvailableAt != nil) { return in, fault.Invalid("delay_ms must be less than task_timeout_ms and cannot be combined with available_at") } if in.RecoverySafety == "" { in.RecoverySafety = pulse.RecoveryReconcileOnly } if in.RecoverySafety != pulse.RecoveryReconcileOnly && in.RecoverySafety != pulse.RecoveryIdempotent { return in, fault.Invalid("invalid recovery_safety") } if in.Retry.MaxAttempts == 0 { in.Retry.MaxAttempts = 1 } if in.Retry.AttemptTimeoutMS == 0 { in.Retry.AttemptTimeoutMS = 30000 } if in.Retry.MaxAttempts > 100 || in.Retry.AttemptTimeoutMS > in.TaskTimeoutMS { return in, fault.Invalid("retry attempts or timeout exceed limits") } if in.Retry.MaxAttempts > 1 { if in.Retry.InitialDelayMS == 0 { in.Retry.InitialDelayMS = 1000 } if in.Retry.MaxDelayMS == 0 { in.Retry.MaxDelayMS = 30000 } } if err := in.Retry.Validate(); err != nil { return in, fault.Invalid("invalid retry policy") } for _, t := range []**time.Time{&in.AvailableAt, &in.StartDeadlineAt} { if *t != nil { v := (**t).UTC() if v.Year() < 2000 || v.Year() > 9998 || v.Nanosecond()%1000 != 0 { return in, fault.Invalid("times must use microsecond precision and supported years") } *t = &v } } return in, nil } func (s *Service) Submit(ctx context.Context, token auth.Token, key string, in SubmitInput) (SubmitResult, error) { if err := ValidateKey(key); err != nil { return SubmitResult{}, err } in, err := NormalizeSubmit(in) if err != nil { return SubmitResult{}, err } return s.store.Submit(ctx, token, key, in) } func (s *Service) GetTask(ctx context.Context, token auth.Token, id core.ID) (TaskView, error) { if id.Validate() != nil { return TaskView{}, fault.Invalid("task id is invalid") } return s.store.GetTask(ctx, token, id) } func (s *Service) ListTasks(ctx context.Context, token auth.Token, in ListInput) (TaskPage, error) { if in.QueueID.Validate() != nil { return TaskPage{}, fault.Invalid("queue_id is required") } if in.Limit == 0 { in.Limit = 20 } if in.Limit < 1 || in.Limit > 100 || len(in.Cursor) > 1024 { return TaskPage{}, fault.Invalid("invalid page limit or cursor") } switch in.State { case "", pulse.TaskWaiting, pulse.TaskLeased, pulse.TaskRunning, pulse.TaskNeedsReview, pulse.TaskSucceeded, pulse.TaskFailed, pulse.TaskCancelled: default: return TaskPage{}, fault.Invalid("invalid task state") } if !utf8.ValidString(in.BusinessKey) || utf8.RuneCountInString(in.BusinessKey) > 191 || strings.ContainsAny(in.BusinessKey, "\x00\r\n") { return TaskPage{}, fault.Invalid("business_key must be at most 191 characters without control newlines") } for _, field := range []**time.Time{&in.CreatedFrom, &in.CreatedBefore} { if *field != nil { v := (**field).UTC() if v.Year() < 2000 || v.Year() > 9998 || v.Nanosecond()%1000 != 0 { return TaskPage{}, fault.Invalid("creation bounds must use supported years and microsecond precision") } *field = &v } } if in.CreatedFrom != nil && in.CreatedBefore != nil && !in.CreatedFrom.Before(*in.CreatedBefore) { return TaskPage{}, fault.Invalid("created_from must be before created_before") } return s.store.ListTasks(ctx, token, in) } func (s *Service) CancelTask(ctx context.Context, token auth.Token, id core.ID, key string, in CancelInput) (CancelResult, error) { if id.Validate() != nil { return CancelResult{}, fault.Invalid("task id is invalid") } if err := ValidateKey(key); err != nil { return CancelResult{}, err } if strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || utf8.RuneCountInString(in.Reason) > 1024 || in.ExpectedRevision == 0 { return CancelResult{}, fault.Invalid("reason and nonzero expected_revision are required") } return s.store.CancelTask(ctx, token, id, key, in) } func (s *Service) CreateQueue(ctx context.Context, token auth.Token, key string, in QueueInput) (QueueResult, error) { if err := ValidateKey(key); err != nil { return QueueResult{}, err } if !ValidName(in.Name, 128) || in.MaxConcurrency == 0 || in.MaxConcurrency > 100000 || in.RatePerSecond == 0 || in.RatePerSecond > 1000000 || in.MaxUnfinishedTasks > 1000000 { return QueueResult{}, fault.Invalid("queue name, concurrency or rate is invalid") } return s.store.CreateQueue(ctx, token, key, in) } func (s *Service) GetQueue(ctx context.Context, token auth.Token, id core.ID) (QueueView, error) { if id.Validate() != nil { return QueueView{}, fault.Invalid("queue id is invalid") } return s.store.GetQueue(ctx, token, id) } func (s *Service) ConfigureQueue(ctx context.Context, token auth.Token, id core.ID, key string, in QueueConfigInput) (QueueResult, error) { if id.Validate() != nil { return QueueResult{}, fault.Invalid("queue id is invalid") } if err := ValidateKey(key); err != nil { return QueueResult{}, err } if in.ExpectedRevision == 0 || strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || utf8.RuneCountInString(in.Reason) > 1024 { return QueueResult{}, fault.Invalid("reason and nonzero expected_revision are required") } if in.MaxConcurrency == 0 || in.MaxConcurrency > 100000 || in.RatePerSecond == 0 || in.RatePerSecond > 1000000 || in.MaxUnfinishedTasks == 0 || in.MaxUnfinishedTasks > 1000000 { return QueueResult{}, fault.Invalid("queue limits are invalid") } return s.store.ConfigureQueue(ctx, token, id, key, in) } // RetireQueue 只关闭已结清的通用资源,不删除任务、交付或审计。 func (s *Service) RetireQueue(ctx context.Context, t auth.Token, id core.ID, key string, in RetireQueueInput) (QueueResult, error) { if err := ValidateKey(key); err != nil { return QueueResult{}, err } if id.Validate() != nil || in.ExpectedRevision == 0 || strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || utf8.RuneCountInString(in.Reason) > 1024 { return QueueResult{}, fault.Invalid("queue id, revision and reason are required") } return s.store.RetireQueue(ctx, t, id, key, in) } // ValidateConcurrencyKey 不归一化资源键,字节精确相等才共享许可。 func ValidateConcurrencyKey(key string) error { if key == "" { return nil } if !utf8.ValidString(key) || utf8.RuneCountInString(key) > 191 || strings.TrimSpace(key) != key { return fault.Invalid("invalid concurrency_key") } for _, c := range key { if unicode.IsControl(c) { return fault.Invalid("invalid concurrency_key") } } return nil }