feat: add bounded workflow decisions and durable result inbox
This commit is contained in:
352
weave/engine/plan.go
Normal file
352
weave/engine/plan.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
|
||||
"git.yuntuo.hk/k2safe/orbit/weave/definition"
|
||||
weave "git.yuntuo.hk/k2safe/orbit/weave/model"
|
||||
)
|
||||
|
||||
type planner struct {
|
||||
program *Program
|
||||
states map[string]State
|
||||
decision Decision
|
||||
limit int
|
||||
allowNew bool
|
||||
verifying int
|
||||
visited map[string]bool
|
||||
}
|
||||
|
||||
// Plan 只计算本轮最多limit个步骤状态变更(1至32),不修改快照或重新执行已完成业务。
|
||||
// 错误时丢弃全部本轮决策,存储适配层须保留原快照并记录核查原因。
|
||||
func (p *Program) Plan(snapshot Snapshot, limit int) (Decision, error) {
|
||||
if limit < 1 || limit > 32 || snapshot.WorkflowID.Validate() != nil || !validBusinessKey(snapshot.BusinessKey) {
|
||||
return Decision{}, problem("", "INVALID_SNAPSHOT")
|
||||
}
|
||||
if err := definition.ValidateData(p.spec.InputType, snapshot.Input); err != nil {
|
||||
return Decision{}, problem("", "WORKFLOW_INPUT_INVALID")
|
||||
}
|
||||
if len(snapshot.Steps) > len(p.nodes) {
|
||||
return Decision{}, problem("", "INVALID_SNAPSHOT")
|
||||
}
|
||||
state := &planner{program: p, states: make(map[string]State, len(p.nodes)), visited: map[string]bool{}, limit: limit, allowNew: snapshot.AllowNew, decision: Decision{Changes: []Change{}}}
|
||||
for key, row := range snapshot.Steps {
|
||||
node, ok := p.nodes[key]
|
||||
if !ok {
|
||||
return Decision{}, problem(key, "UNKNOWN_STEP")
|
||||
}
|
||||
if row.Status == weave.StepFailed || row.Status == weave.StepNeedsReview || row.Status == weave.StepCancelled {
|
||||
return Decision{}, problem(key, "STEP_REQUIRES_REVIEW")
|
||||
}
|
||||
switch row.Status {
|
||||
case weave.StepBlocked, weave.StepSkipped:
|
||||
if len(row.Input) != 0 || len(row.Output) != 0 {
|
||||
return Decision{}, problem(key, "INVALID_STEP_STATE")
|
||||
}
|
||||
case weave.StepActive, weave.StepWaiting, weave.StepSucceeded:
|
||||
if err := definition.ValidateData(definition.Type{Kind: "JSON"}, row.Input); err != nil {
|
||||
return Decision{}, problem(key, "INVALID_STEP_INPUT")
|
||||
}
|
||||
if node.Kind == "TASK" || node.Kind == "POLL" {
|
||||
task := node.Task
|
||||
if node.Poll != nil {
|
||||
task = &node.Poll.Task
|
||||
}
|
||||
if null(row.Input) || definition.ValidateData(task.InputType, row.Input) != nil {
|
||||
return Decision{}, problem(key, "INVALID_STEP_INPUT")
|
||||
}
|
||||
}
|
||||
if row.Status == weave.StepSucceeded {
|
||||
if err := validateOutput(node, row.Output); err != nil {
|
||||
return Decision{}, problem(key, "STEP_OUTPUT_INVALID")
|
||||
}
|
||||
} else if len(row.Output) != 0 {
|
||||
return Decision{}, problem(key, "INVALID_STEP_STATE")
|
||||
}
|
||||
default:
|
||||
return Decision{}, problem(key, "INVALID_STEP_STATE")
|
||||
}
|
||||
state.states[key] = row
|
||||
}
|
||||
workflow, err := jsonwire.Marshal(map[string]string{"id": string(snapshot.WorkflowID), "business_key": snapshot.BusinessKey})
|
||||
if err != nil {
|
||||
return Decision{}, err
|
||||
}
|
||||
complete, output, err := state.block(p.spec.Body, values{"input": copyJSON(snapshot.Input), "workflow": workflow})
|
||||
if err != nil {
|
||||
return Decision{}, err
|
||||
}
|
||||
for key, row := range snapshot.Steps {
|
||||
if (row.Status == weave.StepActive || row.Status == weave.StepWaiting || row.Status == weave.StepSucceeded) && !state.visited[key] {
|
||||
return Decision{}, problem(key, "UNREACHABLE_EXECUTION")
|
||||
}
|
||||
}
|
||||
state.decision.Complete = complete
|
||||
state.decision.Output = copyJSON(output)
|
||||
return state.decision, nil
|
||||
}
|
||||
func null(raw []byte) bool { return bytes.Equal(bytes.TrimSpace(raw), []byte("null")) }
|
||||
func outputType(node definition.Step) definition.Type {
|
||||
switch node.Kind {
|
||||
case "TASK":
|
||||
return node.Task.ResultType
|
||||
case "POLL":
|
||||
return node.Poll.Task.ResultType
|
||||
case "SIGNAL":
|
||||
return node.Signal.PayloadType
|
||||
case "TIMER":
|
||||
return definition.Type{Kind: "NULL"}
|
||||
case "BRANCH":
|
||||
return node.Branch.Default.OutputType
|
||||
case "PARALLEL":
|
||||
t := definition.Type{Kind: "OBJECT", Fields: map[string]definition.Type{}}
|
||||
for _, b := range node.Parallel.Branches {
|
||||
t.Fields[b.Name] = b.Body.OutputType
|
||||
}
|
||||
return t
|
||||
}
|
||||
return definition.Type{}
|
||||
}
|
||||
func validateOutput(node definition.Step, raw json.RawMessage) error {
|
||||
if (node.Kind == "TASK" || node.Kind == "POLL") && null(raw) {
|
||||
return problem(node.Key, "STEP_OUTPUT_INVALID")
|
||||
}
|
||||
if node.Poll != nil {
|
||||
outcome, err := classifyPoll(*node.Poll, raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if outcome != PollSucceeded {
|
||||
return problem(node.Key, "POLL_RESULT_NOT_SUCCESS")
|
||||
}
|
||||
}
|
||||
return definition.ValidateData(outputType(node), raw)
|
||||
}
|
||||
func (s *planner) add(change Change) bool {
|
||||
if len(s.decision.Changes) >= s.limit {
|
||||
s.decision.More = true
|
||||
return false
|
||||
}
|
||||
s.decision.Changes = append(s.decision.Changes, change)
|
||||
s.states[change.Key] = change.State
|
||||
return true
|
||||
}
|
||||
func (s *planner) block(block definition.Block, parent values) (bool, json.RawMessage, error) {
|
||||
env := copyValues(parent)
|
||||
for _, node := range block.Steps {
|
||||
complete, output, err := s.step(node, env)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !complete {
|
||||
return false, nil, nil
|
||||
}
|
||||
env[node.Key] = output
|
||||
}
|
||||
output, err := resolve(block.Output, block.OutputType, env)
|
||||
if err != nil {
|
||||
return false, nil, problem("", "BLOCK_OUTPUT_INVALID")
|
||||
}
|
||||
return true, output, nil
|
||||
}
|
||||
func (s *planner) step(node definition.Step, env values) (bool, json.RawMessage, error) {
|
||||
s.visited[node.Key] = true
|
||||
row, ok := s.states[node.Key]
|
||||
if !ok {
|
||||
row.Status = weave.StepBlocked
|
||||
}
|
||||
completed := row.Status == weave.StepSucceeded
|
||||
if s.verifying > 0 && !completed {
|
||||
return false, nil, problem(node.Key, "INCOMPLETE_COMPOSITE_CHILD")
|
||||
}
|
||||
if completed {
|
||||
if node.Kind != "BRANCH" && node.Kind != "PARALLEL" {
|
||||
return true, copyJSON(row.Output), nil
|
||||
}
|
||||
s.verifying++
|
||||
defer func() { s.verifying-- }()
|
||||
}
|
||||
if row.Status == weave.StepSkipped {
|
||||
return false, nil, problem(node.Key, "SELECTED_STEP_WAS_SKIPPED")
|
||||
}
|
||||
if row.Status == weave.StepBlocked {
|
||||
if !s.allowNew || s.decision.More {
|
||||
return false, nil, nil
|
||||
}
|
||||
change := Change{Key: node.Key, Kind: node.Kind, Operation: "ACTIVATE", State: State{Status: weave.StepActive}}
|
||||
switch node.Kind {
|
||||
case "TASK", "POLL":
|
||||
task := node.Task
|
||||
if node.Poll != nil {
|
||||
task = &node.Poll.Task
|
||||
}
|
||||
prepared, err := prepareTask(*task, env)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "TASK_INPUT_INVALID")
|
||||
}
|
||||
change.State.Input = copyJSON(prepared.Payload)
|
||||
change.Task = &prepared
|
||||
if node.Poll != nil && node.Poll.FinalSignal != nil {
|
||||
wait, err := prepareSignal(*node.Poll.FinalSignal, "POLL", env)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "SIGNAL_CORRELATION_INVALID")
|
||||
}
|
||||
change.Wait = wait
|
||||
}
|
||||
case "SIGNAL":
|
||||
wait, err := prepareSignal(*node.Signal, "SIGNAL", env)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "SIGNAL_CORRELATION_INVALID")
|
||||
}
|
||||
change.Wait = wait
|
||||
change.State.Status = weave.StepWaiting
|
||||
change.State.Input, _ = jsonwire.Marshal(map[string]string{"name": wait.Name, "payload_version": wait.PayloadVersion, "correlation": wait.Correlation})
|
||||
case "TIMER":
|
||||
change.Wait = &WaitCommand{Kind: "TIMER", TimeoutMS: node.Timer.DelayMS}
|
||||
change.State.Status = weave.StepWaiting
|
||||
change.State.Input, _ = jsonwire.Marshal(map[string]uint64{"delay_ms": node.Timer.DelayMS})
|
||||
case "BRANCH":
|
||||
selected := -1
|
||||
for i, path := range node.Branch.Cases {
|
||||
matches, err := compare(path.When, env)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "BRANCH_CONDITION_INVALID")
|
||||
}
|
||||
if matches {
|
||||
selected = i
|
||||
break
|
||||
}
|
||||
}
|
||||
change.State.Input, _ = jsonwire.Marshal(map[string]int{"case": selected})
|
||||
case "PARALLEL":
|
||||
change.State.Input = json.RawMessage(`{}`)
|
||||
default:
|
||||
return false, nil, problem(node.Key, "UNSUPPORTED_NODE")
|
||||
}
|
||||
if !s.add(change) {
|
||||
return false, nil, nil
|
||||
}
|
||||
row = change.State
|
||||
}
|
||||
if node.Kind != "BRANCH" && node.Kind != "PARALLEL" {
|
||||
return false, nil, nil
|
||||
}
|
||||
var output json.RawMessage
|
||||
switch node.Kind {
|
||||
case "BRANCH":
|
||||
contract := definition.Type{Kind: "OBJECT", Fields: map[string]definition.Type{"case": {Kind: "NUMBER"}}}
|
||||
var selection struct {
|
||||
Case int `json:"case"`
|
||||
}
|
||||
if definition.ValidateData(contract, row.Input) != nil || json.Unmarshal(row.Input, &selection) != nil || selection.Case < -1 || selection.Case >= len(node.Branch.Cases) {
|
||||
return false, nil, problem(node.Key, "BRANCH_DECISION_INVALID")
|
||||
}
|
||||
// 已冻结的选择不重算;未选择路径分批标记SKIPPED,不创建业务激活。
|
||||
for i, path := range node.Branch.Cases {
|
||||
if i != selection.Case {
|
||||
if err := s.skip(path.Body); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
body := *node.Branch.Default
|
||||
if selection.Case >= 0 {
|
||||
if err := s.skip(*node.Branch.Default); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
body = node.Branch.Cases[selection.Case].Body
|
||||
}
|
||||
// 预算只阻止新增变更;已有选中路径仍须遍历,避免遗漏并行分支的在途工作。
|
||||
done, raw, err := s.block(body, env)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !done {
|
||||
return false, nil, nil
|
||||
}
|
||||
output = raw
|
||||
case "PARALLEL":
|
||||
if definition.ValidateData(definition.Type{Kind: "OBJECT"}, row.Input) != nil {
|
||||
return false, nil, problem(node.Key, "INVALID_PARALLEL_STATE")
|
||||
}
|
||||
result := make(map[string]json.RawMessage, len(node.Parallel.Branches))
|
||||
all := true
|
||||
for _, branch := range node.Parallel.Branches {
|
||||
done, raw, err := s.block(branch.Body, env)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !done {
|
||||
all = false
|
||||
} else {
|
||||
result[branch.Name] = raw
|
||||
}
|
||||
}
|
||||
if !all {
|
||||
return false, nil, nil
|
||||
}
|
||||
var err error
|
||||
output, err = jsonwire.Marshal(result)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "STEP_OUTPUT_INVALID")
|
||||
}
|
||||
}
|
||||
if validateOutput(node, output) != nil {
|
||||
return false, nil, problem(node.Key, "STEP_OUTPUT_INVALID")
|
||||
}
|
||||
if completed {
|
||||
original, err := jsonvalue.Canonical(row.Output)
|
||||
if err != nil {
|
||||
return false, nil, problem(node.Key, "STEP_OUTPUT_INVALID")
|
||||
}
|
||||
current, err := jsonvalue.Canonical(output)
|
||||
if err != nil || !bytes.Equal(original, current) {
|
||||
return false, nil, problem(node.Key, "COMPOSITE_OUTPUT_MISMATCH")
|
||||
}
|
||||
return true, copyJSON(row.Output), nil
|
||||
}
|
||||
row.Status = weave.StepSucceeded
|
||||
row.Output = copyJSON(output)
|
||||
if !s.add(Change{Key: node.Key, Kind: node.Kind, Operation: "COMPLETE", State: row}) {
|
||||
return false, nil, nil
|
||||
}
|
||||
return true, output, nil
|
||||
}
|
||||
func (s *planner) skip(block definition.Block) error {
|
||||
for _, node := range block.Steps {
|
||||
s.visited[node.Key] = true
|
||||
row, ok := s.states[node.Key]
|
||||
if ok && row.Status != weave.StepBlocked && row.Status != weave.StepSkipped {
|
||||
return problem(node.Key, "UNSELECTED_STEP_HAS_EXECUTION")
|
||||
}
|
||||
if !ok || row.Status == weave.StepBlocked {
|
||||
if s.verifying > 0 {
|
||||
return problem(node.Key, "INCOMPLETE_COMPOSITE_CHILD")
|
||||
}
|
||||
if !s.add(Change{Key: node.Key, Kind: node.Kind, Operation: "SKIP", State: State{Status: weave.StepSkipped}}) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if node.Branch != nil {
|
||||
for _, c := range node.Branch.Cases {
|
||||
if err := s.skip(c.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.skip(*node.Branch.Default); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if node.Parallel != nil {
|
||||
for _, b := range node.Parallel.Branches {
|
||||
if err := s.skip(b.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user