feat: recover workflow waits within original deadlines
This commit is contained in:
@@ -19,6 +19,9 @@ import (
|
||||
|
||||
const Action = "workflows:recover"
|
||||
|
||||
// MaxWaitActivations 包含首次等待,约束人工换代次数,防止无执行尝试的无限重建。
|
||||
const MaxWaitActivations uint32 = 32
|
||||
|
||||
// Decision 核查基于整个实例的最新证据版本;后台新事实会使旧决定失效,原键仍可恢复回执。
|
||||
type Decision struct {
|
||||
ExpectedRevision uint64 `json:"expected_revision,string"` // ExpectedRevision 当前实例版本,不能使用control_revision代替。
|
||||
@@ -59,6 +62,16 @@ type RetryStepInput struct {
|
||||
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 本次流程管理操作的不可变审计标识。
|
||||
@@ -69,6 +82,8 @@ type Result struct {
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -160,3 +175,28 @@ func (s *Service) RetryStep(ctx context.Context, t auth.Token, id core.ID, key s
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ func (s *captureStore) RetryStep(context.Context, auth.Token, core.ID, string, R
|
||||
s.calls++
|
||||
return Result{}, nil
|
||||
}
|
||||
func (s *captureStore) ContinuePoll(context.Context, auth.Token, core.ID, string, ContinuePollInput) (Result, error) {
|
||||
s.calls++
|
||||
return Result{}, nil
|
||||
}
|
||||
func (s *captureStore) RearmWait(context.Context, auth.Token, core.ID, string, RearmWaitInput) (Result, error) {
|
||||
s.calls++
|
||||
return Result{}, nil
|
||||
}
|
||||
func TestRecoveryValidation(t *testing.T) {
|
||||
id := core.ID(strings.Repeat("a", 32))
|
||||
d := Decision{ExpectedRevision: 2, Reason: "核实外部结果", EvidenceRef: "evidence://case/1"}
|
||||
@@ -71,7 +79,19 @@ func TestRecoveryValidation(t *testing.T) {
|
||||
if _, err := s.RetryStep(ctx, token, id, "operation", RetryStepInput{StepDecision: step}); err == nil {
|
||||
t.Fatal("unsafe retry accepted")
|
||||
}
|
||||
if store.calls != 4 {
|
||||
if _, err := s.ContinuePoll(ctx, token, id, "operation", ContinuePollInput{StepDecision: step}); err == nil {
|
||||
t.Fatal("unconfirmed pending accepted")
|
||||
}
|
||||
if _, err := s.RearmWait(ctx, token, id, "operation", RearmWaitInput{StepDecision: step}); err == nil {
|
||||
t.Fatal("unverified wait accepted")
|
||||
}
|
||||
if _, err := s.ContinuePoll(ctx, token, id, "operation", ContinuePollInput{StepDecision: step, PendingConfirmed: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.RearmWait(ctx, token, id, "operation", RearmWaitInput{StepDecision: step, WaitVerified: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.calls != 6 {
|
||||
t.Fatal("invalid inputs reached storage", store.calls)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user