feat: recover poll activations within original round budgets
This commit is contained in:
@@ -17,36 +17,66 @@ import (
|
||||
)
|
||||
|
||||
// readWorkflowPollState 校验当前激活的完整轮次与原预算;时钟和最终信号共用相同检查。
|
||||
func readWorkflowPollState(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, activated time.Time) (weave.PollState, core.ID, error) {
|
||||
func readWorkflowPollState(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep) (weave.PollState, core.ID, error) {
|
||||
p, last, err := readWorkflowPollBudget(ctx, tx, g, row)
|
||||
if err == nil && p.ResolvedAt != nil {
|
||||
err = errors.New("unsettled poll step has resolved budget")
|
||||
}
|
||||
return p, last, err
|
||||
}
|
||||
|
||||
// readWorkflowPollBudget 也允许读取已结清预算,供有证据的人工恢复核对;不重新计时。
|
||||
func readWorkflowPollBudget(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep) (weave.PollState, core.ID, error) {
|
||||
var p weave.PollState
|
||||
err := tx.QueryRowContext(ctx, `SELECT id,revision,max_rounds,used_rounds,max_attempts_per_round,interval_ms,deadline_at,next_poll_at,current_task_id,resolved_at FROM weave_poll_states WHERE namespace_id=? AND workflow_id=? AND activation_id=? FOR UPDATE`, g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&p.ID, &p.Revision, &p.MaxRounds, &p.UsedRounds, &p.MaxAttemptsPerRound, &p.IntervalMS, &p.DeadlineAt, &p.NextPollAt, &p.CurrentTaskID, &p.ResolvedAt)
|
||||
err := tx.QueryRowContext(ctx, `SELECT id,revision,max_rounds,used_rounds,first_round,first_round_attempts_used,max_attempts_per_round,interval_ms,deadline_at,next_poll_at,current_task_id,resolved_at FROM weave_poll_states WHERE namespace_id=? AND workflow_id=? AND activation_id=? FOR UPDATE`, g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&p.ID, &p.Revision, &p.MaxRounds, &p.UsedRounds, &p.FirstRound, &p.FirstRoundAttemptsUsed, &p.MaxAttemptsPerRound, &p.IntervalMS, &p.DeadlineAt, &p.NextPollAt, &p.CurrentTaskID, &p.ResolvedAt)
|
||||
if err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
base, err := workflowActivationOrigin(ctx, tx, g, row)
|
||||
if err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
node, _ := g.program.Step(row.view.Key)
|
||||
policy := node.Poll
|
||||
if policy == nil || p.MaxRounds != policy.MaxRounds || p.MaxAttemptsPerRound != policy.Task.Retry.MaxAttempts || p.IntervalMS != policy.IntervalMS || p.UsedRounds < 1 || !p.DeadlineAt.Equal(minimumTime(g.view.DeadlineAt, activated.Add(time.Duration(policy.TimeoutMS)*time.Millisecond))) {
|
||||
if policy == nil || p.MaxRounds != policy.MaxRounds || p.MaxAttemptsPerRound != policy.Task.Retry.MaxAttempts || p.IntervalMS != policy.IntervalMS || p.UsedRounds < 1 || p.UsedRounds > p.MaxRounds || p.FirstRound < 1 || p.FirstRound > p.UsedRounds || p.FirstRoundAttemptsUsed >= p.MaxAttemptsPerRound || !p.DeadlineAt.Equal(minimumTime(g.view.DeadlineAt, base.started.Add(time.Duration(policy.TimeoutMS)*time.Millisecond))) {
|
||||
return p, "", errors.New("workflow poll policy mismatch")
|
||||
}
|
||||
if p.ResolvedAt != nil {
|
||||
return p, "", errors.New("unsettled poll step has resolved budget")
|
||||
if base.sequence == 1 && (p.FirstRound != 1 || p.FirstRoundAttemptsUsed != 0) {
|
||||
return p, "", errors.New("initial poll recovery budget invalid")
|
||||
}
|
||||
if base.previous != nil {
|
||||
var used uint32
|
||||
var deadline time.Time
|
||||
var settled *time.Time
|
||||
if err := tx.QueryRowContext(ctx, `SELECT used_rounds,deadline_at,resolved_at FROM weave_poll_states WHERE namespace_id=? AND workflow_id=? AND activation_id=?`, g.view.NamespaceID, g.view.ID, base.previous).Scan(&used, &deadline, &settled); err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
if used != p.FirstRound || !deadline.Equal(p.DeadlineAt) || settled == nil {
|
||||
return p, "", errors.New("poll recovery parent budget invalid")
|
||||
}
|
||||
if _, err := readWorkflowPollRoundHistory(ctx, tx, g, row, p, p.FirstRound); err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
}
|
||||
var count, minRound, maxRound uint32
|
||||
if err := tx.QueryRowContext(ctx, "SELECT COUNT(*),COALESCE(MIN(`round`),0),COALESCE(MAX(`round`),0) FROM weave_poll_rounds WHERE namespace_id=? AND workflow_id=? AND activation_id=?", g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&count, &minRound, &maxRound); err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
if count != p.UsedRounds || minRound != 1 || maxRound != p.UsedRounds {
|
||||
if count != p.UsedRounds-p.FirstRound+1 || minRound != p.FirstRound || maxRound != p.UsedRounds {
|
||||
return p, "", errors.New("workflow poll rounds incomplete")
|
||||
}
|
||||
var lastTask core.ID
|
||||
if err := tx.QueryRowContext(ctx, "SELECT task_id FROM weave_poll_rounds WHERE namespace_id=? AND workflow_id=? AND activation_id=? AND `round`=?", g.view.NamespaceID, g.view.ID, row.view.ActivationID, p.UsedRounds).Scan(&lastTask); err != nil {
|
||||
return p, "", err
|
||||
}
|
||||
if p.CurrentTaskID != nil && (*p.CurrentTaskID != lastTask || p.NextPollAt != nil) || p.ResolvedAt != nil && (p.CurrentTaskID != nil || p.NextPollAt != nil) {
|
||||
return p, "", errors.New("poll budget execution pointers invalid")
|
||||
}
|
||||
return p, lastTask, nil
|
||||
}
|
||||
|
||||
func (r *WorkflowRuntime) advancePoll(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, c workflowCandidate, activated time.Time, allow bool, now time.Time) (bool, error) {
|
||||
p, lastTask, err := readWorkflowPollState(ctx, tx, g, row, activated)
|
||||
func (r *WorkflowRuntime) advancePoll(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, c workflowCandidate, allow bool, now time.Time) (bool, error) {
|
||||
p, lastTask, err := readWorkflowPollState(ctx, tx, g, row)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -196,11 +226,17 @@ func validatePollTaskOwner(ctx context.Context, tx *sql.Tx, g *workflowGraph, ro
|
||||
|
||||
// pollTaskInput 复用首轮已经解析并冻结的输入、业务键及互斥键;不重算依赖可能已改变的表达式。
|
||||
func pollTaskInput(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, spec definition.Task) (service.SubmitInput, error) {
|
||||
var first core.ID
|
||||
if err := tx.QueryRowContext(ctx, "SELECT task_id FROM weave_poll_rounds WHERE namespace_id=? AND workflow_id=? AND activation_id=? AND `round`=1", g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&first); err != nil {
|
||||
origin, err := workflowActivationOrigin(ctx, tx, g, row)
|
||||
if err != nil {
|
||||
return service.SubmitInput{}, err
|
||||
}
|
||||
if err := validatePollTaskOwner(ctx, tx, g, row, first); err != nil {
|
||||
originRow := *row
|
||||
originRow.view.ActivationID = &origin.id
|
||||
var first core.ID
|
||||
if err := tx.QueryRowContext(ctx, "SELECT task_id FROM weave_poll_rounds WHERE namespace_id=? AND workflow_id=? AND activation_id=? AND `round`=1", g.view.NamespaceID, g.view.ID, origin.id).Scan(&first); err != nil {
|
||||
return service.SubmitInput{}, err
|
||||
}
|
||||
if err := validatePollTaskOwner(ctx, tx, g, &originRow, first); err != nil {
|
||||
return service.SubmitInput{}, err
|
||||
}
|
||||
base, err := readTask(ctx, tx, g.view.NamespaceID, first, false)
|
||||
|
||||
Reference in New Issue
Block a user