diff --git a/changes/en-us/2.0.0.md b/changes/en-us/2.0.0.md index 33bbc14880e..8c5914483e9 100644 --- a/changes/en-us/2.0.0.md +++ b/changes/en-us/2.0.0.md @@ -80,6 +80,7 @@ The version is updated as follows: - [[#5930](https://github.com/seata/seata/pull/5930)] fix the issue of missing sentinel password in store redis mode ### optimize: +- [[#5928](https://github.com/seata/seata/pull/5928)] add Saga statelang semantic validation - [[#5208](https://github.com/seata/seata/pull/5208)] optimize throwable getCause once more - [[#5212](https://github.com/seata/seata/pull/5212)] optimize log message level - [[#5237](https://github.com/seata/seata/pull/5237)] optimize exception log message print(EnhancedServiceLoader.loadFile#cahtch) diff --git a/changes/zh-cn/2.0.0.md b/changes/zh-cn/2.0.0.md index 0b5401cad99..f7a9d189bd7 100644 --- a/changes/zh-cn/2.0.0.md +++ b/changes/zh-cn/2.0.0.md @@ -79,6 +79,7 @@ Seata 是一款开源的分布式事务解决方案,提供高性能和简单 - [[#5930](https://github.com/seata/seata/pull/5930)] 修复存储为redis哨兵模式下哨兵密码缺失的问题 ### optimize: +- [[#5928](https://github.com/seata/seata/pull/5928)] 增加Saga模式状态机语义验证阶段 - [[#5208](https://github.com/seata/seata/pull/5208)] 优化多次重复获取Throwable#getCause问题 - [[#5212](https://github.com/seata/seata/pull/5212)] 优化不合理的日志信息级别 - [[#5237](https://github.com/seata/seata/pull/5237)] 优化异常日志打印(EnhancedServiceLoader.loadFile#cahtch) diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/impl/StateMachineParserImpl.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/impl/StateMachineParserImpl.java index 5fd73e75f0e..783dfe34ead 100644 --- a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/impl/StateMachineParserImpl.java +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/impl/StateMachineParserImpl.java @@ -30,6 +30,7 @@ import io.seata.saga.statelang.parser.StateParser; import io.seata.saga.statelang.parser.StateParserFactory; import io.seata.saga.statelang.parser.utils.DesignerJsonTransformer; +import io.seata.saga.statelang.validator.StateMachineValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +45,8 @@ public class StateMachineParserImpl implements StateMachineParser { private String jsonParserName = DomainConstants.DEFAULT_JSON_PARSER; + private final StateMachineValidator validator = new StateMachineValidator(); + public StateMachineParserImpl(String jsonParserName) { if (StringUtils.isNotBlank(jsonParserName)) { this.jsonParserName = jsonParserName; @@ -124,6 +127,8 @@ public StateMachine parse(String json) { } } } + + validator.validate(stateMachine); return stateMachine; } @@ -134,4 +139,4 @@ public String getJsonParserName() { public void setJsonParserName(String jsonParserName) { this.jsonParserName = jsonParserName; } -} \ No newline at end of file +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/utils/StateMachineUtils.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/utils/StateMachineUtils.java new file mode 100644 index 00000000000..c7179841f0c --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/parser/utils/StateMachineUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.parser.utils; + +import io.seata.common.util.StringUtils; +import io.seata.saga.statelang.domain.ChoiceState; +import io.seata.saga.statelang.domain.DomainConstants; +import io.seata.saga.statelang.domain.State; +import io.seata.saga.statelang.domain.TaskState; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Util class for parsing StateMachine + * + * @author ptyin + */ +public class StateMachineUtils { + public static Set getAllPossibleSubsequentStates(State state) { + Set subsequentStates = new HashSet<>(); + // Next state + subsequentStates.add(state.getNext()); + switch (state.getType()) { + case DomainConstants.STATE_TYPE_SCRIPT_TASK: + case DomainConstants.STATE_TYPE_SERVICE_TASK: + case DomainConstants.STATE_TYPE_SUB_STATE_MACHINE: + case DomainConstants.STATE_TYPE_SUB_MACHINE_COMPENSATION: + // Next state in catches + Optional.ofNullable(((TaskState) state).getCatches()) + .ifPresent(c -> c.forEach(e -> subsequentStates.add(e.getNext()))); + break; + + case DomainConstants.STATE_TYPE_CHOICE: + // Choice state + Optional.ofNullable(((ChoiceState) state).getChoices()) + .ifPresent(c -> c.forEach(e -> subsequentStates.add(e.getNext()))); + // Default choice + subsequentStates.add(((ChoiceState) state).getDefault()); + break; + default: + // Otherwise do nothing + } + return subsequentStates.stream().filter(StringUtils::isNotBlank).collect(Collectors.toSet()); + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/Rule.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/Rule.java new file mode 100644 index 00000000000..e4c13977430 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/Rule.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator; + +import io.seata.saga.statelang.domain.StateMachine; + +/** + * Validation rule interface, use SPI to inject rules + * + * @author ptyin + */ +public interface Rule { + /** + * Validate a state machine + * + * @param stateMachine state machine + * @return true if passes, false if fails + */ + boolean validate(StateMachine stateMachine); + + /** + * Get the rule name + * + * @return name of the rule + */ + String getName(); + + /** + * Get hints why validation passes or fails. Use this method to show more messages about validation result. + * + * @return hint of the rule + */ + String getHint(); +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/RuleFactory.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/RuleFactory.java new file mode 100644 index 00000000000..74bf3548f06 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/RuleFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator; + +import io.seata.common.loader.EnhancedServiceLoader; + +import java.util.List; + +/** + * Factorial class to get all rules. + * + * @author ptyin + */ +public class RuleFactory { + private static final List RULES = EnhancedServiceLoader.loadAll(Rule.class); + + public static List getRules() { + return RULES; + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/StateMachineValidator.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/StateMachineValidator.java new file mode 100644 index 00000000000..1403644dd0e --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/StateMachineValidator.java @@ -0,0 +1,50 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator; + +import io.seata.saga.statelang.domain.StateMachine; + +import java.util.List; + +/** + * State machine validator used to validate rules. + * + * @author ptyin + */ +public class StateMachineValidator { + + /** + * Validate on state machine + * + * @param stateMachine state machine + * @throws ValidationException throws if there is a validation rule failed + */ + public void validate(StateMachine stateMachine) throws ValidationException { + List rules = RuleFactory.getRules(); + for (Rule rule: rules) { + boolean pass; + try { + pass = rule.validate(stateMachine); + } catch (Throwable e) { + throw new ValidationException(rule, "Exception occurs", e); + } + if (!pass) { + throw new ValidationException(rule, "Failed"); + } + } + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/ValidationException.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/ValidationException.java new file mode 100644 index 00000000000..ef654550b35 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/ValidationException.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator; + +import io.seata.common.util.StringUtils; + +/** + * Validation exception throws if exception occurs in validation phase. + * + * @author ptyin + */ +public class ValidationException extends RuntimeException { + + public ValidationException(Rule rule, String message) { + super(spliceMessage(rule, message)); + } + + public ValidationException(Rule rule, String message, Throwable cause) { + super(spliceMessage(rule, message), cause); + } + + private static String spliceMessage(Rule rule, String message) { + String canonicalMessage = String.format("Rule [%s]: %s", rule.getName(), message); + if (StringUtils.isNotBlank(rule.getHint())) { + canonicalMessage = canonicalMessage + ", hints: " + rule.getHint(); + } + return canonicalMessage; + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/AbstractRule.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/AbstractRule.java new file mode 100644 index 00000000000..99105b8b867 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/AbstractRule.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator.impl; + +import io.seata.saga.statelang.validator.Rule; + +/** + * Abstract class for {@link Rule} + * + * @author ptyin + */ +public abstract class AbstractRule implements Rule { + + protected String hint; + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public String getHint() { + return hint; + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/FiniteTerminationRule.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/FiniteTerminationRule.java new file mode 100644 index 00000000000..61650acb816 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/FiniteTerminationRule.java @@ -0,0 +1,156 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator.impl; + +import io.seata.saga.statelang.domain.State; +import io.seata.saga.statelang.domain.StateMachine; +import io.seata.saga.statelang.parser.utils.StateMachineUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * Rule to check if the state machine can terminate in finite time, i.e. if there is an infinite loop + * + * @author ptyin + */ +public class FiniteTerminationRule extends AbstractRule { + + @Override + public boolean validate(StateMachine stateMachine) { + String stateName = stateMachine.getStartState(); + State startState = stateMachine.getState(stateMachine.getStartState()); + String notFoundHintTemplate = "State [%s] is not defined in state machine"; + if (startState == null) { + hint = String.format(notFoundHintTemplate, stateMachine.getStartState()); + return false; + } + + DisjointSet disjointSet = new DisjointSet(stateMachine.getStates().keySet()); + Set visited = new HashSet<>(); + Map> nextStateNameMap = new HashMap<>(); + + iterate(stateMachine, stateName, disjointSet, visited, nextStateNameMap, new Stack<>()); + + Map> rootToStateNames = new HashMap<>(); + for (String disjointStateName : stateMachine.getStates().keySet()) { + String root = disjointSet.find(disjointStateName); + if (!rootToStateNames.containsKey(root)) { + rootToStateNames.put(root, new HashSet<>()); + } + rootToStateNames.get(root).add(disjointStateName); + } + + for (Set cycleStateNames : rootToStateNames.values()) { + if (cycleStateNames.size() <= 1) { + // Not in a cycle + continue; + } + boolean noOutgoingFlow = true; + for (String cycleStateName : cycleStateNames) { + if (!cycleStateNames.containsAll(nextStateNameMap.get(cycleStateName))) { + // There is at least an outgoing flow not in this cycle + noOutgoingFlow = false; + break; + } + } + if (noOutgoingFlow) { + hint = String.format("There is a infinite loop [%s] without outgoing flow to end", + String.join(", ", cycleStateNames)); + return false; + } + } + return true; + } + + private static void iterate( + StateMachine stateMachine, + String stateName, + DisjointSet disjointSet, + Set visited, + Map> nextStateNameMap, + Stack currentPathWithoutCycles + ) { + State state = stateMachine.getState(stateName); + + if (visited.contains(stateName)) { + // If it has ever been visited before, means it is in a cycle + if (currentPathWithoutCycles.size() > 1) { + // Union all states in a cycle + int curr = currentPathWithoutCycles.size() - 1; + do { + disjointSet.union(currentPathWithoutCycles.get(curr), currentPathWithoutCycles.get(--curr)); + } while (!currentPathWithoutCycles.get(curr).equals(stateName)); + } + } else { + Set nextStateNames = StateMachineUtils.getAllPossibleSubsequentStates(state); + nextStateNameMap.put(stateName, nextStateNames); + + visited.add(stateName); + currentPathWithoutCycles.push(stateName); + for (String nextStateName: nextStateNames) { + iterate(stateMachine, nextStateName, disjointSet, visited, nextStateNameMap, currentPathWithoutCycles); + } + currentPathWithoutCycles.pop(); + visited.remove(stateName); + } + + } + + private static class DisjointSet { + Map parent = new HashMap<>(); + Map rank = new HashMap<>(); + + DisjointSet(Collection stateNames) { + for (String stateName : stateNames) { + parent.put(stateName, stateName); + rank.put(stateName, 0); + } + } + + String find(String state) { + if (parent.get(state).equals(state)) { + return state; + } + + String root = find(parent.get(state)); + parent.put(state, root); + return root; + } + + void union(String i, String j) { + String parentI = find(i), parentJ = find(j); + int rankI = rank.get(parentI), rankJ = rank.get(parentJ); + + if (!parentI.equals(parentJ)) { + + if (rankI > rankJ) { + parent.put(parentJ, parentI); + } else { + parent.put(parentI, parentJ); + if (rankI == rankJ) { + rank.put(parentI, rankI + 1); + } + } + } + } + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/NoRecursiveSubStateMachineRule.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/NoRecursiveSubStateMachineRule.java new file mode 100644 index 00000000000..821dd5b41c2 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/NoRecursiveSubStateMachineRule.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator.impl; + +import io.seata.saga.statelang.domain.DomainConstants; +import io.seata.saga.statelang.domain.State; +import io.seata.saga.statelang.domain.StateMachine; +import io.seata.saga.statelang.domain.SubStateMachine; + +/** + * Rule to check if all SubStateMachines has no recursive call + * + * @author ptyin + */ +public class NoRecursiveSubStateMachineRule extends AbstractRule { + + @Override + public boolean validate(StateMachine stateMachine) { + for (State state: stateMachine.getStates().values()) { + if (!DomainConstants.STATE_TYPE_SUB_STATE_MACHINE.equals(state.getType())) { + continue; + } + if (stateMachine.getName().equals(((SubStateMachine) state).getStateMachineName())) { + hint = String.format("SubStateMachine state [%s] call itself", state.getName()); + return false; + } + } + return true; + } +} diff --git a/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/StateNameExistsRule.java b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/StateNameExistsRule.java new file mode 100644 index 00000000000..51a95447c97 --- /dev/null +++ b/saga/seata-saga-statelang/src/main/java/io/seata/saga/statelang/validator/impl/StateNameExistsRule.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2019 Seata.io Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.seata.saga.statelang.validator.impl; + +import io.seata.saga.statelang.domain.State; +import io.seata.saga.statelang.domain.StateMachine; +import io.seata.saga.statelang.parser.utils.StateMachineUtils; + +import java.util.Set; + +/** + * Rule to check if all the state name exists + * + * @author ptyin + */ +public class StateNameExistsRule extends AbstractRule { + + @Override + public boolean validate(StateMachine stateMachine) { + for (State state: stateMachine.getStates().values()) { + Set subsequentStates = StateMachineUtils.getAllPossibleSubsequentStates(state); + for (String subsequentState: subsequentStates) { + if (stateMachine.getState(subsequentState) == null) { + hint = String.format("Subsequent state [%s] of [%s] does not exist", subsequentState, state); + return false; + } + } + } + return true; + } +} diff --git a/saga/seata-saga-statelang/src/main/resources/META-INF/services/io.seata.saga.statelang.validator.Rule b/saga/seata-saga-statelang/src/main/resources/META-INF/services/io.seata.saga.statelang.validator.Rule new file mode 100644 index 00000000000..7137ac1326d --- /dev/null +++ b/saga/seata-saga-statelang/src/main/resources/META-INF/services/io.seata.saga.statelang.validator.Rule @@ -0,0 +1,3 @@ +io.seata.saga.statelang.validator.impl.StateNameExistsRule +io.seata.saga.statelang.validator.impl.FiniteTerminationRule +io.seata.saga.statelang.validator.impl.NoRecursiveSubStateMachineRule \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/StateParserTests.java b/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/StateParserTests.java index bfe37507918..4f1edb489d3 100644 --- a/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/StateParserTests.java +++ b/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/StateParserTests.java @@ -21,6 +21,7 @@ import io.seata.saga.statelang.domain.StateMachine; import io.seata.saga.statelang.parser.utils.DesignerJsonTransformer; +import io.seata.saga.statelang.validator.ValidationException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; @@ -74,4 +75,47 @@ public void testDesignerJsonTransformer() throws IOException { String fastjsonOutputJson = fastjsonParser.toJsonString(fastjsonParsedObj, true); System.out.println(fastjsonOutputJson); } + + @Test + public void singleInfiniteLoopTest() throws IOException { + ClassPathResource resource = new ClassPathResource("statelang/simple_statemachine_with_single_infinite_loop.json"); + String json = io.seata.saga.statelang.parser.utils.IOUtils.toString(resource.getInputStream(), "UTF-8"); + Throwable e = Assertions.assertThrows(ValidationException.class, () -> { + StateMachineParserFactory.getStateMachineParser(null).parse(json); + }); + System.out.println(e.getMessage()); + Assertions.assertTrue(e.getMessage().endsWith("without outgoing flow to end")); + } + + @Test + public void testMultipleInfiniteLoop() throws IOException { + ClassPathResource resource = new ClassPathResource("statelang/simple_statemachine_with_multiple_infinite_loop.json"); + String json = io.seata.saga.statelang.parser.utils.IOUtils.toString(resource.getInputStream(), "UTF-8"); + Throwable e = Assertions.assertThrows(ValidationException.class, () -> { + StateMachineParserFactory.getStateMachineParser(null).parse(json); + }); + System.out.println(e.getMessage()); + Assertions.assertTrue(e.getMessage().endsWith("without outgoing flow to end")); + } + + @Test + public void testNonExistedName() throws IOException { + ClassPathResource resource = new ClassPathResource("statelang/simple_statemachine_with_non_existed_name.json"); + String json = io.seata.saga.statelang.parser.utils.IOUtils.toString(resource.getInputStream(), "UTF-8"); + Throwable e = Assertions.assertThrows(ValidationException.class, () -> { + StateMachineParserFactory.getStateMachineParser(null).parse(json); + }); + System.out.println(e.getMessage()); + Assertions.assertTrue(e.getMessage().endsWith("does not exist")); + } + + @Test + public void testRecursiveSubStateMachine() throws IOException { + ClassPathResource resource = new ClassPathResource("statelang/simple_statemachine_with_recursive_sub_machine.json"); + String json = io.seata.saga.statelang.parser.utils.IOUtils.toString(resource.getInputStream(), "UTF-8"); + Throwable e = Assertions.assertThrows(ValidationException.class, () -> { + StateMachineParserFactory.getStateMachineParser(null).parse(json); + }); + Assertions.assertTrue(e.getMessage().endsWith("call itself")); + } } \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/utils/ResourceUtilTests.java b/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/utils/ResourceUtilTests.java index 6db1ded8312..f1ec7b277c1 100644 --- a/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/utils/ResourceUtilTests.java +++ b/saga/seata-saga-statelang/src/test/java/io/seata/saga/statelang/parser/utils/ResourceUtilTests.java @@ -30,9 +30,9 @@ public class ResourceUtilTests { @Test public void getResources_test() { Resource[] resources = ResourceUtil.getResources("classpath*:statelang/*.json"); - assertThat(resources.length).isEqualTo(2); + assertThat(resources.length).isEqualTo(6); Resource[] resources2 = ResourceUtil.getResources(new String[]{"classpath*:statelang/*.json"}); - assertThat(resources2.length).isEqualTo(2); + assertThat(resources2.length).isEqualTo(6); } } \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine.json b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine.json index 417d3b5b8de..91ed501953f 100644 --- a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine.json +++ b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine.json @@ -112,7 +112,7 @@ }, "SecondMatchState": { "Type": "SubStateMachine", - "StateMachineName": "simpleTestStateMachine", + "StateMachineName": "simpleTestSubStateMachine", "Input": [ { "input": "$.data" diff --git a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_multiple_infinite_loop.json b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_multiple_infinite_loop.json new file mode 100644 index 00000000000..85d934a8439 --- /dev/null +++ b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_multiple_infinite_loop.json @@ -0,0 +1,33 @@ +{ + "Name": "simpleTestStateMachineWithMultipleInfiniteLoop", + "Comment": "测试状态机定义", + "StartState": "FirstState", + "Version": "0.0.2", + "States": { + "FirstState": { + "Type": "ServiceTask", + "Next": "ChoiceState" + }, + "ChoiceState": { + "Type": "Choice", + "Choices":[ + { + "Expression":"[a] == 1", + "Next":"SecondState" + }, + { + "Expression":"[a] == 2", + "Next":"ThirdState" + } + ] + }, + "SecondState": { + "Type": "ServiceTask", + "Next": "FirstState" + }, + "ThirdState": { + "Type": "ServiceTask", + "Next": "FirstState" + } + } +} \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_non_existed_name.json b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_non_existed_name.json new file mode 100644 index 00000000000..03ad391b4bf --- /dev/null +++ b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_non_existed_name.json @@ -0,0 +1,29 @@ +{ + "Name": "simpleTestStateMachineWithNonExistedName", + "Comment": "测试状态机定义", + "StartState": "FirstState", + "Version": "0.0.2", + "States": { + "FirstState": { + "Type": "ServiceTask", + "Next": "ChoiceState" + }, + "ChoiceState": { + "Type": "Choice", + "Choices":[ + { + "Expression":"[a] == 1", + "Next":"SecondState" + }, + { + "Expression":"[a] == 2", + "Next":"ThirdState" + } + ] + }, + "SecondState": { + "Type": "ServiceTask", + "Next": "FirstState" + } + } +} \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_recursive_sub_machine.json b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_recursive_sub_machine.json new file mode 100644 index 00000000000..56b566c17f9 --- /dev/null +++ b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_recursive_sub_machine.json @@ -0,0 +1,18 @@ +{ + "Name": "simpleStateMachineWithRecursiveSubMachine", + "Comment": "递归调用子状态机", + "StartState": "CallSubStateMachine", + "Version": "0.0.1", + "IsRetryPersistModeUpdate": false, + "IsCompensatePersistModeUpdate": false, + "States": { + "CallSubStateMachine": { + "Type": "SubStateMachine", + "StateMachineName": "simpleStateMachineWithRecursiveSubMachine", + "Next": "Succeed" + }, + "Succeed": { + "Type":"Succeed" + } + } +} \ No newline at end of file diff --git a/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_single_infinite_loop.json b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_single_infinite_loop.json new file mode 100644 index 00000000000..d2ff7c82d14 --- /dev/null +++ b/saga/seata-saga-statelang/src/test/resources/statelang/simple_statemachine_with_single_infinite_loop.json @@ -0,0 +1,20 @@ +{ + "Name": "simpleTestStateMachineWithSimpleInfiniteLoop", + "Comment": "测试状态机定义", + "StartState": "FirstState", + "Version": "0.0.2", + "States": { + "FirstState": { + "Type": "ServiceTask", + "ServiceName": "demoService", + "ServiceMethod": "foo", + "Next": "SecondState" + }, + "SecondState": { + "Type": "ServiceTask", + "ServiceName": "demoService", + "ServiceMethod": "bar", + "Next": "FirstState" + } + } +} \ No newline at end of file