package mysql import ( "context" "database/sql" "errors" "time" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/weave/definition" "git.yuntuo.hk/k2safe/orbit/weave/engine" "git.yuntuo.hk/k2safe/orbit/weave/instance" weave "git.yuntuo.hk/k2safe/orbit/weave/model" ) func createWorkflowSteps(ctx context.Context, tx *sql.Tx, g *workflowGraph, now time.Time) error { create := func(key, kind string) error { id, err := core.NewID() if err != nil { return err } if _, err := tx.ExecContext(ctx, "INSERT INTO weave_steps(namespace_id,id,workflow_id,`key`,kind,created_at,updated_at) VALUES (?,?,?,?,?,?,?)", g.view.NamespaceID, id, g.view.ID, key, kind, now, now); err != nil { return err } g.steps[key] = &workflowStep{view: instance.StepView{ID: id, Key: key, Kind: weave.NodeKind(kind), State: weave.StepBlocked, Revision: 1}} return nil } var visit func(definition.Block) error visit = func(b definition.Block) error { for _, s := range b.Steps { if err := create(s.Key, s.Kind); err != nil { return err } if s.Branch != nil { for _, c := range s.Branch.Cases { if err := visit(c.Body); err != nil { return err } } if err := visit(*s.Branch.Default); err != nil { return err } } if s.Parallel != nil { for _, branch := range s.Parallel.Branches { if err := visit(branch.Body); err != nil { return err } } } } return nil } if err := visit(g.version.Specification.Body); err != nil { return err } if err := create(workflowRootKey, "JOIN"); err != nil { return err } root := g.steps[workflowRootKey] activation, err := core.NewID() if err != nil { return err } if _, err := tx.ExecContext(ctx, `INSERT INTO weave_activations(namespace_id,id,workflow_id,step_id,sequence,state,input,created_at,updated_at) VALUES (?,?,?,?,1,'ACTIVE',?,?,?)`, g.view.NamespaceID, activation, g.view.ID, root.view.ID, string(g.view.Input), now, now); err != nil { return err } r, err := tx.ExecContext(ctx, `UPDATE weave_steps SET state='ACTIVE',current_activation_id=? WHERE namespace_id=? AND id=?`, activation, g.view.NamespaceID, root.view.ID) if err := exactlyOne(r, err); err != nil { return err } root.view.State, root.view.ActivationID, root.input = weave.StepActive, &activation, g.view.Input return nil } // applyWorkflowDecision 调用方必须已按应用→排序队列→实例持锁,结果和待续状态一并提交。 // 初始受理的实例行尚未对其他事务可见,同样由应用锁保护,不在函数内重排父级锁。 func applyWorkflowDecision(ctx context.Context, tx *sql.Tx, g *workflowGraph, d engine.Decision, now time.Time) error { if len(d.Changes) > 32 { return errors.New("workflow change budget exceeded") } for _, c := range d.Changes { row, ok := g.steps[c.Key] if !ok || string(row.view.Kind) != c.Kind { return errors.New("workflow decision step mismatch") } switch c.Operation { case "ACTIVATE": if row.view.State != weave.StepBlocked || row.view.ActivationID != nil { return errors.New("workflow step already activated") } if err := activateWorkflowStep(ctx, tx, g, row, c, now); err != nil { return err } case "SKIP": if row.view.State != weave.StepBlocked || row.view.ActivationID != nil || c.State.Status != weave.StepSkipped { return errors.New("workflow skip state invalid") } case "COMPLETE": if row.view.ActivationID == nil || (row.view.State != weave.StepActive && row.view.State != weave.StepWaiting) || c.State.Status != weave.StepSucceeded { return errors.New("workflow completion state invalid") } r, err := tx.ExecContext(ctx, `UPDATE weave_activations SET state='SUCCEEDED',output=?,completed_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND workflow_id=? AND step_id=? AND state IN ('ACTIVE','WAITING')`, string(c.State.Output), now, now, g.view.NamespaceID, row.view.ActivationID, g.view.ID, row.view.ID) if err := exactlyOne(r, err); err != nil { return err } default: return errors.New("unsupported workflow decision") } r, err := tx.ExecContext(ctx, `UPDATE weave_steps SET state=?,current_activation_id=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND workflow_id=? AND revision=?`, c.State.Status, row.view.ActivationID, now, g.view.NamespaceID, row.view.ID, g.view.ID, row.view.Revision) if err := exactlyOne(r, err); err != nil { return err } row.view.State, row.input, row.output = c.State.Status, c.State.Input, c.State.Output row.view.Revision++ if err := emitEvent(ctx, tx, g.view.NamespaceID, row.view.ID, row.view.Revision, "workflow_step", "workflow.step.changed", map[string]any{"workflow_id": g.view.ID, "key": c.Key, "state": c.State.Status, "activation_id": row.view.ActivationID}); err != nil { return err } } root := g.steps[workflowRootKey] if root == nil || root.view.ActivationID == nil { return errors.New("workflow root missing") } state := weave.InstanceRunning var completed *time.Time var output any rootState := weave.StepActive if d.Complete { if d.More { return errors.New("workflow complete decision has pending work") } for key, row := range g.steps { if key != workflowRootKey && row.view.State != weave.StepSucceeded && row.view.State != weave.StepSkipped { return errors.New("workflow completion has unfinished steps") } } state, rootState, completed, output = weave.InstanceSucceeded, weave.StepSucceeded, &now, string(d.Output) } else if !d.More { waiting, active := false, false for key, row := range g.steps { if key == workflowRootKey { continue } if row.view.State == weave.StepWaiting { waiting = true } if row.view.State == weave.StepActive && (row.view.Kind == weave.NodeTask || row.view.Kind == weave.NodePoll) { active = true } } if waiting && !active { state = weave.InstanceWaiting } } r, err := tx.ExecContext(ctx, `UPDATE weave_activations SET state=?,output=?,completed_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND workflow_id=? AND step_id=? AND state='ACTIVE'`, rootState, output, completed, now, g.view.NamespaceID, root.view.ActivationID, g.view.ID, root.view.ID) if err := exactlyOne(r, err); err != nil { return err } r, err = tx.ExecContext(ctx, `UPDATE weave_steps SET state=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND revision=?`, rootState, now, g.view.NamespaceID, root.view.ID, root.view.Revision) if err := exactlyOne(r, err); err != nil { return err } root.view.Revision++ root.view.State = rootState // 包括首批决策在内,每次持久化推进递增实例版本;不能以未改变字段的affected_rows判断存在性。 r, err = tx.ExecContext(ctx, `UPDATE weave_instances SET state=?,output=?,completed_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND revision=?`, state, output, completed, now, g.view.NamespaceID, g.view.ID, g.view.Revision) if err := exactlyOne(r, err); err != nil { return err } if d.Complete { if _, err := tx.ExecContext(ctx, `UPDATE pulse_execution_gates SET paused=TRUE,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=?`, now, g.view.NamespaceID, g.gate); err != nil { return err } } if d.More { id, err := core.NewID() if err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO weave_pending_advances(namespace_id,id,workflow_id,step_id,expected_step_revision,checkpoint,available_at,created_at,updated_at) VALUES (?,?,?,?,?,'{"schema_version":1,"kind":"ROOT"}',?,?,?) ON DUPLICATE KEY UPDATE expected_step_revision=VALUES(expected_step_revision),checkpoint=VALUES(checkpoint),available_at=VALUES(available_at),revision=revision+1,updated_at=VALUES(updated_at)`, g.view.NamespaceID, id, g.view.ID, root.view.ID, root.view.Revision, now, now, now) if err != nil { return err } } else { if _, err := tx.ExecContext(ctx, `DELETE FROM weave_pending_advances WHERE namespace_id=? AND workflow_id=? AND step_id=?`, g.view.NamespaceID, g.view.ID, root.view.ID); err != nil { return err } } g.view.State = state g.view.Revision++ g.view.CompletedAt = completed g.view.Output = d.Output return nil } func activateWorkflowStep(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, c engine.Change, now time.Time) error { id, err := core.NewID() if err != nil { return err } var taskID *core.ID if c.Task != nil { v, err := core.NewID() if err != nil { return err } taskID = &v } var direct *core.ID if c.Kind == "TASK" { direct = taskID } if _, err := tx.ExecContext(ctx, `INSERT INTO weave_activations(namespace_id,id,workflow_id,step_id,sequence,state,task_id,input,created_at,updated_at) VALUES (?,?,?,?,1,?,?,?,?,?)`, g.view.NamespaceID, id, g.view.ID, row.view.ID, c.State.Status, direct, string(c.State.Input), now, now); err != nil { return err } row.view.ActivationID = &id row.view.TaskID = direct deadline := g.view.DeadlineAt if c.Kind == "POLL" { node, ok := g.program.Step(c.Key) if !ok || node.Poll == nil || taskID == nil { return errors.New("workflow poll decision incomplete") } p := node.Poll deadline = minimumTime(deadline, now.Add(time.Duration(p.TimeoutMS)*time.Millisecond)) pollID, err := core.NewID() if err != nil { return err } if _, err := tx.ExecContext(ctx, `INSERT INTO weave_poll_states(namespace_id,id,workflow_id,activation_id,max_rounds,used_rounds,max_attempts_per_round,interval_ms,deadline_at,current_task_id,created_at,updated_at) VALUES (?,?,?,?,?,1,?,?,?,?,?,?)`, g.view.NamespaceID, pollID, g.view.ID, id, p.MaxRounds, p.Task.Retry.MaxAttempts, p.IntervalMS, deadline, taskID, now, now); err != nil { return err } roundID, err := core.NewID() if err != nil { return err } if _, err := tx.ExecContext(ctx, "INSERT INTO weave_poll_rounds(namespace_id,id,workflow_id,activation_id,`round`,task_id,created_at,updated_at) VALUES (?,?,?,?,1,?,?,?)", g.view.NamespaceID, roundID, g.view.ID, id, taskID, now, now); err != nil { return err } } if c.Wait != nil { waitID, err := core.NewID() if err != nil { return err } until := minimumTime(deadline, now.Add(time.Duration(c.Wait.TimeoutMS)*time.Millisecond)) var name, version any if c.Wait.Kind != "TIMER" { name, version = c.Wait.Name, c.Wait.PayloadVersion } if _, err := tx.ExecContext(ctx, `INSERT INTO weave_waits(namespace_id,id,workflow_id,activation_id,kind,correlation_key,deadline_at,signal_name,payload_version,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, g.view.NamespaceID, waitID, g.view.ID, id, c.Wait.Kind, []byte(c.Wait.Correlation), until, name, version, now, now); err != nil { return err } } if c.Task != nil { if taskID == nil { return errors.New("workflow task identity missing") } task, err := insertAcceptedTaskUntil(ctx, tx, g.view.NamespaceID, *taskID, *c.Task, nil, &deadline) if err != nil { return err } r, err := tx.ExecContext(ctx, `UPDATE pulse_tasks SET owner_kind=?,owner_id=?,gate_id=? WHERE namespace_id=? AND id=? AND owner_kind IS NULL`, engine.TaskOwnerKind, id, g.gate, g.view.NamespaceID, *taskID) if err := exactlyOne(r, err); err != nil { return err } if err := emitEvent(ctx, tx, g.view.NamespaceID, *taskID, task.Revision, "task", "task.accepted", map[string]any{"task_id": task.ID, "state": task.State, "workflow_id": g.view.ID, "activation_id": id}); err != nil { return err } } return nil }