diff --git a/pkg/saga/statemachine/constant/constant.go b/pkg/saga/statemachine/constant/constant.go new file mode 100644 index 00000000..f9e1eeba --- /dev/null +++ b/pkg/saga/statemachine/constant/constant.go @@ -0,0 +1,28 @@ +package constant + +const ( + VarNameProcessType string = "_ProcessType_" + VarNameOperationName string = "_operation_name_" + OperationNameStart string = "start" + VarNameAsyncCallback string = "_async_callback_" + VarNameStateMachineInst string = "_current_statemachine_instance_" + VarNameStateMachine string = "_current_statemachine_" + VarNameStateMachineEngine string = "_current_statemachine_engine_" + VarNameStateMachineConfig string = "_statemachine_config_" + VarNameStateMachineContext string = "context" + VarNameIsAsyncExecution string = "_is_async_execution_" + VarNameStateInst string = "_current_state_instance_" + SeqEntityStateMachineInst string = "STATE_MACHINE_INST" + VarNameBusinesskey string = "_business_key_" + VarNameParentId string = "_parent_id_" + StateTypeServiceTask string = "ServiceTask" + StateTypeChoice string = "Choice" + StateTypeSubStateMachine string = "SubStateMachine" + CompensateSubMachine string = "CompensateSubMachine" + StateTypeSucceed string = "Succeed" + StateTypeFail string = "Fail" + StateTypeCompensationTrigger string = "CompensationTrigger" + StateTypeScriptTask string = "ScriptTask" + CompensateSubMachineStateNamePrefix string = "_compensate_sub_machine_state_" + DefaultScriptType string = "groovy" +) diff --git a/pkg/saga/statemachine/constant/contant.go b/pkg/saga/statemachine/constant/contant.go deleted file mode 100644 index 39cb2924..00000000 --- a/pkg/saga/statemachine/constant/contant.go +++ /dev/null @@ -1,20 +0,0 @@ -package constant - -const ( - VarNameProcessType string = "_ProcessType_" - VarNameOperationName string = "_operation_name_" - OperationNameStart string = "start" - VarNameAsyncCallback string = "_async_callback_" - VarNameStateMachineInst string = "_current_statemachine_instance_" - VarNameStateMachine string = "_current_statemachine_" - VarNameStateMachineEngine string = "_current_statemachine_engine_" - VarNameStateMachineConfig string = "_statemachine_config_" - VarNameStateMachineContext string = "context" - VarNameIsAsyncExecution string = "_is_async_execution_" - VarNameStateInst string = "_current_state_instance_" - SeqEntityStateMachineInst string = "STATE_MACHINE_INST" - VarNameBusinesskey string = "_business_key_" - VarNameParentId string = "_parent_id_" - StateTypeServiceTask string = "ServiceTask" - StateTypeChoice string = "Choice" -) diff --git a/pkg/saga/statemachine/statelang/parser/choice_state_json_parser.go b/pkg/saga/statemachine/statelang/parser/choice_state_json_parser.go index 04729c57..9b4d51cd 100644 --- a/pkg/saga/statemachine/statelang/parser/choice_state_json_parser.go +++ b/pkg/saga/statemachine/statelang/parser/choice_state_json_parser.go @@ -3,21 +3,23 @@ package parser import ( "fmt" "github.com/pkg/errors" - "github.com/seata/seata-go/pkg/saga/statemachine/engine" + "github.com/seata/seata-go/pkg/saga/statemachine/constant" "github.com/seata/seata-go/pkg/saga/statemachine/statelang" "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" ) type ChoiceStateParser struct { - BaseStateParser + *BaseStateParser } func NewChoiceStateParser() *ChoiceStateParser { - return &ChoiceStateParser{} + return &ChoiceStateParser{ + &BaseStateParser{}, + } } func (c ChoiceStateParser) StateType() string { - return engine.StateTypeChoice + return constant.StateTypeChoice } func (c ChoiceStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { diff --git a/pkg/saga/statemachine/statelang/parser/compensation_trigger_state_parser.go b/pkg/saga/statemachine/statelang/parser/compensation_trigger_state_parser.go new file mode 100644 index 00000000..d41a6d29 --- /dev/null +++ b/pkg/saga/statemachine/statelang/parser/compensation_trigger_state_parser.go @@ -0,0 +1,31 @@ +package parser + +import ( + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" +) + +type CompensationTriggerStateParser struct { + *BaseStateParser +} + +func NewCompensationTriggerStateParser() *CompensationTriggerStateParser { + return &CompensationTriggerStateParser{ + &BaseStateParser{}, + } +} + +func (c CompensationTriggerStateParser) StateType() string { + return constant.StateTypeCompensationTrigger +} + +func (c CompensationTriggerStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + compensateSubStateMachineStateImpl := state.NewCompensationTriggerStateImpl() + err := c.ParseBaseAttributes(stateName, compensateSubStateMachineStateImpl, stateMap) + if err != nil { + return nil, err + } + + return compensateSubStateMachineStateImpl, nil +} diff --git a/pkg/saga/statemachine/statelang/parser/end_state_parser.go b/pkg/saga/statemachine/statelang/parser/end_state_parser.go new file mode 100644 index 00000000..823ee8df --- /dev/null +++ b/pkg/saga/statemachine/statelang/parser/end_state_parser.go @@ -0,0 +1,66 @@ +package parser + +import ( + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" +) + +type SucceedEndStateParser struct { + *BaseStateParser +} + +func NewSucceedEndStateParser() *SucceedEndStateParser { + return &SucceedEndStateParser{ + &BaseStateParser{}, + } +} + +func (s SucceedEndStateParser) StateType() string { + return constant.StateTypeSucceed +} + +func (s SucceedEndStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + succeedEndStateImpl := state.NewSucceedEndStateImpl() + err := s.ParseBaseAttributes(stateName, succeedEndStateImpl, stateMap) + if err != nil { + return nil, err + } + + return succeedEndStateImpl, nil +} + +type FailEndStateParser struct { + *BaseStateParser +} + +func NewFailEndStateParser() *FailEndStateParser { + return &FailEndStateParser{ + &BaseStateParser{}, + } +} + +func (f FailEndStateParser) StateType() string { + return constant.StateTypeFail +} + +func (f FailEndStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + failEndStateImpl := state.NewFailEndStateImpl() + err := f.ParseBaseAttributes(stateName, failEndStateImpl, stateMap) + if err != nil { + return nil, err + } + + errorCode, err := f.GetStringOrDefault(stateName, stateMap, "ErrorCode", "") + if err != nil { + return nil, err + } + failEndStateImpl.SetErrorCode(errorCode) + + message, err := f.GetStringOrDefault(stateName, stateMap, "Message", "") + if err != nil { + return nil, err + } + failEndStateImpl.SetMessage(message) + return failEndStateImpl, nil +} diff --git a/pkg/saga/statemachine/statelang/parser/statemachine_json_parser.go b/pkg/saga/statemachine/statelang/parser/statemachine_json_parser.go index c0977034..4bcfdcbc 100644 --- a/pkg/saga/statemachine/statelang/parser/statemachine_json_parser.go +++ b/pkg/saga/statemachine/statelang/parser/statemachine_json_parser.go @@ -3,14 +3,19 @@ package parser import ( "encoding/json" "github.com/pkg/errors" + "github.com/seata/seata-go/pkg/saga/statemachine/constant" "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" ) type JSONStateMachineParser struct { + *BaseStateParser } func NewJSONStateMachineParser() *JSONStateMachineParser { - return &JSONStateMachineParser{} + return &JSONStateMachineParser{ + &BaseStateParser{}, + } } func (stateMachineParser JSONStateMachineParser) GetType() string { @@ -77,15 +82,40 @@ func (stateMachineParser JSONStateMachineParser) Parse(content string) (statelan stateMachine.States()[stateName] = state } - //TODO setCompensateState - //for stateName, state := range stateMachine.GetStates() { - // - //} - // + for _, stateValue := range stateMachine.States() { + if stateMachineParser.isTaskState(stateValue.Type()) { + stateMachineParser.setForCompensation(stateValue, stateMachine) + } + } return stateMachine, nil } +func (stateMachineParser JSONStateMachineParser) setForCompensation(stateValue statelang.State, stateMachine *statelang.StateMachineImpl) { + switch stateValue.Type() { + case stateValue.Type(): + serviceTaskStateImpl, ok := stateValue.(*state.ServiceTaskStateImpl) + if ok { + if serviceTaskStateImpl.CompensateState() != "" { + compState := stateMachine.States()[serviceTaskStateImpl.CompensateState()] + if stateMachineParser.isTaskState(compState.Type()) { + compStateImpl, ok := compState.(state.ServiceTaskStateImpl) + if ok { + compStateImpl.SetForCompensation(true) + } + } + } + } + } +} + +func (stateMachineParser JSONStateMachineParser) isTaskState(stateType string) bool { + if stateType == constant.StateTypeServiceTask { + return true + } + return false +} + type StateMachineJsonObject struct { Name string `json:"Name"` Comment string `json:"Comment"` diff --git a/pkg/saga/statemachine/statelang/parser/statemachine_json_parser_test.go b/pkg/saga/statemachine/statelang/parser/statemachine_json_parser_test.go index c6d00b75..0028309a 100644 --- a/pkg/saga/statemachine/statelang/parser/statemachine_json_parser_test.go +++ b/pkg/saga/statemachine/statelang/parser/statemachine_json_parser_test.go @@ -1,12 +1,44 @@ package parser import ( + "os" "testing" ) func TestParseChoice(t *testing.T) { - var content = "{\n \"Name\":\"ChoiceTest\",\n \"Comment\":\"ChoiceTest\",\n \"StartState\":\"ChoiceState\",\n \"Version\":\"0.0.1\",\n \"States\":{\n \"ChoiceState\":{\n \"Type\":\"Choice\",\n \"Choices\":[\n {\n \"Expression\":\"[a] == 1\",\n \"Next\":\"SecondState\"\n },\n {\n \"Expression\":\"[a] == 2\",\n \"Next\":\"ThirdState\"\n }\n ],\n \"Default\":\"Fail\"\n }\n }\n}" - _, err := NewJSONStateMachineParser().Parse(content) + filePath := "../../../../../testdata/saga/statelang/simple_statelang_with_choice.json" + fileContent, err := os.ReadFile(filePath) + if err != nil { + t.Error("parse fail: " + err.Error()) + return + } + _, err = NewJSONStateMachineParser().Parse(string(fileContent)) + if err != nil { + t.Error("parse fail: " + err.Error()) + } +} + +func TestParseServiceTaskForSimpleStateMachine(t *testing.T) { + filePath := "../../../../../testdata/saga/statelang/simple_statemachine.json" + fileContent, err := os.ReadFile(filePath) + if err != nil { + t.Error("parse fail: " + err.Error()) + return + } + _, err = NewJSONStateMachineParser().Parse(string(fileContent)) + if err != nil { + t.Error("parse fail: " + err.Error()) + } +} + +func TestParseServiceTaskForNewDesigner(t *testing.T) { + filePath := "../../../../../testdata/saga/statelang/state_machine_new_designer.json" + fileContent, err := os.ReadFile(filePath) + if err != nil { + t.Error("parse fail: " + err.Error()) + return + } + _, err = NewJSONStateMachineParser().Parse(string(fileContent)) if err != nil { t.Error("parse fail: " + err.Error()) } diff --git a/pkg/saga/statemachine/statelang/parser/statemachine_parser.go b/pkg/saga/statemachine/statelang/parser/statemachine_parser.go index 99ee95e1..a74c79ff 100644 --- a/pkg/saga/statemachine/statelang/parser/statemachine_parser.go +++ b/pkg/saga/statemachine/statelang/parser/statemachine_parser.go @@ -3,6 +3,8 @@ package parser import ( "github.com/pkg/errors" "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "strconv" + "strings" "sync" ) @@ -19,20 +21,23 @@ type StateParser interface { type BaseStateParser struct { } +func NewBaseStateParser() *BaseStateParser { + return &BaseStateParser{} +} + func (b BaseStateParser) ParseBaseAttributes(stateName string, state statelang.State, stateMap map[string]interface{}) error { state.SetName(stateName) - comment, err := b.GetString(stateName, stateMap, "Comment") + comment, err := b.GetStringOrDefault(stateName, stateMap, "Comment", "") if err != nil { return err } state.SetComment(comment) - next, err := b.GetString(stateName, stateMap, "Next") + next, err := b.GetStringOrDefault(stateName, stateMap, "Next", "") if err != nil { return err } - state.SetNext(next) return nil } @@ -52,9 +57,21 @@ func (b BaseStateParser) GetString(stateName string, stateMap map[string]interfa return valueAsString, nil } -func (b BaseStateParser) GetSlice(stateName string, stateMap map[string]interface{}, key string) ([]interface{}, error) { +func (b BaseStateParser) GetStringOrDefault(stateName string, stateMap map[string]interface{}, key string, defaultValue string) (string, error) { value := stateMap[key] + if value == nil { + return defaultValue, nil + } + valueAsString, ok := value.(string) + if !ok { + return defaultValue, errors.New("State [" + stateName + "] " + key + " illegal, required string") + } + return valueAsString, nil +} + +func (b BaseStateParser) GetSlice(stateName string, stateMap map[string]interface{}, key string) ([]interface{}, error) { + value := stateMap[key] if value == nil { var result []interface{} return result, errors.New("State [" + stateName + "] " + key + " not exist") @@ -62,12 +79,103 @@ func (b BaseStateParser) GetSlice(stateName string, stateMap map[string]interfac valueAsSlice, ok := value.([]interface{}) if !ok { - var result []interface{} - return result, errors.New("State [" + stateName + "] " + key + " illegal, required slice") + var slice []interface{} + return slice, errors.New("State [" + stateName + "] " + key + " illegal, required []interface{}") } return valueAsSlice, nil } +func (b BaseStateParser) GetSliceOrDefault(stateName string, stateMap map[string]interface{}, key string, defaultValue []interface{}) ([]interface{}, error) { + value := stateMap[key] + + if value == nil { + return defaultValue, nil + } + + valueAsSlice, ok := value.([]interface{}) + if !ok { + return defaultValue, errors.New("State [" + stateName + "] " + key + " illegal, required []interface{}") + } + return valueAsSlice, nil +} + +func (b BaseStateParser) GetMapOrDefault(stateMap map[string]interface{}, key string, defaultValue map[string]interface{}) (map[string]interface{}, error) { + value := stateMap[key] + + if value == nil { + return defaultValue, nil + } + + valueAsMap, ok := value.(map[string]interface{}) + if !ok { + return defaultValue, nil + } + return valueAsMap, nil +} + +func (b BaseStateParser) GetBool(stateName string, stateMap map[string]interface{}, key string) (bool, error) { + value := stateMap[key] + + if value == nil { + return false, errors.New("State [" + stateName + "] " + key + " not exist") + } + + valueAsBool, ok := value.(bool) + if !ok { + return false, errors.New("State [" + stateName + "] " + key + " illegal, required bool") + } + return valueAsBool, nil +} + +func (b BaseStateParser) GetBoolOrDefault(stateName string, stateMap map[string]interface{}, key string, defaultValue bool) (bool, error) { + value := stateMap[key] + + if value == nil { + return defaultValue, nil + } + + valueAsBool, ok := value.(bool) + if !ok { + return false, errors.New("State [" + stateName + "] " + key + " illegal, required bool") + } + return valueAsBool, nil +} + +func (b BaseStateParser) GetIntOrDefault(stateName string, stateMap map[string]interface{}, key string, defaultValue int) (int, error) { + value := stateMap[key] + + if value == nil { + return defaultValue, nil + } + + // just use float64 to convert, json reader will read all number as float64 + valueAsFloat64, ok := value.(float64) + if !ok { + return defaultValue, errors.New("State [" + stateName + "] " + key + " illegal, required int") + } + + floatStr := strconv.FormatFloat(valueAsFloat64, 'f', -1, 64) + if strings.Contains(floatStr, ".") { + return defaultValue, errors.New("State [" + stateName + "] " + key + " illegal, required int") + } + + return int(valueAsFloat64), nil +} + +func (b BaseStateParser) GetFloat64OrDefault(stateName string, stateMap map[string]interface{}, key string, defaultValue float64) (float64, error) { + value := stateMap[key] + + if value == nil { + return defaultValue, nil + } + + valueAsFloat64, ok := value.(float64) + if !ok { + return defaultValue, errors.New("State [" + stateName + "] " + key + " illegal, required float64") + } + return valueAsFloat64, nil +} + type StateParserFactory interface { RegistryStateParser(stateType string, stateParser StateParser) @@ -89,8 +197,21 @@ func NewDefaultStateParserFactory() *DefaultStateParserFactory { // InitDefaultStateParser init StateParser by default func (d *DefaultStateParserFactory) InitDefaultStateParser() { choiceStateParser := NewChoiceStateParser() + serviceTaskStateParser := NewServiceTaskStateParser() + subStateMachineParser := NewSubStateMachineParser() + succeedEndStateParser := NewSucceedEndStateParser() + compensationTriggerStateParser := NewCompensationTriggerStateParser() + failEndStateParser := NewFailEndStateParser() + scriptTaskStateParser := NewScriptTaskStateParser() d.RegistryStateParser(choiceStateParser.StateType(), choiceStateParser) + d.RegistryStateParser(serviceTaskStateParser.StateType(), serviceTaskStateParser) + d.RegistryStateParser(subStateMachineParser.StateType(), subStateMachineParser) + d.RegistryStateParser(succeedEndStateParser.StateType(), succeedEndStateParser) + d.RegistryStateParser(compensationTriggerStateParser.StateType(), compensationTriggerStateParser) + d.RegistryStateParser(compensationTriggerStateParser.StateType(), compensationTriggerStateParser) + d.RegistryStateParser(failEndStateParser.StateType(), failEndStateParser) + d.RegistryStateParser(scriptTaskStateParser.StateType(), scriptTaskStateParser) } func (d *DefaultStateParserFactory) RegistryStateParser(stateType string, stateParser StateParser) { diff --git a/pkg/saga/statemachine/statelang/parser/sub_state_machine_parser.go b/pkg/saga/statemachine/statelang/parser/sub_state_machine_parser.go new file mode 100644 index 00000000..dc466da5 --- /dev/null +++ b/pkg/saga/statemachine/statelang/parser/sub_state_machine_parser.go @@ -0,0 +1,84 @@ +package parser + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" +) + +type SubStateMachineParser struct { + *AbstractTaskStateParser +} + +func NewSubStateMachineParser() *SubStateMachineParser { + return &SubStateMachineParser{ + NewAbstractTaskStateParser(), + } +} + +func (s SubStateMachineParser) StateType() string { + return constant.StateTypeSubStateMachine +} + +func (s SubStateMachineParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + subStateMachineImpl := state.NewSubStateMachineImpl() + + err := s.ParseTaskAttributes(stateName, subStateMachineImpl.AbstractTaskState, stateMap) + if err != nil { + return nil, err + } + + stateMachineName, err := s.BaseStateParser.GetString(stateName, stateMap, "StateMachineName") + if err != nil { + return nil, err + } + subStateMachineImpl.SetName(stateMachineName) + + if subStateMachineImpl.CompensateState() == "" { + // build default SubStateMachine compensate state + compensateSubStateMachineStateParser := NewCompensateSubStateMachineStateParser() + compensateState, err := compensateSubStateMachineStateParser.Parse(stateName, nil) + if err != nil { + return nil, err + } + compensateStateImpl, ok := compensateState.(state.TaskState) + if !ok { + return nil, errors.New(fmt.Sprintf("State [name:%s] has wrong compensateState type", stateName)) + } + subStateMachineImpl.SetCompensateStateImpl(compensateStateImpl) + subStateMachineImpl.SetCompensateState(compensateStateImpl.Name()) + } + return subStateMachineImpl, nil +} + +type CompensateSubStateMachineStateParser struct { + *AbstractTaskStateParser +} + +func NewCompensateSubStateMachineStateParser() *CompensateSubStateMachineStateParser { + return &CompensateSubStateMachineStateParser{ + NewAbstractTaskStateParser(), + } +} + +func (c CompensateSubStateMachineStateParser) StateType() string { + return constant.CompensateSubMachine +} + +func (c CompensateSubStateMachineStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + compensateSubStateMachineStateImpl := state.NewCompensateSubStateMachineStateImpl() + compensateSubStateMachineStateImpl.SetForCompensation(true) + + if stateMap != nil { + err := c.ParseTaskAttributes(stateName, compensateSubStateMachineStateImpl.ServiceTaskStateImpl.AbstractTaskState, stateMap) + if err != nil { + return nil, err + } + } + if compensateSubStateMachineStateImpl.Name() == "" { + compensateSubStateMachineStateImpl.SetName(constant.CompensateSubMachineStateNamePrefix + compensateSubStateMachineStateImpl.Hashcode()) + } + return compensateSubStateMachineStateImpl, nil +} diff --git a/pkg/saga/statemachine/statelang/parser/task_state_json_parser.go b/pkg/saga/statemachine/statelang/parser/task_state_json_parser.go new file mode 100644 index 00000000..5e702487 --- /dev/null +++ b/pkg/saga/statemachine/statelang/parser/task_state_json_parser.go @@ -0,0 +1,330 @@ +package parser + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" +) + +type AbstractTaskStateParser struct { + *BaseStateParser +} + +func NewAbstractTaskStateParser() *AbstractTaskStateParser { + return &AbstractTaskStateParser{ + &BaseStateParser{}, + } +} + +func (a *AbstractTaskStateParser) ParseTaskAttributes(stateName string, state *state.AbstractTaskState, stateMap map[string]interface{}) error { + err := a.ParseBaseAttributes(state.Name(), state.BaseState, stateMap) + if err != nil { + return err + } + + compensateState, err := a.GetStringOrDefault(stateName, stateMap, "CompensateState", "") + if err != nil { + return err + } + state.SetCompensateState(compensateState) + + isForCompensation, err := a.GetBoolOrDefault(stateName, stateMap, "IsForCompensation", false) + if err != nil { + return err + } + state.SetForCompensation(isForCompensation) + + isForUpdate, err := a.GetBoolOrDefault(stateName, stateMap, "IsForUpdate", false) + if err != nil { + return err + } + state.SetForUpdate(isForUpdate) + + isPersist, err := a.GetBoolOrDefault(stateName, stateMap, "IsPersist", false) + if err != nil { + return err + } + state.SetPersist(isPersist) + + isRetryPersistModeUpdate, err := a.GetBoolOrDefault(stateName, stateMap, "IsRetryPersistModeUpdate", false) + if err != nil { + return err + } + state.SetRetryPersistModeUpdate(isRetryPersistModeUpdate) + + isCompensatePersistModeUpdate, err := a.GetBoolOrDefault(stateName, stateMap, "IsCompensatePersistModeUpdate", false) + if err != nil { + return err + } + state.SetCompensatePersistModeUpdate(isCompensatePersistModeUpdate) + + retryInterfaces, err := a.GetSliceOrDefault(stateName, stateMap, "Retry", nil) + if err != nil { + return err + } + if retryInterfaces != nil { + retries, err := a.parseRetries(state.Name(), retryInterfaces) + if err != nil { + return err + } + state.SetRetry(retries) + } + + catchInterfaces, err := a.GetSliceOrDefault(stateName, stateMap, "Catch", nil) + if err != nil { + return err + } + if catchInterfaces != nil { + catches, err := a.parseCatches(state.Name(), catchInterfaces) + if err != nil { + return err + } + state.SetCatches(catches) + } + + inputInterfaces, err := a.GetSliceOrDefault(stateName, stateMap, "Input", nil) + if err != nil { + return err + } + if inputInterfaces != nil { + state.SetInput(inputInterfaces) + } + + output, err := a.GetMapOrDefault(stateMap, "Output", nil) + if err != nil { + return err + } + if output != nil { + state.SetOutput(output) + } + + statusMap, ok := stateMap["Status"].(map[string]string) + if ok { + state.SetStatus(statusMap) + } + + loopMap, ok := stateMap["Loop"].(map[string]interface{}) + if ok { + loop := a.parseLoop(stateName, loopMap) + state.SetLoop(loop) + } + + return nil +} + +func (a *AbstractTaskStateParser) parseLoop(stateName string, loopMap map[string]interface{}) state.Loop { + loopImpl := &state.LoopImpl{} + parallel, err := a.GetIntOrDefault(stateName, loopMap, "Parallel", 1) + if err != nil { + return nil + } + loopImpl.SetParallel(parallel) + + collection, err := a.GetStringOrDefault(stateName, loopMap, "Collection", "") + if err != nil { + return nil + } + loopImpl.SetCollection(collection) + + elementVariableName, err := a.GetStringOrDefault(stateName, loopMap, "ElementVariableName", "loopElement") + if err != nil { + return nil + } + loopImpl.SetElementVariableName(elementVariableName) + + elementIndexName, err := a.GetStringOrDefault(stateName, loopMap, "ElementIndexName", "loopCounter") + if err != nil { + return nil + } + loopImpl.SetElementIndexName(elementIndexName) + + completionCondition, err := a.GetStringOrDefault(stateName, loopMap, "CompletionCondition", "[nrOfInstances] == [nrOfCompletedInstances]") + if err != nil { + return nil + } + loopImpl.SetElementIndexName(completionCondition) + return loopImpl +} + +func (a *AbstractTaskStateParser) parseRetries(stateName string, retryInterfaces []interface{}) ([]state.Retry, error) { + retries := make([]state.Retry, 0) + for _, retryInterface := range retryInterfaces { + retryMap, ok := retryInterface.(map[string]interface{}) + if !ok { + + return nil, errors.New("State [" + stateName + "] " + "Retry illegal, require map[string]interface{}") + } + retry := &state.RetryImpl{} + errorTypes, err := a.GetSliceOrDefault(stateName, retryMap, "Exceptions", nil) + if err != nil { + return nil, err + } + if errorTypes != nil { + errorTypeNames := make([]string, 0) + for _, errorType := range errorTypes { + errorTypeNames = append(errorTypeNames, errorType.(string)) + } + retry.SetErrorTypeNames(errorTypeNames) + } + + maxAttempts, err := a.GetIntOrDefault(stateName, retryMap, "MaxAttempts", 0) + if err != nil { + return nil, err + } + retry.SetMaxAttempt(maxAttempts) + + backoffInterval, err := a.GetFloat64OrDefault(stateName, retryMap, "BackoffInterval", 0) + if err != nil { + return nil, err + } + retry.SetBackoffRate(backoffInterval) + + intervalSeconds, err := a.GetFloat64OrDefault(stateName, retryMap, "IntervalSeconds", 0) + if err != nil { + return nil, err + } + retry.SetIntervalSecond(intervalSeconds) + retries = append(retries, retry) + } + return retries, nil +} + +func (a *AbstractTaskStateParser) parseCatches(stateName string, catchInterfaces []interface{}) ([]state.ErrorMatch, error) { + errorMatches := make([]state.ErrorMatch, 0, len(catchInterfaces)) + for _, catchInterface := range catchInterfaces { + catchMap, ok := catchInterface.(map[string]interface{}) + if !ok { + return nil, errors.New("State [" + stateName + "] " + "Catch illegal, require map[string]interface{}") + } + errorMatch := &state.ErrorMatchImpl{} + errorInterfaces, err := a.GetSliceOrDefault(stateName, catchMap, "Exceptions", nil) + if err != nil { + return nil, err + } + if errorInterfaces != nil { + errorNames := make([]string, 0) + for _, errorType := range errorInterfaces { + errorNames = append(errorNames, errorType.(string)) + } + errorMatch.SetErrors(errorNames) + } + next, err := a.GetStringOrDefault(stateName, catchMap, "Next", "") + if err != nil { + return nil, err + } + errorMatch.SetNext(next) + errorMatches = append(errorMatches, errorMatch) + } + return errorMatches, nil +} + +type ServiceTaskStateParser struct { + *AbstractTaskStateParser +} + +func NewServiceTaskStateParser() *ServiceTaskStateParser { + return &ServiceTaskStateParser{ + NewAbstractTaskStateParser(), + } +} + +func (s ServiceTaskStateParser) StateType() string { + return constant.StateTypeServiceTask +} + +func (s ServiceTaskStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + serviceTaskStateImpl := state.NewServiceTaskStateImpl() + + err := s.ParseTaskAttributes(stateName, serviceTaskStateImpl.AbstractTaskState, stateMap) + if err != nil { + return nil, err + } + + serviceName, err := s.GetString(stateName, stateMap, "ServiceName") + if err != nil { + return nil, err + } + serviceTaskStateImpl.SetServiceName(serviceName) + + serviceMethod, err := s.GetString(stateName, stateMap, "ServiceMethod") + if err != nil { + return nil, err + } + serviceTaskStateImpl.SetServiceMethod(serviceMethod) + + serviceType, err := s.GetStringOrDefault(stateName, stateMap, "ServiceType", "") + if err != nil { + return nil, err + } + serviceTaskStateImpl.SetServiceType(serviceType) + + parameterTypeInterfaces, err := s.GetSliceOrDefault(stateName, stateMap, "ParameterTypes", nil) + if err != nil { + return nil, err + } + if parameterTypeInterfaces != nil { + var parameterTypes []string + for i := range parameterTypeInterfaces { + parameterType, ok := parameterTypeInterfaces[i].(string) + if !ok { + return nil, errors.New(fmt.Sprintf("State [%s] parameterType required string", stateName)) + } + + parameterTypes = append(parameterTypes, parameterType) + } + serviceTaskStateImpl.SetParameterTypes(parameterTypes) + } + + isAsync, err := s.GetBoolOrDefault(stateName, stateMap, "IsAsync", false) + if err != nil { + return nil, err + } + serviceTaskStateImpl.SetIsAsync(isAsync) + + return serviceTaskStateImpl, nil +} + +type ScriptTaskStateParser struct { + *AbstractTaskStateParser +} + +func NewScriptTaskStateParser() *ScriptTaskStateParser { + return &ScriptTaskStateParser{ + NewAbstractTaskStateParser(), + } +} + +func (s ScriptTaskStateParser) StateType() string { + return constant.StateTypeScriptTask +} + +func (s ScriptTaskStateParser) Parse(stateName string, stateMap map[string]interface{}) (statelang.State, error) { + scriptTaskStateImpl := state.NewScriptTaskStateImpl() + + err := s.ParseTaskAttributes(stateName, scriptTaskStateImpl.AbstractTaskState, stateMap) + if err != nil { + return nil, err + } + + scriptType, err := s.GetStringOrDefault(stateName, stateMap, "ScriptType", "") + if err != nil { + return nil, err + } + if scriptType != "" { + scriptTaskStateImpl.SetScriptType(scriptType) + } + + scriptContent, err := s.GetStringOrDefault(stateName, stateMap, "ScriptContent", "") + if err != nil { + return nil, err + } + scriptTaskStateImpl.SetScriptContent(scriptContent) + + scriptTaskStateImpl.SetForCompensation(false) + scriptTaskStateImpl.SetForUpdate(false) + scriptTaskStateImpl.SetPersist(false) + + return scriptTaskStateImpl, nil +} diff --git a/pkg/saga/statemachine/statelang/state.go b/pkg/saga/statemachine/statelang/state.go index 1e490ac0..29ac6727 100644 --- a/pkg/saga/statemachine/statelang/state.go +++ b/pkg/saga/statemachine/statelang/state.go @@ -30,6 +30,10 @@ type BaseState struct { stateMachine StateMachine } +func NewBaseState() *BaseState { + return &BaseState{} +} + func (b *BaseState) Name() string { return b.name } diff --git a/pkg/saga/statemachine/statelang/state/choice_state.go b/pkg/saga/statemachine/statelang/state/choice_state.go index e1cd973c..c3f48276 100644 --- a/pkg/saga/statemachine/statelang/state/choice_state.go +++ b/pkg/saga/statemachine/statelang/state/choice_state.go @@ -21,14 +21,15 @@ type Choice interface { } type ChoiceStateImpl struct { - statelang.BaseState + *statelang.BaseState defaultChoice string `alias:"Default"` choices []Choice `alias:"Choices"` } func NewChoiceStateImpl() *ChoiceStateImpl { return &ChoiceStateImpl{ - choices: make([]Choice, 0), + BaseState: statelang.NewBaseState(), + choices: make([]Choice, 0), } } diff --git a/pkg/saga/statemachine/statelang/state/compensation_trigger_state.go b/pkg/saga/statemachine/statelang/state/compensation_trigger_state.go new file mode 100644 index 00000000..cac71420 --- /dev/null +++ b/pkg/saga/statemachine/statelang/state/compensation_trigger_state.go @@ -0,0 +1,22 @@ +package state + +import ( + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" +) + +type CompensationTriggerState interface { + statelang.State +} + +type CompensationTriggerStateImpl struct { + *statelang.BaseState +} + +func NewCompensationTriggerStateImpl() *CompensationTriggerStateImpl { + s := &CompensationTriggerStateImpl{ + BaseState: statelang.NewBaseState(), + } + s.SetType(constant.StateTypeCompensationTrigger) + return s +} diff --git a/pkg/saga/statemachine/statelang/state/end_state.go b/pkg/saga/statemachine/statelang/state/end_state.go new file mode 100644 index 00000000..099e2e07 --- /dev/null +++ b/pkg/saga/statemachine/statelang/state/end_state.go @@ -0,0 +1,64 @@ +package state + +import ( + "github.com/seata/seata-go/pkg/saga/statemachine/constant" + "github.com/seata/seata-go/pkg/saga/statemachine/statelang" +) + +type EndState interface { + statelang.State +} + +type SucceedEndState interface { + EndState +} + +type SucceedEndStateImpl struct { + *statelang.BaseState +} + +func NewSucceedEndStateImpl() *SucceedEndStateImpl { + s := &SucceedEndStateImpl{ + BaseState: statelang.NewBaseState(), + } + s.SetType(constant.StateTypeSucceed) + return s +} + +type FailEndState interface { + EndState + + ErrorCode() string + + Message() string +} + +type FailEndStateImpl struct { + *statelang.BaseState + errorCode string + message string +} + +func NewFailEndStateImpl() *FailEndStateImpl { + s := &FailEndStateImpl{ + BaseState: statelang.NewBaseState(), + } + s.SetType(constant.StateTypeFail) + return s +} + +func (f *FailEndStateImpl) ErrorCode() string { + return f.errorCode +} + +func (f *FailEndStateImpl) SetErrorCode(errorCode string) { + f.errorCode = errorCode +} + +func (f *FailEndStateImpl) Message() string { + return f.message +} + +func (f *FailEndStateImpl) SetMessage(message string) { + f.message = message +} diff --git a/pkg/saga/statemachine/statelang/state/sub_state_machine.go b/pkg/saga/statemachine/statelang/state/sub_state_machine.go new file mode 100644 index 00000000..bf0d96d5 --- /dev/null +++ b/pkg/saga/statemachine/statelang/state/sub_state_machine.go @@ -0,0 +1,69 @@ +package state + +import ( + "github.com/google/uuid" + "github.com/seata/seata-go/pkg/saga/statemachine/constant" +) + +type SubStateMachine interface { + TaskState + + StateMachineName() string + + CompensateStateImpl() TaskState +} + +type SubStateMachineImpl struct { + *ServiceTaskStateImpl + stateMachineName string + compensateState TaskState +} + +func NewSubStateMachineImpl() *SubStateMachineImpl { + return &SubStateMachineImpl{ + ServiceTaskStateImpl: NewServiceTaskStateImpl(), + } +} + +func (s *SubStateMachineImpl) StateMachineName() string { + return s.stateMachineName +} + +func (s *SubStateMachineImpl) SetStateMachineName(stateMachineName string) { + s.stateMachineName = stateMachineName +} + +func (s *SubStateMachineImpl) CompensateStateImpl() TaskState { + return s.compensateState +} + +func (s *SubStateMachineImpl) SetCompensateStateImpl(compensateState TaskState) { + s.compensateState = compensateState +} + +type CompensateSubStateMachineState interface { + ServiceTaskState +} + +type CompensateSubStateMachineStateImpl struct { + *ServiceTaskStateImpl + hashcode string +} + +func NewCompensateSubStateMachineStateImpl() *CompensateSubStateMachineStateImpl { + uuid := uuid.New() + c := &CompensateSubStateMachineStateImpl{ + ServiceTaskStateImpl: NewServiceTaskStateImpl(), + hashcode: uuid.String(), + } + c.SetType(constant.CompensateSubMachine) + return c +} + +func (c *CompensateSubStateMachineStateImpl) Hashcode() string { + return c.hashcode +} + +func (c *CompensateSubStateMachineStateImpl) SetHashcode(hashcode string) { + c.hashcode = hashcode +} diff --git a/pkg/saga/statemachine/statelang/state/task_state.go b/pkg/saga/statemachine/statelang/state/task_state.go index a9164d35..63e33f3c 100644 --- a/pkg/saga/statemachine/statelang/state/task_state.go +++ b/pkg/saga/statemachine/statelang/state/task_state.go @@ -1,7 +1,9 @@ package state import ( + "github.com/seata/seata-go/pkg/saga/statemachine/constant" "github.com/seata/seata-go/pkg/saga/statemachine/statelang" + "reflect" ) type TaskState interface { @@ -9,9 +11,39 @@ type TaskState interface { CompensateState() string - Status() map[string]string + ForCompensation() bool + + ForUpdate() bool Retry() []Retry + + Catches() []ErrorMatch + + Status() map[string]string + + Loop() Loop +} + +type Loop interface { + Parallel() int + + Collection() string + + ElementVariableName() string + + ElementIndexName() string + + CompletionCondition() string +} + +type ErrorMatch interface { + Errors() []string + + ErrorTypes() []reflect.Type + + SetErrorTypes(errorTypes []reflect.Type) + + Next() string } type Retry interface { @@ -26,5 +58,376 @@ type Retry interface { type ServiceTaskState interface { TaskState - //TODO add serviceTask + + ServiceType() string + + ServiceName() string + + ServiceMethod() string + + ParameterTypes() []string + + Persist() bool + + RetryPersistModeUpdate() bool + + CompensatePersistModeUpdate() bool +} + +type AbstractTaskState struct { + *statelang.BaseState + loop Loop + catches []ErrorMatch + input []interface{} + output map[string]interface{} + compensatePersistModeUpdate bool + retryPersistModeUpdate bool + forCompensation bool + forUpdate bool + persist bool + compensateState string + status map[string]string + retry []Retry +} + +func NewAbstractTaskState() *AbstractTaskState { + return &AbstractTaskState{ + BaseState: &statelang.BaseState{}, + } +} + +func (a *AbstractTaskState) Input() []interface{} { + return a.input +} + +func (a *AbstractTaskState) SetInput(input []interface{}) { + a.input = input +} + +func (a *AbstractTaskState) Output() map[string]interface{} { + return a.output +} + +func (a *AbstractTaskState) SetOutput(output map[string]interface{}) { + a.output = output +} + +func (a *AbstractTaskState) CompensatePersistModeUpdate() bool { + return a.compensatePersistModeUpdate +} + +func (a *AbstractTaskState) SetCompensatePersistModeUpdate(isCompensatePersistModeUpdate bool) { + a.compensatePersistModeUpdate = isCompensatePersistModeUpdate +} + +func (a *AbstractTaskState) RetryPersistModeUpdate() bool { + return a.retryPersistModeUpdate +} + +func (a *AbstractTaskState) SetRetryPersistModeUpdate(retryPersistModeUpdate bool) { + a.retryPersistModeUpdate = retryPersistModeUpdate +} + +func (a *AbstractTaskState) Persist() bool { + return a.persist +} + +func (a *AbstractTaskState) SetPersist(persist bool) { + a.persist = persist +} + +func (a *AbstractTaskState) SetLoop(loop Loop) { + a.loop = loop +} + +func (a *AbstractTaskState) SetCatches(catches []ErrorMatch) { + a.catches = catches +} + +func (a *AbstractTaskState) SetForCompensation(forCompensation bool) { + a.forCompensation = forCompensation +} + +func (a *AbstractTaskState) SetForUpdate(forUpdate bool) { + a.forUpdate = forUpdate +} + +func (a *AbstractTaskState) SetCompensateState(compensateState string) { + a.compensateState = compensateState +} + +func (a *AbstractTaskState) SetStatus(status map[string]string) { + a.status = status +} + +func (a *AbstractTaskState) SetRetry(retry []Retry) { + a.retry = retry +} + +func (a *AbstractTaskState) ForCompensation() bool { + return a.forCompensation +} + +func (a *AbstractTaskState) ForUpdate() bool { + return a.forUpdate +} + +func (a *AbstractTaskState) Catches() []ErrorMatch { + return a.catches +} + +func (a *AbstractTaskState) Loop() Loop { + return a.loop +} + +func (a *AbstractTaskState) CompensateState() string { + return a.compensateState +} + +func (a *AbstractTaskState) Status() map[string]string { + return a.status +} + +func (a *AbstractTaskState) Retry() []Retry { + return a.retry +} + +type ServiceTaskStateImpl struct { + *AbstractTaskState + serviceType string + serviceName string + serviceMethod string + parameterTypes []string + persist bool + retryPersistModeUpdate bool + compensatePersistModeUpdate bool + isAsync bool +} + +func NewServiceTaskStateImpl() *ServiceTaskStateImpl { + return &ServiceTaskStateImpl{ + AbstractTaskState: NewAbstractTaskState(), + } +} + +func (s *ServiceTaskStateImpl) IsAsync() bool { + return s.isAsync +} + +func (s *ServiceTaskStateImpl) SetIsAsync(isAsync bool) { + s.isAsync = isAsync +} + +func (s *ServiceTaskStateImpl) SetServiceType(serviceType string) { + s.serviceType = serviceType +} + +func (s *ServiceTaskStateImpl) SetServiceName(serviceName string) { + s.serviceName = serviceName +} + +func (s *ServiceTaskStateImpl) SetServiceMethod(serviceMethod string) { + s.serviceMethod = serviceMethod +} + +func (s *ServiceTaskStateImpl) SetParameterTypes(parameterTypes []string) { + s.parameterTypes = parameterTypes +} + +func (s *ServiceTaskStateImpl) SetPersist(persist bool) { + s.persist = persist +} + +func (s *ServiceTaskStateImpl) SetRetryPersistModeUpdate(retryPersistModeUpdate bool) { + s.retryPersistModeUpdate = retryPersistModeUpdate +} + +func (s *ServiceTaskStateImpl) SetCompensatePersistModeUpdate(compensatePersistModeUpdate bool) { + s.compensatePersistModeUpdate = compensatePersistModeUpdate +} + +func (s *ServiceTaskStateImpl) Loop() Loop { + return s.loop +} + +func (s *ServiceTaskStateImpl) ServiceType() string { + return s.serviceType +} + +func (s *ServiceTaskStateImpl) ServiceName() string { + return s.serviceName +} + +func (s *ServiceTaskStateImpl) ServiceMethod() string { + return s.serviceMethod +} + +func (s *ServiceTaskStateImpl) ParameterTypes() []string { + return s.parameterTypes +} + +func (s *ServiceTaskStateImpl) Persist() bool { + return s.persist +} + +func (s *ServiceTaskStateImpl) RetryPersistModeUpdate() bool { + return s.retryPersistModeUpdate +} + +func (s *ServiceTaskStateImpl) CompensatePersistModeUpdate() bool { + return s.compensatePersistModeUpdate +} + +type LoopImpl struct { + parallel int + collection string + elementVariableName string + elementIndexName string + completionCondition string +} + +func (l *LoopImpl) SetParallel(parallel int) { + l.parallel = parallel +} + +func (l *LoopImpl) SetCollection(collection string) { + l.collection = collection +} + +func (l *LoopImpl) SetElementVariableName(elementVariableName string) { + l.elementVariableName = elementVariableName +} + +func (l *LoopImpl) SetElementIndexName(elementIndexName string) { + l.elementIndexName = elementIndexName +} + +func (l *LoopImpl) SetCompletionCondition(completionCondition string) { + l.completionCondition = completionCondition +} + +func (l *LoopImpl) Parallel() int { + return l.parallel +} + +func (l *LoopImpl) Collection() string { + return l.collection +} + +func (l *LoopImpl) ElementVariableName() string { + return l.elementVariableName +} + +func (l *LoopImpl) ElementIndexName() string { + return l.elementIndexName +} + +func (l *LoopImpl) CompletionCondition() string { + return l.completionCondition +} + +type RetryImpl struct { + errorTypeNames []string + intervalSecond float64 + maxAttempt int + backoffRate float64 +} + +func (r *RetryImpl) SetErrorTypeNames(errorTypeNames []string) { + r.errorTypeNames = errorTypeNames +} + +func (r *RetryImpl) SetIntervalSecond(intervalSecond float64) { + r.intervalSecond = intervalSecond +} + +func (r *RetryImpl) SetMaxAttempt(maxAttempt int) { + r.maxAttempt = maxAttempt +} + +func (r *RetryImpl) SetBackoffRate(backoffRate float64) { + r.backoffRate = backoffRate +} + +func (r *RetryImpl) ErrorTypeNames() []string { + return r.errorTypeNames +} + +func (r *RetryImpl) IntervalSecond() float64 { + return r.intervalSecond +} + +func (r *RetryImpl) MaxAttempt() int { + return r.maxAttempt +} + +func (r *RetryImpl) BackoffRate() float64 { + return r.backoffRate +} + +type ErrorMatchImpl struct { + errors []string + errorTypes []reflect.Type + next string +} + +func (e *ErrorMatchImpl) SetErrors(errors []string) { + e.errors = errors +} + +func (e *ErrorMatchImpl) SetNext(next string) { + e.next = next +} + +func (e *ErrorMatchImpl) Errors() []string { + return e.errors +} + +func (e *ErrorMatchImpl) ErrorTypes() []reflect.Type { + return e.errorTypes +} + +func (e *ErrorMatchImpl) SetErrorTypes(errorTypes []reflect.Type) { + e.errorTypes = errorTypes +} + +func (e *ErrorMatchImpl) Next() string { + return e.next +} + +type ScriptTaskState interface { + TaskState + + ScriptType() string + + ScriptContent() string +} + +type ScriptTaskStateImpl struct { + *AbstractTaskState + scriptType string + scriptContent string +} + +func NewScriptTaskStateImpl() *ScriptTaskStateImpl { + return &ScriptTaskStateImpl{ + AbstractTaskState: NewAbstractTaskState(), + scriptType: constant.DefaultScriptType, + } +} + +func (s *ScriptTaskStateImpl) SetScriptType(scriptType string) { + s.scriptType = scriptType +} + +func (s *ScriptTaskStateImpl) SetScriptContent(scriptContent string) { + s.scriptContent = scriptContent +} + +func (s *ScriptTaskStateImpl) ScriptType() string { + return s.scriptType +} + +func (s *ScriptTaskStateImpl) ScriptContent() string { + return s.scriptContent } diff --git a/testdata/saga/statelang/simple_statelang_with_choice.json b/testdata/saga/statelang/simple_statelang_with_choice.json new file mode 100644 index 00000000..4be7a48f --- /dev/null +++ b/testdata/saga/statelang/simple_statelang_with_choice.json @@ -0,0 +1,38 @@ +{ + "Name": "simpleChoiceTestStateMachine", + "Comment": "带条件分支的测试状态机定义", + "StartState": "FirstState", + "Version": "0.0.1", + "States": { + "FirstState": { + "Type": "ServiceTask", + "ServiceName": "demoService", + "ServiceMethod": "foo", + "Next": "ChoiceState" + }, + "ChoiceState":{ + "Type": "Choice", + "Choices":[ + { + "Expression":"[a] == 1", + "Next":"SecondState" + }, + { + "Expression":"[a] == 2", + "Next":"ThirdState" + } + ], + "Default":"SecondState" + }, + "SecondState": { + "Type": "ServiceTask", + "ServiceName": "demoService", + "ServiceMethod": "bar" + }, + "ThirdState": { + "Type": "ServiceTask", + "ServiceName": "demoService", + "ServiceMethod": "foo" + } + } +} \ No newline at end of file diff --git a/testdata/saga/statelang/simple_statemachine.json b/testdata/saga/statelang/simple_statemachine.json new file mode 100644 index 00000000..91ed5019 --- /dev/null +++ b/testdata/saga/statelang/simple_statemachine.json @@ -0,0 +1,138 @@ +{ + "Name": "simpleTestStateMachine", + "Comment": "测试状态机定义", + "StartState": "FirstState", + "Version": "0.0.1", + "States": { + "FirstState": { + "Type": "ServiceTask", + "ServiceName": "is.seata.saga.DemoService", + "ServiceMethod": "foo", + "IsPersist": false, + "Next": "ScriptState" + }, + "ScriptState": { + "Type": "ScriptTask", + "ScriptType": "groovy", + "ScriptContent": "return 'hello ' + inputA", + "Input": [ + { + "inputA": "$.data1" + } + ], + "Output": { + "scriptStateResult": "$.#root" + }, + "Next": "ChoiceState" + }, + "ChoiceState": { + "Type": "Choice", + "Choices": [ + { + "Expression": "foo == 1", + "Next": "FirstMatchState" + }, + { + "Expression": "foo == 2", + "Next": "SecondMatchState" + } + ], + "Default": "FailState" + }, + "FirstMatchState": { + "Type": "ServiceTask", + "ServiceName": "is.seata.saga.DemoService", + "ServiceMethod": "bar", + "CompensateState": "CompensateFirst", + "Status": { + "return.code == 'S'": "SU", + "return.code == 'F'": "FA", + "$exception{java.lang.Throwable}": "UN" + }, + "Input": [ + { + "inputA1": "$.data1", + "inputA2": { + "a": "$.data2.a" + } + }, + { + "inputB": "$.header" + } + ], + "Output": { + "firstMatchStateResult": "$.#root" + }, + "Retry": [ + { + "Exceptions": ["java.lang.Exception"], + "IntervalSeconds": 2, + "MaxAttempts": 3, + "BackoffRate": 1.5 + } + ], + "Catch": [ + { + "Exceptions": [ + "java.lang.Exception" + ], + "Next": "CompensationTrigger" + } + ], + "Next": "SuccessState" + }, + "CompensateFirst": { + "Type": "ServiceTask", + "ServiceName": "is.seata.saga.DemoService", + "ServiceMethod": "compensateBar", + "IsForCompensation": true, + "IsForUpdate": true, + "Input": [ + { + "input": "$.data" + } + ], + "Output": { + "firstMatchStateResult": "$.#root" + }, + "Status": { + "return.code == 'S'": "SU", + "return.code == 'F'": "FA", + "$exception{java.lang.Throwable}": "UN" + } + }, + "CompensationTrigger": { + "Type": "CompensationTrigger", + "Next": "CompensateEndState" + }, + "CompensateEndState": { + "Type": "Fail", + "ErrorCode": "StateCompensated", + "Message": "State Compensated!" + }, + "SecondMatchState": { + "Type": "SubStateMachine", + "StateMachineName": "simpleTestSubStateMachine", + "Input": [ + { + "input": "$.data" + }, + { + "header": "$.header" + } + ], + "Output": { + "firstMatchStateResult": "$.#root" + }, + "Next": "SuccessState" + }, + "FailState": { + "Type": "Fail", + "ErrorCode": "DefaultStateError", + "Message": "No Matches!" + }, + "SuccessState": { + "Type": "Succeed" + } + } +} \ No newline at end of file diff --git a/testdata/saga/statelang/state_machine_new_designer.json b/testdata/saga/statelang/state_machine_new_designer.json new file mode 100644 index 00000000..0fd815ec --- /dev/null +++ b/testdata/saga/statelang/state_machine_new_designer.json @@ -0,0 +1,424 @@ +{ + "Name": "StateMachineNewDesigner", + "Comment": "This state machine is modeled by designer tools.", + "Version": "0.0.1", + "style": { + "bounds": { + "x": 200, + "y": 200, + "width": 36, + "height": 36 + } + }, + "States": { + "ServiceTask-a9h2o51": { + "style": { + "bounds": { + "x": 300, + "y": 178, + "width": 100, + "height": 80 + } + }, + "Name": "ServiceTask-a9h2o51", + "IsForCompensation": false, + "Input": [ + {} + ], + "Output": {}, + "Status": {}, + "Retry": [], + "ServiceName": "", + "ServiceMethod": "", + "Type": "ServiceTask", + "Next": "Choice-4ajl8nt", + "edge": { + "Choice-4ajl8nt": { + "style": { + "waypoints": [ + { + "original": { + "x": 400, + "y": 218 + }, + "x": 400, + "y": 218 + }, + { + "x": 435, + "y": 218 + }, + { + "original": { + "x": 455, + "y": 218 + }, + "x": 455, + "y": 218 + } + ], + "source": "ServiceTask-a9h2o51", + "target": "Choice-4ajl8nt" + }, + "Type": "Transition" + } + }, + "CompensateState": "CompensateFirstState" + }, + "Choice-4ajl8nt": { + "style": { + "bounds": { + "x": 455, + "y": 193, + "width": 50, + "height": 50 + } + }, + "Name": "Choice-4ajl8nt", + "Type": "Choice", + "Choices": [ + { + "Expression": "", + "Next": "SubStateMachine-cauj9uy" + }, + { + "Expression": "", + "Next": "ServiceTask-vdij28l" + } + ], + "Default": "SubStateMachine-cauj9uy", + "edge": { + "SubStateMachine-cauj9uy": { + "style": { + "waypoints": [ + { + "original": { + "x": 505, + "y": 218 + }, + "x": 505, + "y": 218 + }, + { + "x": 530, + "y": 218 + }, + { + "original": { + "x": 550, + "y": 218 + }, + "x": 550, + "y": 218 + } + ], + "source": "Choice-4ajl8nt", + "target": "SubStateMachine-cauj9uy" + }, + "Type": "ChoiceEntry" + }, + "ServiceTask-vdij28l": { + "style": { + "waypoints": [ + { + "original": { + "x": 480, + "y": 243 + }, + "x": 480, + "y": 243 + }, + { + "x": 600, + "y": 290 + }, + { + "original": { + "x": 600, + "y": 310 + }, + "x": 600, + "y": 310 + } + ], + "source": "Choice-4ajl8nt", + "target": "ServiceTask-vdij28l" + }, + "Type": "ChoiceEntry" + } + } + }, + "CompensateFirstState": { + "style": { + "bounds": { + "x": 300, + "y": 310, + "width": 100, + "height": 80 + } + }, + "Name": "CompensateFirstState", + "IsForCompensation": true, + "Input": [ + {} + ], + "Output": {}, + "Status": {}, + "Retry": [], + "ServiceName": "", + "ServiceMethod": "", + "Type": "ServiceTask" + }, + "SubStateMachine-cauj9uy": { + "style": { + "bounds": { + "x": 550, + "y": 178, + "width": 100, + "height": 80 + } + }, + "Name": "SubStateMachine-cauj9uy", + "IsForCompensation": false, + "Input": [ + {} + ], + "Output": {}, + "Status": {}, + "Retry": [], + "StateMachineName": "", + "Type": "SubStateMachine", + "Next": "Succeed-5x3z98u", + "edge": { + "Succeed-5x3z98u": { + "style": { + "waypoints": [ + { + "original": { + "x": 650, + "y": 218 + }, + "x": 650, + "y": 218 + }, + { + "x": 702, + "y": 218 + }, + { + "original": { + "x": 722, + "y": 218 + }, + "x": 722, + "y": 218 + } + ], + "source": "SubStateMachine-cauj9uy", + "target": "Succeed-5x3z98u" + }, + "Type": "Transition" + } + } + }, + "ServiceTask-vdij28l": { + "style": { + "bounds": { + "x": 550, + "y": 310, + "width": 100, + "height": 80 + } + }, + "Name": "ServiceTask-vdij28l", + "IsForCompensation": false, + "Input": [ + {} + ], + "Output": {}, + "Status": {}, + "Retry": [], + "ServiceName": "", + "ServiceMethod": "", + "Catch": [ + { + "Exceptions": [], + "Next": "CompensationTrigger-uldp2ou" + } + ], + "Type": "ServiceTask", + "catch": { + "style": { + "bounds": { + "x": 632, + "y": 372, + "width": 36, + "height": 36 + } + }, + "edge": { + "CompensationTrigger-uldp2ou": { + "style": { + "waypoints": [ + { + "original": { + "x": 668, + "y": 390 + }, + "x": 668, + "y": 390 + }, + { + "x": 702, + "y": 390 + }, + { + "original": { + "x": 722, + "y": 390 + }, + "x": 722, + "y": 390 + } + ], + "source": "ServiceTask-vdij28l", + "target": "CompensationTrigger-uldp2ou" + }, + "Type": "ExceptionMatch" + } + } + }, + "Next": "Succeed-5x3z98u", + "edge": { + "Succeed-5x3z98u": { + "style": { + "waypoints": [ + { + "original": { + "x": 600, + "y": 310 + }, + "x": 600, + "y": 310 + }, + { + "x": 740, + "y": 256 + }, + { + "original": { + "x": 740, + "y": 236 + }, + "x": 740, + "y": 236 + } + ], + "source": "ServiceTask-vdij28l", + "target": "Succeed-5x3z98u" + }, + "Type": "Transition" + } + } + }, + "Succeed-5x3z98u": { + "style": { + "bounds": { + "x": 722, + "y": 200, + "width": 36, + "height": 36 + } + }, + "Name": "Succeed-5x3z98u", + "Type": "Succeed" + }, + "CompensationTrigger-uldp2ou": { + "style": { + "bounds": { + "x": 722, + "y": 372, + "width": 36, + "height": 36 + } + }, + "Name": "CompensationTrigger-uldp2ou", + "Type": "CompensationTrigger", + "Next": "Fail-9roxcv5", + "edge": { + "Fail-9roxcv5": { + "style": { + "waypoints": [ + { + "original": { + "x": 758, + "y": 390 + }, + "x": 758, + "y": 390 + }, + { + "x": 792, + "y": 390 + }, + { + "original": { + "x": 812, + "y": 390 + }, + "x": 812, + "y": 390 + } + ], + "source": "CompensationTrigger-uldp2ou", + "target": "Fail-9roxcv5" + }, + "Type": "Transition" + } + } + }, + "Fail-9roxcv5": { + "style": { + "bounds": { + "x": 812, + "y": 372, + "width": 36, + "height": 36 + } + }, + "Name": "Fail-9roxcv5", + "ErrorCode": "", + "Message": "", + "Type": "Fail" + } + }, + "StartState": "ServiceTask-a9h2o51", + "edge": { + "style": { + "waypoints": [ + { + "original": { + "x": 236, + "y": 218 + }, + "x": 236, + "y": 218 + }, + { + "x": 280, + "y": 218 + }, + { + "original": { + "x": 300, + "y": 218 + }, + "x": 300, + "y": 218 + } + ], + "target": "ServiceTask-a9h2o51" + }, + "Type": "Transition" + } +} \ No newline at end of file