Files
orbit/weave/recovery/service.go

203 lines
9.8 KiB
Go
Raw 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 recovery 定义有证据的人工核查与接续;不通过暂停开关清除未知执行或恢复预算。
package recovery
import (
"context"
"encoding/json"
"strings"
"unicode"
"unicode/utf8"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
"git.yuntuo.hk/k2safe/orbit/weave/instance"
)
const Action = "workflows:recover"
// MaxWaitActivations 包含首次等待,约束人工换代次数,防止无执行尝试的无限重建。
const MaxWaitActivations uint32 = 32
// Decision 核查基于整个实例的最新证据版本;后台新事实会使旧决定失效,原键仍可恢复回执。
type Decision struct {
ExpectedRevision uint64 `json:"expected_revision,string"` // ExpectedRevision 当前实例版本,不能使用control_revision代替。
Reason string `json:"reason"` // Reason 必填核查原因,最多1024个Unicode字符。
EvidenceRef string `json:"evidence_ref"` // EvidenceRef 受控证据引用,最多512个无空白或控制字符的Unicode字符;平台不请求地址。
}
type ReviewTaskInput struct {
Decision
TaskID core.ID `json:"task_id"` // TaskID 本实例任一历史激活拥有的待核查Pulse任务。
ExpectedTaskRevision uint64 `json:"expected_task_revision,string"` // ExpectedTaskRevision 任务当前状态版本。
ExecutionSettled bool `json:"execution_settled" required:"true"` // ExecutionSettled 显式确认该任务全部历史外部执行已结束且不会继续产生效果。
Outcome pulse.TaskState `json:"outcome"` // Outcome SUCCEEDED或FAILED,表示该次请求的核查结论,不代替POLL业务结果。
Result json.RawMessage `json:"result"` // Result 非null的核查结果,最多256 KiB,保留精确数字。
}
type StepDecision struct {
Decision
StepKey string `json:"step_key"` // StepKey 固定定义中的当前步骤,不接受内部根步骤。
ActivationID core.ID `json:"activation_id"` // ActivationID 调用者核查的当前激活,禁止旧激活作用于新工作。
ExpectedStepRevision uint64 `json:"expected_step_revision,string"` // ExpectedStepRevision 步骤版本,与实例版本共同校验。
}
type ResolveStepInput struct {
StepDecision
ExecutionSettled bool `json:"execution_settled" required:"true"` // ExecutionSettled 确认本步骤关联外部执行与业务结果已核实。
Outcome string `json:"outcome"` // Outcome SUCCEEDED或FAILED;成功必须符合固定节点输出契约。
Result json.RawMessage `json:"result" nullable:"true"` // Result 核查后的逻辑输出;TIMER成功输出为显式null,失败结论保留在审计摘要。
}
type ContinueInput struct {
Decision
ReviewResolved bool `json:"review_resolved" required:"true"` // ReviewResolved 确认全部核查事项已处理;服务端仍检查未决步骤、历史任务与Inbox。
}
type RetryStepInput struct {
StepDecision
SafeToRetry bool `json:"safe_to_retry" required:"true"` // SafeToRetry 确认旧执行已结束,并按原业务身份再次执行安全;不能只依据未收到响应。
}
type ContinuePollInput struct {
StepDecision
PendingConfirmed bool `json:"pending_confirmed" required:"true"` // PendingConfirmed 确认权威查询结果为处理中,按原间隔与剩余业务轮次接续。
}
type RearmWaitInput struct {
StepDecision
WaitVerified bool `json:"wait_verified" required:"true"` // WaitVerified 已核验当前等待及旧回执,按原契约/截止开启新等待代次。
}
type Result struct {
Instance instance.View `json:"instance"` // Instance 操作提交时的冻结实例回执;核查步骤不自动解除实例核查。
AuditID core.ID `json:"audit_id"` // AuditID 本次流程管理操作的不可变审计标识。
Step *instance.StepView `json:"step,omitempty"` // Step 被核查或重新激活步骤的回执。
Task *service.TaskView `json:"task,omitempty"` // Task 被核查或新创建的执行任务。
TaskAuditID *core.ID `json:"task_audit_id,omitempty"` // TaskAuditID 独立的执行结果核查审计,和流程审计同事务。
Deduplicated bool `json:"deduplicated"` // Deduplicated 原操作回执恢复,不再次修改状态或预算。
}
type Store interface {
ContinuePoll(context.Context, auth.Token, core.ID, string, ContinuePollInput) (Result, error)
RearmWait(context.Context, auth.Token, core.ID, string, RearmWaitInput) (Result, error)
ReviewTask(context.Context, auth.Token, core.ID, string, ReviewTaskInput) (Result, error)
ResolveStep(context.Context, auth.Token, core.ID, string, ResolveStepInput) (Result, error)
Continue(context.Context, auth.Token, core.ID, string, ContinueInput) (Result, error)
RetryStep(context.Context, auth.Token, core.ID, string, RetryStepInput) (Result, error)
}
type Service struct{ store Store }
func New(store Store) *Service { return &Service{store: store} }
func validateDecision(id core.ID, key string, in Decision) error {
if id.Validate() != nil || in.ExpectedRevision == 0 {
return fault.Invalid("workflow id and positive expected_revision required")
}
if err := service.ValidateKey(key); err != nil {
return err
}
if !utf8.ValidString(in.Reason) || strings.TrimSpace(in.Reason) == "" || utf8.RuneCountInString(in.Reason) > 1024 || strings.ContainsRune(in.Reason, 0) {
return fault.Invalid("recovery reason required, maximum 1024 characters")
}
if !utf8.ValidString(in.EvidenceRef) || len(in.EvidenceRef) == 0 || utf8.RuneCountInString(in.EvidenceRef) > 512 {
return fault.Invalid("valid evidence_ref required, maximum 512 characters")
}
for _, r := range in.EvidenceRef {
if unicode.IsSpace(r) || unicode.IsControl(r) {
return fault.Invalid("evidence_ref cannot contain whitespace or control characters")
}
}
return nil
}
func validateStep(in StepDecision) error {
if in.ActivationID.Validate() != nil || in.ExpectedStepRevision == 0 || len(in.StepKey) == 0 || len(in.StepKey) > 64 {
return fault.Invalid("current step identity and revision required")
}
for i, r := range in.StepKey {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || i > 0 && (r == '_' || r == '.' || r == '-')) {
return fault.Invalid("invalid step key")
}
}
return nil
}
func validateResult(raw json.RawMessage) error {
return definition.ValidateData(definition.Type{Kind: "JSON"}, raw)
}
func (s *Service) ReviewTask(ctx context.Context, t auth.Token, id core.ID, key string, in ReviewTaskInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if in.TaskID.Validate() != nil || in.ExpectedTaskRevision == 0 || !in.ExecutionSettled || in.Outcome != pulse.TaskSucceeded && in.Outcome != pulse.TaskFailed || strings.TrimSpace(string(in.Result)) == "null" {
return Result{}, fault.Invalid("task identity, execution_settled and known outcome required")
}
if err := validateResult(in.Result); err != nil {
return Result{}, fault.Invalid("valid non-null review result required, maximum 256 KiB")
}
return s.store.ReviewTask(ctx, t, id, key, in)
}
func (s *Service) ResolveStep(ctx context.Context, t auth.Token, id core.ID, key string, in ResolveStepInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if err := validateStep(in.StepDecision); err != nil {
return Result{}, err
}
if !in.ExecutionSettled || in.Outcome != "SUCCEEDED" && in.Outcome != "FAILED" {
return Result{}, fault.Invalid("execution_settled and known step outcome required")
}
if err := validateResult(in.Result); err != nil {
return Result{}, fault.Invalid("valid review result required, maximum 256 KiB")
}
return s.store.ResolveStep(ctx, t, id, key, in)
}
func (s *Service) Continue(ctx context.Context, t auth.Token, id core.ID, key string, in ContinueInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if !in.ReviewResolved {
return Result{}, fault.Invalid("review_resolved=true required")
}
return s.store.Continue(ctx, t, id, key, in)
}
func (s *Service) RetryStep(ctx context.Context, t auth.Token, id core.ID, key string, in RetryStepInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if err := validateStep(in.StepDecision); err != nil {
return Result{}, err
}
if !in.SafeToRetry {
return Result{}, fault.Invalid("safe_to_retry=true required")
}
return s.store.RetryStep(ctx, t, id, key, in)
}
func (s *Service) ContinuePoll(ctx context.Context, t auth.Token, id core.ID, key string, in ContinuePollInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if err := validateStep(in.StepDecision); err != nil {
return Result{}, err
}
if !in.PendingConfirmed {
return Result{}, fault.Invalid("pending_confirmed=true required")
}
return s.store.ContinuePoll(ctx, t, id, key, in)
}
func (s *Service) RearmWait(ctx context.Context, t auth.Token, id core.ID, key string, in RearmWaitInput) (Result, error) {
if err := validateDecision(id, key, in.Decision); err != nil {
return Result{}, err
}
if err := validateStep(in.StepDecision); err != nil {
return Result{}, err
}
if !in.WaitVerified {
return Result{}, fault.Invalid("wait_verified=true required")
}
return s.store.RearmWait(ctx, t, id, key, in)
}