diff --git a/liteflow-core/pom.xml b/liteflow-core/pom.xml
index 7a4403c32..d62bb4ae8 100644
--- a/liteflow-core/pom.xml
+++ b/liteflow-core/pom.xml
@@ -67,5 +67,9 @@
org.apache.commons
commons-text
+
+ com.github.ben-manes.caffeine
+ caffeine
+
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java b/liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java
index 628d7f70b..9f0001764 100644
--- a/liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java
@@ -28,6 +28,8 @@ import com.yomahub.liteflow.flow.element.Rollbackable;
import com.yomahub.liteflow.flow.entity.CmpStep;
import com.yomahub.liteflow.flow.id.IdGeneratorHolder;
import com.yomahub.liteflow.lifecycle.LifeCycleHolder;
+import com.yomahub.liteflow.lifecycle.PostProcessChainExecuteLifeCycle;
+import com.yomahub.liteflow.lifecycle.impl.ChainCacheLifeCycle;
import com.yomahub.liteflow.log.LFLog;
import com.yomahub.liteflow.log.LFLoggerManager;
import com.yomahub.liteflow.monitor.MonitorFile;
@@ -103,6 +105,12 @@ public class FlowExecutor {
IdGeneratorHolder.init();
}
+ // 规则缓存
+ if (isStart && liteflowConfig.getChainCacheEnabled()) {
+ // 放到解析节点后,是因为要根据节点数量判断缓存大小设置是否合理
+ initChainCache();
+ }
+
String ruleSource = liteflowConfig.getRuleSource();
if (StrUtil.isBlank(ruleSource)) {
// 查看有没有Parser的SPI实现
@@ -229,6 +237,11 @@ public class FlowExecutor {
}
}
+
+ // 初始化或reload时,评估规则缓存容量大小
+ if (liteflowConfig.getChainCacheEnabled()) {
+ evaluateChainCacheCapacity();
+ }
}
// 此方法就是从原有的配置源主动拉取新的进行刷新
@@ -664,4 +677,44 @@ public class FlowExecutor {
return resultSlotList;
}
+
+ private void initChainCache() {
+ // 启动chain缓存必须使用 PARSE_ONE_ON_FIRST_EXEC 模式
+ if (!ParseModeEnum.PARSE_ONE_ON_FIRST_EXEC.equals(liteflowConfig.getParseMode())) {
+ LOG.warn("The parse mode is not PARSE_ONE_ON_FIRST_EXE, so the chain cache cannot be enabled.");
+ return;
+ }
+ // 容量不能小于等于0
+ Integer capacity = liteflowConfig.getChainCacheCapacity();
+ if (ObjectUtil.isNull(capacity) || capacity <= 0) {
+ throw new ConfigErrorException("The chain cache capacity must be greater than 0");
+ }
+
+ // 添加规则缓存生命周期
+ List lifeCycleList = LifeCycleHolder.getPostProcessChainExecuteLifeCycleList();
+ boolean exist = lifeCycleList.stream()
+ .anyMatch(lifeCycle -> lifeCycle instanceof ChainCacheLifeCycle);
+ if (!exist) {
+ boolean success = ChainCacheLifeCycle.initIfAbsent(capacity);
+ if (!success) {
+ throw new FlowExecutorNotInitException("Initialization of ChainCacheLifeCycle failed");
+ }
+ LifeCycleHolder.addLifeCycle(ChainCacheLifeCycle.getLifeCycle());
+ }
+ }
+
+ // 评估规则缓存容量
+ private void evaluateChainCacheCapacity() {
+ if (!ParseModeEnum.PARSE_ONE_ON_FIRST_EXEC.equals(liteflowConfig.getParseMode())) {
+ return;
+ }
+ Integer capacity = liteflowConfig.getChainCacheCapacity();
+ // 容量不足chain总数的30%给予警告
+ int chainNum = FlowBus.getChainMap().size();
+ double threshold = chainNum * 0.3;
+ if (capacity < threshold) {
+ LOG.warn("The chain cache capacity {} is too small, the current total number of chains is {}, "
+ +"it is recommended to be greater than 30% of the number of chains", capacity, chainNum);
+ }
+ }
}
\ No newline at end of file
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/Chain.java b/liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/Chain.java
index aca03f3c6..80aa73bc8 100644
--- a/liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/Chain.java
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/Chain.java
@@ -10,6 +10,8 @@ package com.yomahub.liteflow.flow.element;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
import com.yomahub.liteflow.common.ChainConstant;
@@ -19,6 +21,7 @@ import com.yomahub.liteflow.exception.FlowSystemException;
import com.yomahub.liteflow.lifecycle.LifeCycleHolder;
import com.yomahub.liteflow.log.LFLog;
import com.yomahub.liteflow.log.LFLoggerManager;
+import com.yomahub.liteflow.meta.LiteflowMetaOperator;
import com.yomahub.liteflow.slot.DataBus;
import com.yomahub.liteflow.slot.Slot;
@@ -31,6 +34,7 @@ import java.util.List;
* @author Bryan.Zhang
* @author jason
* @author luo yi
+ * @author DaleLee
*/
public class Chain implements Executable {
@@ -115,8 +119,15 @@ public class Chain implements Executable {
}
}
- if (CollUtil.isEmpty(conditionList)) {
- throw new FlowSystemException("no conditionList in this chain[" + chainId + "]");
+ // 这里先拿到this.conditionList的引用
+ // 因为在正式执行condition之前,this.conditionList有可能被其他线程置空
+ // 比如,该chain在规则缓存中被淘汰
+ List conditionListRef = this.conditionList;
+ // 但在编译后到拿到引用之前,this.conditionList还是有可能已经被置空了
+ if (CollUtil.isEmpty(conditionListRef)) {
+ // 如果conditionListRef为空,
+ // 尝试构建临时conditionList确保本次一定可以执行
+ conditionListRef = buildTemporaryConditionList();
}
Slot slot = DataBus.getSlot(slotIndex);
try {
@@ -131,7 +142,7 @@ public class Chain implements Executable {
slot.setChainId(chainId);
slot.addChainInstance(this);
// 执行主体Condition
- for (Condition condition : conditionList) {
+ for (Condition condition : conditionListRef) {
condition.setCurrChainId(chainId);
condition.execute(slotIndex);
}
@@ -258,4 +269,28 @@ public class Chain implements Executable {
public void setElMd5(String elMd5) {
this.elMd5 = elMd5;
}
+
+ // 构建临时的ConditionList
+ private List buildTemporaryConditionList() {
+ if (StrUtil.isBlank(el)) {
+ // 无法构建
+ throw new FlowSystemException("no conditionList in this chain[" + chainId + "]");
+ }
+ // 构建临时chain
+ String tempChainId = chainId + "_temp_" + IdUtil.simpleUUID();
+ // 使用LiteFlowChainELBuilder创建chain,为了设置md5
+ LiteFlowChainELBuilder.createChain()
+ .setChainId(tempChainId)
+ .setEL(el)
+ .build();
+ // 当前模式可能为PARSE_ONE_ON_FIRST_EXEC,所以临时chain可能未编译
+ Chain tempChain = LiteflowMetaOperator.getChain(tempChainId);
+ LiteFlowChainELBuilder.buildUnCompileChain(tempChain);
+ // 移除临时chain
+ LiteflowMetaOperator.removeChain(tempChainId);
+ // 打印警告,可用于排查临时chain与已有chain重名(几乎不可能发生)而将已有chain覆盖的情况
+ LOG.warn("The conditionList of chain[{}] is empty, " +
+ "temporarily using chain[{}] (now removed) to build it.", chainId, tempChainId);
+ return tempChain.getConditionList();
+ }
}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/impl/ChainCacheLifeCycle.java b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/impl/ChainCacheLifeCycle.java
new file mode 100644
index 000000000..5bc68e3a0
--- /dev/null
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/lifecycle/impl/ChainCacheLifeCycle.java
@@ -0,0 +1,165 @@
+package com.yomahub.liteflow.lifecycle.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.RemovalCause;
+import com.github.benmanes.caffeine.cache.RemovalListener;
+import com.yomahub.liteflow.flow.element.Chain;
+import com.yomahub.liteflow.flow.element.Condition;
+import com.yomahub.liteflow.lifecycle.PostProcessChainExecuteLifeCycle;
+import com.yomahub.liteflow.meta.LiteflowMetaOperator;
+import com.yomahub.liteflow.slot.Slot;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+
+/**
+ * Chain 缓存生命周期
+ * @author DaleLee
+ * @since 2.13.3
+ */
+public class ChainCacheLifeCycle implements PostProcessChainExecuteLifeCycle {
+ /**
+ * 缓存
+ */
+ private final Cache cache;
+
+ /**
+ * 实例
+ */
+ private static ChainCacheLifeCycle INSTANCE;
+
+ private ChainCacheLifeCycle(int capacity) {
+ this.cache = Caffeine.newBuilder()
+ .maximumSize(capacity)
+ .evictionListener(new ChainRemovalListener())
+ .build();
+ }
+
+ @Override
+ public void postProcessBeforeChainExecute(String chainId, Slot slot) {
+ // 记录 chainId 在缓存中
+ // 初始状态为 ACTIVE
+ cache.get(chainId, key -> ChainState.newActiveState());
+ }
+
+ @Override
+ public void postProcessAfterChainExecute(String chainId, Slot slot) {
+ // 检测是否有不在缓存中、或处于非活跃状态而未被清理的 Chain
+ if (!isActive(chainId) && !isCleaned(chainId)) {
+ cleanChain(chainId);
+ }
+ }
+
+ /**
+ * Chain 在缓存中状态
+ */
+ public static class ChainState {
+ /**
+ * Chain 活跃状态
+ */
+ private volatile boolean active;
+
+ public ChainState(boolean active) {
+ this.active = active;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public static ChainState newActiveState() {
+ return new ChainState(true);
+ }
+ }
+
+ /**
+ * 监听在缓存中被移除的 Chain
+ */
+ private static class ChainRemovalListener implements RemovalListener {
+
+ @Override
+ public void onRemoval(@Nullable String chainId, @Nullable ChainState chainState, @NonNull RemovalCause removalCause) {
+ if (ObjectUtil.isNotNull(chainState)) {
+ chainState.setActive(false);
+ }
+ cleanChain(chainId);
+ }
+ }
+
+ /**
+ * 获取缓存
+ * @return cache
+ */
+ public Cache getCache() {
+ return cache;
+ }
+
+ /**
+ * 判断 Chain 的 Condition 是否被清理
+ * @param chainId chainId
+ * @return 被清理返回 true,否则返回 false
+ */
+ private boolean isCleaned(String chainId) {
+ Chain chain = LiteflowMetaOperator.getChain(chainId);
+ if (ObjectUtil.isNull(chain)) {
+ return true;
+ }
+ List conditionList = chain.getConditionList();
+ return CollUtil.isEmpty(conditionList);
+ }
+
+ /**
+ * 判断 Chain 在缓存中是活跃状态
+ * @param chainId chainId
+ * @return 活跃状态返回 true,不在缓存中或处于非活状态返回 false
+ */
+ private boolean isActive(String chainId) {
+ ChainState chainState = cache.getIfPresent(chainId);
+ return ObjectUtil.isNotNull(chainState)
+ && chainState.isActive();
+ }
+
+ /**
+ * 清理 Chain 的 Condition
+ * @param chainId chainId
+ */
+ private static void cleanChain(String chainId) {
+ Chain chain = LiteflowMetaOperator.getChain(chainId);
+ // chain可能已经在FlowBus中被移除了
+ if (ObjectUtil.isNull(chain)) {
+ return;
+ }
+ // 将chain设置为未编译并清空condition
+ chain.setCompiled(false);
+ chain.setConditionList(null);
+ }
+
+ /**
+ * 初始化生命周期实例
+ * @param capacity 缓存容量
+ * @return 成功 true,失败返回 false
+ */
+ public synchronized static boolean initIfAbsent(int capacity) {
+ if (ObjectUtil.isNull(INSTANCE)) {
+ INSTANCE = new ChainCacheLifeCycle(capacity);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 获取生命周期实例
+ * @return lifeCycle
+ */
+ public static ChainCacheLifeCycle getLifeCycle() {
+ return INSTANCE;
+ }
+}
diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/LiteflowConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/LiteflowConfig.java
index c59944917..859b6b1ef 100644
--- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/LiteflowConfig.java
+++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/LiteflowConfig.java
@@ -116,6 +116,12 @@ public class LiteflowConfig {
// instance id 生成器
private String instanceIdGeneratorClass;
+ // 是否启用chain缓存
+ private Boolean chainCacheEnabled;
+
+ // chain缓存容量
+ private Integer chainCacheCapacity;
+
public Boolean getEnableMonitorFile() {
return enableMonitorFile;
}
@@ -495,4 +501,26 @@ public class LiteflowConfig {
public void setInstanceIdGeneratorClass(String instanceIdGeneratorClass) {
this.instanceIdGeneratorClass = instanceIdGeneratorClass;
}
+
+ public Boolean getChainCacheEnabled() {
+ if (ObjectUtil.isNull(chainCacheEnabled)) {
+ return Boolean.FALSE;
+ }
+ return chainCacheEnabled;
+ }
+
+ public void setChainCacheEnabled(Boolean chainCacheEnabled) {
+ this.chainCacheEnabled = chainCacheEnabled;
+ }
+
+ public Integer getChainCacheCapacity() {
+ if (ObjectUtil.isNull(chainCacheCapacity)) {
+ return 10000;
+ }
+ return chainCacheCapacity;
+ }
+
+ public void setChainCacheCapacity(Integer chainCacheCapacity) {
+ this.chainCacheCapacity = chainCacheCapacity;
+ }
}
diff --git a/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowAutoConfiguration.java b/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowAutoConfiguration.java
index f0e81d247..36c5cbae7 100644
--- a/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowAutoConfiguration.java
+++ b/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowAutoConfiguration.java
@@ -51,6 +51,8 @@ public class LiteflowAutoConfiguration {
liteflowConfig.setGlobalThreadPoolQueueSize(property.getGlobalThreadPoolQueueSize());
liteflowConfig.setWhenThreadPoolIsolate(property.getWhenThreadPoolIsolate());
liteflowConfig.setEnableNodeInstanceId(property.isEnableNodeInstanceId());
+ liteflowConfig.setChainCacheEnabled(property.getChainCache().isEnabled());
+ liteflowConfig.setChainCacheCapacity(property.getChainCache().getCapacity());
return liteflowConfig;
}
diff --git a/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowProperty.java b/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowProperty.java
index cca6b3542..f6aa0e50c 100644
--- a/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowProperty.java
+++ b/liteflow-solon-plugin/src/main/java/com/yomahub/liteflow/solon/config/LiteflowProperty.java
@@ -101,6 +101,33 @@ public class LiteflowProperty {
//是否启用节点实例ID
private boolean enableNodeInstanceId;
+ // 规则缓存配置
+ private ChainCacheProperty chainCache;
+
+ public static class ChainCacheProperty {
+ // 是否启用规则缓存
+ private Boolean enabled;
+
+ // 规则缓存容量
+ private Integer capacity;
+
+ public Boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Integer getCapacity() {
+ return capacity;
+ }
+
+ public void setCapacity(Integer capacity) {
+ this.capacity = capacity;
+ }
+ }
+
public boolean isEnable() {
return enable;
}
@@ -341,4 +368,12 @@ public class LiteflowProperty {
public void setEnableNodeInstanceId(boolean enableNodeInstanceId) {
this.enableNodeInstanceId = enableNodeInstanceId;
}
+
+ public ChainCacheProperty getChainCache() {
+ return chainCache;
+ }
+
+ public void setChainCache(ChainCacheProperty chainCache) {
+ this.chainCache = chainCache;
+ }
}
diff --git a/liteflow-solon-plugin/src/main/resources/META-INF/liteflow-default.properties b/liteflow-solon-plugin/src/main/resources/META-INF/liteflow-default.properties
index 0f6f6dc8c..820d04aba 100644
--- a/liteflow-solon-plugin/src/main/resources/META-INF/liteflow-default.properties
+++ b/liteflow-solon-plugin/src/main/resources/META-INF/liteflow-default.properties
@@ -18,4 +18,6 @@ liteflow.fallback-cmp-enable=false
liteflow.global-thread-pool-size=16
liteflow.global-thread-pool-queue-size=512
liteflow.global-thread-pool-executor-class=com.yomahub.liteflow.thread.LiteFlowDefaultGlobalExecutorBuilder
-liteflow.enable-node-instance-id=false
\ No newline at end of file
+liteflow.enable-node-instance-id=false
+liteflow.chain-cache.enabled=false
+liteflow.chain-cache.capacity=10000
diff --git a/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/LiteflowProperty.java b/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/LiteflowProperty.java
index 48b0b5e6c..eff05a112 100644
--- a/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/LiteflowProperty.java
+++ b/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/LiteflowProperty.java
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.enums.ParseModeEnum;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -99,6 +100,34 @@ public class LiteflowProperty {
//是否启用节点实例ID
private boolean enableNodeInstanceId;
+ // 规则缓存配置
+ @NestedConfigurationProperty
+ private ChainCacheProperty chainCache;
+
+ public static class ChainCacheProperty {
+ // 是否启用规则缓存
+ private Boolean enabled;
+
+ // chain缓存容量
+ private Integer capacity;
+
+ public Boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Integer getCapacity() {
+ return capacity;
+ }
+
+ public void setCapacity(Integer capacity) {
+ this.capacity = capacity;
+ }
+ }
+
public boolean isEnableMonitorFile() {
return enableMonitorFile;
}
@@ -330,4 +359,12 @@ public class LiteflowProperty {
public void setEnableNodeInstanceId(boolean enableNodeInstanceId) {
this.enableNodeInstanceId = enableNodeInstanceId;
}
+
+ public ChainCacheProperty getChainCache() {
+ return chainCache;
+ }
+
+ public void setChainCache(ChainCacheProperty chainCache) {
+ this.chainCache = chainCache;
+ }
}
diff --git a/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/config/LiteflowPropertyAutoConfiguration.java b/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/config/LiteflowPropertyAutoConfiguration.java
index 4a318e740..26a28059a 100644
--- a/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/config/LiteflowPropertyAutoConfiguration.java
+++ b/liteflow-spring-boot-starter/src/main/java/com/yomahub/liteflow/springboot/config/LiteflowPropertyAutoConfiguration.java
@@ -53,6 +53,8 @@ public class LiteflowPropertyAutoConfiguration {
liteflowConfig.setGlobalThreadPoolQueueSize(property.getGlobalThreadPoolQueueSize());
liteflowConfig.setGlobalThreadPoolSize(property.getGlobalThreadPoolSize());
liteflowConfig.setEnableNodeInstanceId(property.isEnableNodeInstanceId());
+ liteflowConfig.setChainCacheEnabled(property.getChainCache().isEnabled());
+ liteflowConfig.setChainCacheCapacity(property.getChainCache().getCapacity());
return liteflowConfig;
}
diff --git a/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index e1a898cb7..615533078 100644
--- a/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/liteflow-spring-boot-starter/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -206,6 +206,19 @@
"description": "enable node instance id",
"sourceType": "com.yomahub.liteflow.springboot.LiteflowProperty",
"defaultValue": false
+ },
+ {
+ "name": "liteflow.rule-cache.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Enable rule cache.",
+ "sourceType": "com.yomahub.liteflow.springboot.LiteflowProperty",
+ "defaultValue": false
+ },
+ {
+ "name": "liteflow.rule-cache.capacity",
+ "type": "java.lang.Integer",
+ "description": "Set rule cache capacity.",
+ "sourceType": "com.yomahub.liteflow.springboot.LiteflowProperty"
}
]
}
\ No newline at end of file
diff --git a/liteflow-spring-boot-starter/src/main/resources/META-INF/liteflow-default.properties b/liteflow-spring-boot-starter/src/main/resources/META-INF/liteflow-default.properties
index 3a159577f..ca67a1d37 100644
--- a/liteflow-spring-boot-starter/src/main/resources/META-INF/liteflow-default.properties
+++ b/liteflow-spring-boot-starter/src/main/resources/META-INF/liteflow-default.properties
@@ -24,4 +24,7 @@ liteflow.global-thread-pool-size=64
liteflow.global-thread-pool-queue-size=512
liteflow.global-thread-pool-executor-class=com.yomahub.liteflow.thread.LiteFlowDefaultGlobalExecutorBuilder
liteflow.enable-node-instance-id=false
+liteflow.chain-cache.enabled=false
+liteflow.chain-cache.capacity=10000
+
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheTest.java b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheTest.java
new file mode 100644
index 000000000..6355deaec
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheTest.java
@@ -0,0 +1,237 @@
+package com.yomahub.liteflow.test.chainCache;
+
+import cn.hutool.core.collection.CollUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
+import com.yomahub.liteflow.core.FlowExecutor;
+import com.yomahub.liteflow.core.FlowExecutorHolder;
+import com.yomahub.liteflow.enums.ParseModeEnum;
+import com.yomahub.liteflow.exception.ChainNotFoundException;
+import com.yomahub.liteflow.flow.FlowBus;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.flow.element.Chain;
+import com.yomahub.liteflow.flow.element.Condition;
+import com.yomahub.liteflow.lifecycle.impl.ChainCacheLifeCycle;
+import com.yomahub.liteflow.property.LiteflowConfig;
+import com.yomahub.liteflow.test.BaseTest;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * 非Spring环境下的chain缓存测试
+ * @author DaleLee
+ */
+public class ChainCacheTest extends BaseTest {
+
+ private static FlowExecutor flowExecutor;
+
+ @BeforeAll
+ public static void init() {
+ LiteflowConfig config = new LiteflowConfig();
+ config.setRuleSource("chainCache/flow.el.xml");
+ config.setChainCacheEnabled(true);
+ config.setChainCacheCapacity(5);
+ config.setParseMode(ParseModeEnum.PARSE_ONE_ON_FIRST_EXEC);
+ flowExecutor = FlowExecutorHolder.loadInstance(config);
+ }
+
+ @BeforeEach
+ public void reload() {
+ flowExecutor.reloadRule();
+ // 清空缓存
+ Cache cache = getCache();
+ cache.invalidateAll();
+ cache.cleanUp();
+ }
+
+ // 测试chain被淘汰
+ @Test
+ public void testChainCache1() {
+ // 加满缓存
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain6", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("c==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // 测试被淘汰的chain仍可正常执行
+ response = flowExecutor.execute2Resp(chainId, "arg");
+ Assertions.assertTrue(response.isSuccess());
+ }
+
+ // 测试缓存数量上限(串行)
+ @Test
+ public void testChainCache2() {
+ // 确保至少执行过5个不同的chain
+ loadCache();
+ // 随机执行chain
+ Random random = new Random();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ flowExecutor.execute2Resp("chain" + id);
+ }
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试缓存数量上限(并行)
+ @Test
+ public void testChainCache3() {
+ loadCache();
+ Random random = new Random();
+ List> futureList = CollUtil.newArrayList();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ Future future = flowExecutor.execute2Future("chain" + id, "arg");
+ futureList.add(future);
+ }
+ futureList.forEach(future -> {
+ try {
+ LiteflowResponse response = future.get();
+ Assertions.assertTrue(response.isSuccess());
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain可以正常被更新
+ @Test
+ public void testChainCache4() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 更新chain7
+ LiteFlowChainELBuilder
+ .createChain()
+ .setChainId("chain7")
+ .setEL("THEN(a, b, c)")
+ .build();
+ // 重新执行chain7
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b==>c", response.getExecuteStepStr());
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain被移除后无法执行
+ @Test
+ public void testChainCache5() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 手动移除chain7
+ FlowBus.removeChain("chain7");
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertFalse(response.isSuccess());
+ Assertions.assertEquals(ChainNotFoundException.class, response.getCause().getClass());
+ }
+
+ // 测试并发下,正在执行的chain的condition被清理但仍能执行
+ @Test
+ public void testChainCache6() throws InterruptedException {
+ // 模拟清空编译好的chain
+ Thread thread = new Thread(()-> {
+ Chain chain1 = FlowBus.getChain("chain1");
+ // 绕过初次编译
+ chain1.setCompiled(true);
+ chain1.setConditionList(null);
+ });
+ thread.start();
+ thread.join();
+ LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b", response.getExecuteStepStr());
+ }
+
+ // 加载缓存, chain1~chain5
+ private void loadCache() {
+ // 容量上限为5
+ for (int i = 1; i <= 5; i++) {
+ flowExecutor.execute2Resp("chain" + i);
+ }
+ }
+
+ // 测试 chain 被淘汰
+ private void testEvicted(String chanId) {
+ Chain chain = FlowBus.getChain(chanId);
+ getCache().cleanUp();
+ // 测试缓存中不存在
+ Assertions.assertFalse(getCache().asMap().containsKey(chanId));
+ // 测试chain被设置为未编译
+ Assertions.assertFalse(chain.isCompiled());
+ Assertions.assertNull(chain.getConditionList());
+ }
+
+ public Cache getCache() {
+ return ChainCacheLifeCycle.getLifeCycle().getCache();
+ }
+
+ // 获得淘汰的chain,传入淘汰前的chain集合
+ // 确保只有一个被淘汰时使用
+ String getEvictedChain(Set set) {
+ Cache cache = getCache();
+ cache.cleanUp();
+ Set<@NonNull String> strings = cache.asMap().keySet();
+ set.removeAll(strings);
+ Assertions.assertEquals(1, set.size());
+ return set.iterator().next();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
new file mode 100644
index 000000000..8d6b10e39
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
@@ -0,0 +1,18 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+
+public class ACmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("ACmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
new file mode 100644
index 000000000..7e7cc3913
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
@@ -0,0 +1,19 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+
+public class BCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("BCmp executed!");
+ }
+
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
new file mode 100644
index 000000000..4c828a4b9
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
@@ -0,0 +1,18 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+
+public class CCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("CCmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
new file mode 100644
index 000000000..d4c343a9b
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
@@ -0,0 +1,10 @@
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeBooleanComponent;
+
+public class XCmp extends NodeBooleanComponent {
+ @Override
+ public boolean processBoolean() throws Exception {
+ return true;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/resources/chainCache/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/resources/chainCache/flow.el.xml
new file mode 100644
index 000000000..7f247d576
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-nospring/src/test/resources/chainCache/flow.el.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+ THEN(a, b);
+
+
+
+ THEN(a, c);
+
+
+
+ THEN(b, a);
+
+
+
+ THEN(b, c);
+
+
+
+ THEN(c, a);
+
+
+
+ THEN(c, b);
+
+
+
+ THEN(IF(x, a), b);
+
+
+
+ THEN(IF(x, a), c);
+
+
+
+ WHEN(a, b, c);
+
+
+
+ FOR(5).DO(THEN(a, b, c));
+
+
+
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-redis-springboot/src/test/resources/redis/application-poll-cluster-xml.properties b/liteflow-testcase-el/liteflow-testcase-el-redis-springboot/src/test/resources/redis/application-poll-cluster-xml.properties
index 6153f5d55..657442292 100644
--- a/liteflow-testcase-el/liteflow-testcase-el-redis-springboot/src/test/resources/redis/application-poll-cluster-xml.properties
+++ b/liteflow-testcase-el/liteflow-testcase-el-redis-springboot/src/test/resources/redis/application-poll-cluster-xml.properties
@@ -4,6 +4,7 @@ liteflow.rule-source-ext-data={\
"pollingInterval":1,\
"pollingStartTime":2,\
"chainKey":"pollChainKey",\
- "scriptKey":"pollScriptKey"\
+ "scriptKey":"pollScriptKey",\
+ "scriptDataBase":1\
}
liteflow.parse-mode=PARSE_ALL_ON_FIRST_EXEC
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSolonTest.java b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSolonTest.java
new file mode 100644
index 000000000..f64c5fc4e
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSolonTest.java
@@ -0,0 +1,229 @@
+package com.yomahub.liteflow.test.chainCache;
+
+import cn.hutool.core.collection.CollUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
+import com.yomahub.liteflow.core.FlowExecutor;
+import com.yomahub.liteflow.exception.ChainNotFoundException;
+import com.yomahub.liteflow.flow.FlowBus;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.flow.element.Chain;
+import com.yomahub.liteflow.flow.element.Condition;
+import com.yomahub.liteflow.lifecycle.impl.ChainCacheLifeCycle;
+import com.yomahub.liteflow.test.BaseTest;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.noear.solon.annotation.Import;
+import org.noear.solon.annotation.Inject;
+import org.noear.solon.test.SolonTest;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Solon环境下chain缓存测试
+ * @author DaleLee
+ */
+@SolonTest
+@Import(profiles="classpath:/chainCache/application.properties")
+public class ChainCacheSolonTest extends BaseTest {
+
+ @Inject
+ private FlowExecutor flowExecutor;
+
+ @BeforeEach
+ public void reload() {
+ flowExecutor.reloadRule();
+ // 清空缓存
+ Cache cache = getCache();
+ cache.invalidateAll();
+ cache.cleanUp();
+ }
+
+ // 测试chain被淘汰
+ @Test
+ public void testChainCache1() {
+ // 加满缓存
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain6", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("c==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // 测试被淘汰的chain仍可正常执行
+ response = flowExecutor.execute2Resp(chainId, "arg");
+ Assertions.assertTrue(response.isSuccess());
+ }
+
+ // 测试缓存数量上限(串行)
+ @Test
+ public void testChainCache2() {
+ // 确保至少执行过5个不同的chain
+ loadCache();
+ // 随机执行chain
+ Random random = new Random();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ flowExecutor.execute2Resp("chain" + id);
+ }
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试缓存数量上限(并行)
+ @Test
+ public void testChainCache3() {
+ loadCache();
+ Random random = new Random();
+ List> futureList = CollUtil.newArrayList();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ Future future = flowExecutor.execute2Future("chain" + id, "arg");
+ futureList.add(future);
+ }
+ futureList.forEach(future -> {
+ try {
+ LiteflowResponse response = future.get();
+ Assertions.assertTrue(response.isSuccess());
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain可以正常被更新
+ @Test
+ public void testChainCache4() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 更新chain7
+ LiteFlowChainELBuilder
+ .createChain()
+ .setChainId("chain7")
+ .setEL("THEN(a, b, c)")
+ .build();
+ // 重新执行chain7
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b==>c", response.getExecuteStepStr());
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain被移除后无法执行
+ @Test
+ public void testChainCache5() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 手动移除chain7
+ FlowBus.removeChain("chain7");
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertFalse(response.isSuccess());
+ Assertions.assertEquals(ChainNotFoundException.class, response.getCause().getClass());
+ }
+
+ // 测试并发下,正在执行的chain的condition被清理但仍能执行
+ @Test
+ public void testChainCache6() throws InterruptedException {
+ // 模拟清空编译好的chain
+ Thread thread = new Thread(()-> {
+ Chain chain1 = FlowBus.getChain("chain1");
+ // 绕过初次编译
+ chain1.setCompiled(true);
+ chain1.setConditionList(null);
+ });
+ thread.start();
+ thread.join();
+ LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b", response.getExecuteStepStr());
+ }
+
+ // 加载缓存, chain1~chain5
+ private void loadCache() {
+ // 容量上限为5
+ for (int i = 1; i <= 5; i++) {
+ flowExecutor.execute2Resp("chain" + i);
+ }
+ }
+
+ // 测试 chain 被淘汰
+ private void testEvicted(String chanId) {
+ Chain chain = FlowBus.getChain(chanId);
+ getCache().cleanUp();
+ // 测试缓存中不存在
+ Assertions.assertFalse(getCache().asMap().containsKey(chanId));
+ // 测试chain被设置为未编译
+ Assertions.assertFalse(chain.isCompiled());
+ Assertions.assertNull(chain.getConditionList());
+ }
+
+ public Cache getCache() {
+ return ChainCacheLifeCycle.getLifeCycle().getCache();
+ }
+
+ // 获得淘汰的chain,传入淘汰前的chain集合
+ // 确保只有一个被淘汰时使用
+ String getEvictedChain(Set set) {
+ Cache cache = getCache();
+ cache.cleanUp();
+ Set<@NonNull String> strings = cache.asMap().keySet();
+ set.removeAll(strings);
+ Assertions.assertEquals(1, set.size());
+ return set.iterator().next();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
new file mode 100644
index 000000000..b45ae448d
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.noear.solon.annotation.Component;
+
+@Component("a")
+public class ACmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("ACmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
new file mode 100644
index 000000000..73c4d630d
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
@@ -0,0 +1,21 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.noear.solon.annotation.Component;
+
+@Component("b")
+public class BCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("BCmp executed!");
+ }
+
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
new file mode 100644
index 000000000..033821755
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.noear.solon.annotation.Component;
+
+@Component("c")
+public class CCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("CCmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
new file mode 100644
index 000000000..fddd1a000
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
@@ -0,0 +1,12 @@
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeBooleanComponent;
+import org.noear.solon.annotation.Component;
+
+@Component("x")
+public class XCmp extends NodeBooleanComponent {
+ @Override
+ public boolean processBoolean() throws Exception {
+ return true;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/application.properties b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/application.properties
new file mode 100644
index 000000000..2f9c44ccc
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/application.properties
@@ -0,0 +1,4 @@
+liteflow.rule-source=chainCache/flow.el.xml
+liteflow.chain-cache.enabled=true
+liteflow.chain-cache.capacity=5
+liteflow.parse-mode=PARSE_ONE_ON_FIRST_EXEC
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/flow.el.xml
new file mode 100644
index 000000000..11acd1cd4
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-solon/src/test/resources/chainCache/flow.el.xml
@@ -0,0 +1,43 @@
+
+
+
+ THEN(a, b);
+
+
+
+ THEN(a, c);
+
+
+
+ THEN(b, a);
+
+
+
+ THEN(b, c);
+
+
+
+ THEN(c, a);
+
+
+
+ THEN(c, b);
+
+
+
+ THEN(IF(x, a), b);
+
+
+
+ THEN(IF(x, a), c);
+
+
+
+ WHEN(a, b, c);
+
+
+
+ FOR(5).DO(THEN(a, b, c));
+
+
+
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringbootTest.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringbootTest.java
new file mode 100644
index 000000000..31e6e5139
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringbootTest.java
@@ -0,0 +1,232 @@
+package com.yomahub.liteflow.test.chainCache;
+
+import cn.hutool.core.collection.CollUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
+import com.yomahub.liteflow.core.FlowExecutor;
+import com.yomahub.liteflow.exception.ChainNotFoundException;
+import com.yomahub.liteflow.flow.FlowBus;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.flow.element.Chain;
+import com.yomahub.liteflow.flow.element.Condition;
+import com.yomahub.liteflow.lifecycle.impl.ChainCacheLifeCycle;
+import com.yomahub.liteflow.test.BaseTest;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.context.TestPropertySource;
+
+import javax.annotation.Resource;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Springboot环境下chain缓存测试
+ * @author DaleLee
+ */
+@TestPropertySource(value = "classpath:/chainCache/application.properties")
+@SpringBootTest(classes = ChainCacheSpringbootTest.class)
+@EnableAutoConfiguration
+@ComponentScan({"com.yomahub.liteflow.test.chainCache.cmp"})
+public class ChainCacheSpringbootTest extends BaseTest {
+ @Resource
+ private FlowExecutor flowExecutor;
+
+ @BeforeEach
+ public void reload() {
+ flowExecutor.reloadRule();
+ // 清空缓存
+ Cache cache = getCache();
+ cache.invalidateAll();
+ cache.cleanUp();
+ }
+
+ // 测试chain被淘汰
+ @Test
+ public void testChainCache1() {
+ // 加满缓存
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain6", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("c==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // 测试被淘汰的chain仍可正常执行
+ response = flowExecutor.execute2Resp(chainId, "arg");
+ Assertions.assertTrue(response.isSuccess());
+ }
+
+ // 测试缓存数量上限(串行)
+ @Test
+ public void testChainCache2() {
+ // 确保至少执行过5个不同的chain
+ loadCache();
+ // 随机执行chain
+ Random random = new Random();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ flowExecutor.execute2Resp("chain" + id);
+ }
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试缓存数量上限(并行)
+ @Test
+ public void testChainCache3() {
+ loadCache();
+ Random random = new Random();
+ List> futureList = CollUtil.newArrayList();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ Future future = flowExecutor.execute2Future("chain" + id, "arg");
+ futureList.add(future);
+ }
+ futureList.forEach(future -> {
+ try {
+ LiteflowResponse response = future.get();
+ Assertions.assertTrue(response.isSuccess());
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain可以正常被更新
+ @Test
+ public void testChainCache4() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 更新chain7
+ LiteFlowChainELBuilder
+ .createChain()
+ .setChainId("chain7")
+ .setEL("THEN(a, b, c)")
+ .build();
+ // 重新执行chain7
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b==>c", response.getExecuteStepStr());
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain被移除后无法执行
+ @Test
+ public void testChainCache5() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 手动移除chain7
+ FlowBus.removeChain("chain7");
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertFalse(response.isSuccess());
+ Assertions.assertEquals(ChainNotFoundException.class, response.getCause().getClass());
+ }
+
+ // 测试并发下,正在执行的chain的condition被清理但仍能执行
+ @Test
+ public void testChainCache6() throws InterruptedException {
+ // 模拟清空编译好的chain
+ Thread thread = new Thread(()-> {
+ Chain chain1 = FlowBus.getChain("chain1");
+ // 绕过初次编译
+ chain1.setCompiled(true);
+ chain1.setConditionList(null);
+ });
+ thread.start();
+ thread.join();
+ LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b", response.getExecuteStepStr());
+ }
+
+ // 加载缓存, chain1~chain5
+ private void loadCache() {
+ // 容量上限为5
+ for (int i = 1; i <= 5; i++) {
+ flowExecutor.execute2Resp("chain" + i);
+ }
+ }
+
+ // 测试 chain 被淘汰
+ private void testEvicted(String chanId) {
+ Chain chain = FlowBus.getChain(chanId);
+ getCache().cleanUp();
+ // 测试缓存中不存在
+ Assertions.assertFalse(getCache().asMap().containsKey(chanId));
+ // 测试chain被设置为未编译
+ Assertions.assertFalse(chain.isCompiled());
+ Assertions.assertNull(chain.getConditionList());
+ }
+
+ public Cache getCache() {
+ return ChainCacheLifeCycle.getLifeCycle().getCache();
+ }
+
+ // 获得淘汰的chain,传入淘汰前的chain集合
+ // 确保只有一个被淘汰时使用
+ String getEvictedChain(Set set) {
+ Cache cache = getCache();
+ cache.cleanUp();
+ Set<@NonNull String> strings = cache.asMap().keySet();
+ set.removeAll(strings);
+ Assertions.assertEquals(1, set.size());
+ return set.iterator().next();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
new file mode 100644
index 000000000..d943cb205
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("a")
+public class ACmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("ACmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
new file mode 100644
index 000000000..356750126
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
@@ -0,0 +1,21 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("b")
+public class BCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("BCmp executed!");
+ }
+
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
new file mode 100644
index 000000000..84dedc927
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("c")
+public class CCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("CCmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
new file mode 100644
index 000000000..1f671e9e4
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
@@ -0,0 +1,12 @@
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeBooleanComponent;
+import org.springframework.stereotype.Component;
+
+@Component("x")
+public class XCmp extends NodeBooleanComponent {
+ @Override
+ public boolean processBoolean() throws Exception {
+ return true;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/subflow/SubflowSpringbootTest.java b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/subflow/SubflowSpringbootTest.java
index dcb777def..25583f539 100644
--- a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/subflow/SubflowSpringbootTest.java
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/java/com/yomahub/liteflow/test/subflow/SubflowSpringbootTest.java
@@ -36,7 +36,7 @@ public class SubflowSpringbootTest extends BaseTest {
public void testSubflow1() throws Exception {
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assertions.assertTrue(response.isSuccess());
- Assertions.assertTrue(ListUtil.toList("a==>c==>d==>b","a==>c==>b==>d").contains(response.getExecuteStepStr()));
+ Assertions.assertTrue(ListUtil.toList("a==>c==>d==>b","a==>d==>c==>b").contains(response.getExecuteStepStr()));
}
//测试子chain
@@ -44,7 +44,7 @@ public class SubflowSpringbootTest extends BaseTest {
public void testSubflow2() throws Exception {
LiteflowResponse response = flowExecutor.execute2Resp("chain2", "arg");
Assertions.assertTrue(response.isSuccess());
- Assertions.assertTrue(ListUtil.toList("a==>c==>d==>b","a==>c==>b==>d").contains(response.getExecuteStepStr()));
+ Assertions.assertTrue(ListUtil.toList("a==>c==>d==>b","a==>d==>c==>b").contains(response.getExecuteStepStr()));
}
//测试在组件里调用另一个流程
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/application.properties b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/application.properties
new file mode 100644
index 000000000..2f9c44ccc
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/application.properties
@@ -0,0 +1,4 @@
+liteflow.rule-source=chainCache/flow.el.xml
+liteflow.chain-cache.enabled=true
+liteflow.chain-cache.capacity=5
+liteflow.parse-mode=PARSE_ONE_ON_FIRST_EXEC
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/flow.el.xml
new file mode 100644
index 000000000..11acd1cd4
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springboot/src/test/resources/chainCache/flow.el.xml
@@ -0,0 +1,43 @@
+
+
+
+ THEN(a, b);
+
+
+
+ THEN(a, c);
+
+
+
+ THEN(b, a);
+
+
+
+ THEN(b, c);
+
+
+
+ THEN(c, a);
+
+
+
+ THEN(c, b);
+
+
+
+ THEN(IF(x, a), b);
+
+
+
+ THEN(IF(x, a), c);
+
+
+
+ WHEN(a, b, c);
+
+
+
+ FOR(5).DO(THEN(a, b, c));
+
+
+
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringTest.java b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringTest.java
new file mode 100644
index 000000000..984a3e46a
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/ChainCacheSpringTest.java
@@ -0,0 +1,229 @@
+package com.yomahub.liteflow.test.chainCache;
+
+import cn.hutool.core.collection.CollUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
+import com.yomahub.liteflow.core.FlowExecutor;
+import com.yomahub.liteflow.exception.ChainNotFoundException;
+import com.yomahub.liteflow.flow.FlowBus;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.flow.element.Chain;
+import com.yomahub.liteflow.flow.element.Condition;
+import com.yomahub.liteflow.lifecycle.impl.ChainCacheLifeCycle;
+import com.yomahub.liteflow.test.BaseTest;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import javax.annotation.Resource;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Spring环境下chain缓存测试
+ * @author DaleLee
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration("classpath:/chainCache/application.xml")
+public class ChainCacheSpringTest extends BaseTest {
+ @Resource
+ private FlowExecutor flowExecutor;
+
+ @BeforeEach
+ public void reload() {
+ flowExecutor.reloadRule();
+ // 清空缓存
+ Cache cache = getCache();
+ cache.invalidateAll();
+ cache.cleanUp();
+ }
+
+ // 测试chain被淘汰
+ @Test
+ public void testChainCache1() {
+ // 加满缓存
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain6", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("c==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // 测试被淘汰的chain仍可正常执行
+ response = flowExecutor.execute2Resp(chainId, "arg");
+ Assertions.assertTrue(response.isSuccess());
+ }
+
+ // 测试缓存数量上限(串行)
+ @Test
+ public void testChainCache2() {
+ // 确保至少执行过5个不同的chain
+ loadCache();
+ // 随机执行chain
+ Random random = new Random();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ flowExecutor.execute2Resp("chain" + id);
+ }
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试缓存数量上限(并行)
+ @Test
+ public void testChainCache3() {
+ loadCache();
+ Random random = new Random();
+ List> futureList = CollUtil.newArrayList();
+ for (int i = 0; i < 100; i++) {
+ int id = random.nextInt(10) + 1;
+ Future future = flowExecutor.execute2Future("chain" + id, "arg");
+ futureList.add(future);
+ }
+ futureList.forEach(future -> {
+ try {
+ LiteflowResponse response = future.get();
+ Assertions.assertTrue(response.isSuccess());
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ // 等待缓存淘汰
+ getCache().cleanUp();
+ Assertions.assertEquals(10, FlowBus.getChainMap().size());
+ // 测试只有5个chain被编译
+ int count = 0;
+ for (Chain chain : FlowBus.getChainMap().values()) {
+ List conditionList = chain.getConditionList();
+ if (chain.isCompiled()) {
+ Assertions.assertTrue(CollUtil.isNotEmpty(conditionList));
+ count++;
+ } else {
+ Assertions.assertNull(conditionList);
+ }
+ }
+ Assertions.assertTrue(count <= 5);
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain可以正常被更新
+ @Test
+ public void testChainCache4() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 更新chain7
+ LiteFlowChainELBuilder
+ .createChain()
+ .setChainId("chain7")
+ .setEL("THEN(a, b, c)")
+ .build();
+ // 重新执行chain7
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b==>c", response.getExecuteStepStr());
+ }
+
+ // 测试开启规则缓存后,进入缓存的chain被移除后无法执行
+ @Test
+ public void testChainCache5() {
+ loadCache();
+ // 缓存快照
+ HashSet<@NonNull String> strings = CollUtil.newHashSet(getCache().asMap().keySet());
+ LiteflowResponse response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("x==>a==>b", response.getExecuteStepStr());
+ // 获得被淘汰chain
+ String chainId = getEvictedChain(strings);
+ testEvicted(chainId);
+ // chain7进入缓存
+ Assertions.assertTrue(getCache().asMap().containsKey("chain7"));
+ // 手动移除chain7
+ FlowBus.removeChain("chain7");
+ response = flowExecutor.execute2Resp("chain7", "arg");
+ Assertions.assertFalse(response.isSuccess());
+ Assertions.assertEquals(ChainNotFoundException.class, response.getCause().getClass());
+ }
+
+ // 测试并发下,正在执行的chain的condition被清理但仍能执行
+ @Test
+ public void testChainCache6() throws InterruptedException {
+ // 模拟清空编译好的chain
+ Thread thread = new Thread(()-> {
+ Chain chain1 = FlowBus.getChain("chain1");
+ // 绕过初次编译
+ chain1.setCompiled(true);
+ chain1.setConditionList(null);
+ });
+ thread.start();
+ thread.join();
+ LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
+ Assertions.assertTrue(response.isSuccess());
+ Assertions.assertEquals("a==>b", response.getExecuteStepStr());
+ }
+
+ // 加载缓存, chain1~chain5
+ private void loadCache() {
+ // 容量上限为5
+ for (int i = 1; i <= 5; i++) {
+ flowExecutor.execute2Resp("chain" + i);
+ }
+ }
+
+ // 测试 chain 被淘汰
+ private void testEvicted(String chanId) {
+ Chain chain = FlowBus.getChain(chanId);
+ getCache().cleanUp();
+ // 测试缓存中不存在
+ Assertions.assertFalse(getCache().asMap().containsKey(chanId));
+ // 测试chain被设置为未编译
+ Assertions.assertFalse(chain.isCompiled());
+ Assertions.assertNull(chain.getConditionList());
+ }
+
+ public Cache getCache() {
+ return ChainCacheLifeCycle.getLifeCycle().getCache();
+ }
+
+ // 获得淘汰的chain,传入淘汰前的chain集合
+ // 确保只有一个被淘汰时使用
+ String getEvictedChain(Set set) {
+ Cache cache = getCache();
+ cache.cleanUp();
+ Set<@NonNull String> strings = cache.asMap().keySet();
+ set.removeAll(strings);
+ Assertions.assertEquals(1, set.size());
+ return set.iterator().next();
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
new file mode 100644
index 000000000..d943cb205
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/ACmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("a")
+public class ACmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("ACmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
new file mode 100644
index 000000000..356750126
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/BCmp.java
@@ -0,0 +1,21 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("b")
+public class BCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("BCmp executed!");
+ }
+
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
new file mode 100644
index 000000000..84dedc927
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/CCmp.java
@@ -0,0 +1,20 @@
+/**
+ * Title: liteflow
+ * Description: 轻量级的组件式流程框架
+ * @author Bryan.Zhang
+ * @email weenyc31@163.com
+ * @Date 2020/4/1
+ */
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeComponent;
+import org.springframework.stereotype.Component;
+
+@Component("c")
+public class CCmp extends NodeComponent {
+
+ @Override
+ public void process() {
+ System.out.println("CCmp executed!");
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
new file mode 100644
index 000000000..1f671e9e4
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/java/com/yomahub/liteflow/test/chainCache/cmp/XCmp.java
@@ -0,0 +1,12 @@
+package com.yomahub.liteflow.test.chainCache.cmp;
+
+import com.yomahub.liteflow.core.NodeBooleanComponent;
+import org.springframework.stereotype.Component;
+
+@Component("x")
+public class XCmp extends NodeBooleanComponent {
+ @Override
+ public boolean processBoolean() throws Exception {
+ return true;
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/application.xml b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/application.xml
new file mode 100644
index 000000000..27640330e
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/application.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/flow.el.xml
new file mode 100644
index 000000000..11acd1cd4
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-springnative/src/test/resources/chainCache/flow.el.xml
@@ -0,0 +1,43 @@
+
+
+
+ THEN(a, b);
+
+
+
+ THEN(a, c);
+
+
+
+ THEN(b, a);
+
+
+
+ THEN(b, c);
+
+
+
+ THEN(c, a);
+
+
+
+ THEN(c, b);
+
+
+
+ THEN(IF(x, a), b);
+
+
+
+ THEN(IF(x, a), c);
+
+
+
+ WHEN(a, b, c);
+
+
+
+ FOR(5).DO(THEN(a, b, c));
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 1e8b2e321..270f5fed7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,7 +58,7 @@
0.10
0.7.3
1.4.4
- 3.3.2
+ 3.3.4
3.0.8
22.0.0
1.14.10
@@ -81,6 +81,7 @@
4.3.1
4.1.1
1.13.1
+ 2.9.3
@@ -339,6 +340,11 @@
commons-text
${apache-commons-test.version}
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+