Skip to content

Commit

Permalink
optimize: add saga statelang semantic validation (#5928)
Browse files Browse the repository at this point in the history
  • Loading branch information
ptyin authored Oct 24, 2023
1 parent 2626f24 commit f99a3dd
Show file tree
Hide file tree
Showing 20 changed files with 679 additions and 4 deletions.
1 change: 1 addition & 0 deletions changes/en-us/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions changes/zh-cn/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -124,6 +127,8 @@ public StateMachine parse(String json) {
}
}
}

validator.validate(stateMachine);
return stateMachine;
}

Expand All @@ -134,4 +139,4 @@ public String getJsonParserName() {
public void setJsonParserName(String jsonParserName) {
this.jsonParserName = jsonParserName;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> getAllPossibleSubsequentStates(State state) {
Set<String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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<Rule> RULES = EnhancedServiceLoader.loadAll(Rule.class);

public static List<Rule> getRules() {
return RULES;
}
}
Original file line number Diff line number Diff line change
@@ -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<Rule> 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");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit f99a3dd

Please sign in to comment.