feat: recover poll activations within original round budgets

This commit is contained in:
2026-09-18 05:40:08 +08:00
parent 1d1a441167
commit 56b9a2ccfe
32 changed files with 1051 additions and 65 deletions

View File

@@ -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)