mirror of https://gitee.com/dromara/liteFlow
bug #IK6XVN liteflow‑script‑javax‑pro 中 JavaxProExecutor 的 ThreadLocal<Stack<Node>> 泄漏
This commit is contained in:
parent
039d5cff0c
commit
5e1e9dab2b
|
|
@ -115,9 +115,10 @@ public class JavaxProExecutor extends ScriptExecutor {
|
|||
|
||||
@Override
|
||||
public Object executeScript(ScriptExecuteWrap wrap) throws Exception {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.process();
|
||||
return cmp.getItemResultMetaValue(wrap.slotIndex);
|
||||
return withExecutableCmp(wrap, cmp -> {
|
||||
cmp.process();
|
||||
return cmp.getItemResultMetaValue(wrap.slotIndex);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -162,50 +163,78 @@ public class JavaxProExecutor extends ScriptExecutor {
|
|||
|
||||
@Override
|
||||
public boolean executeIsAccess(ScriptExecuteWrap wrap) {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
return cmp.isAccess();
|
||||
return withExecutableCmp(wrap, NodeComponent::isAccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean executeIsContinueOnError(ScriptExecuteWrap wrap) {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
return cmp.isContinueOnError();
|
||||
return withExecutableCmp(wrap, NodeComponent::isContinueOnError);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean executeIsEnd(ScriptExecuteWrap wrap) {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
return cmp.isEnd();
|
||||
return withExecutableCmp(wrap, NodeComponent::isEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeBeforeProcess(ScriptExecuteWrap wrap) {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.beforeProcess();
|
||||
runWithExecutableCmp(wrap, NodeComponent::beforeProcess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeAfterProcess(ScriptExecuteWrap wrap) {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.afterProcess();
|
||||
runWithExecutableCmp(wrap, NodeComponent::afterProcess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnSuccess(ScriptExecuteWrap wrap) throws Exception {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.onSuccess();
|
||||
runWithExecutableCmp(wrap, NodeComponent::onSuccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnError(ScriptExecuteWrap wrap, Exception e) throws Exception {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.onError(e);
|
||||
runWithExecutableCmp(wrap, cmp -> cmp.onError(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeRollback(ScriptExecuteWrap wrap) throws Exception {
|
||||
runWithExecutableCmp(wrap, NodeComponent::rollback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在编译出的脚本组件上执行一段有返回值的动作。
|
||||
* <p>
|
||||
* 编译产物是跨执行共享的单例,每次调用前都要把当前执行现场(refNode 等)注入进去,
|
||||
* 因此用完必须成对地清理,否则 refNodeStackTL 会随调用次数无界增长(内存泄漏)。
|
||||
* 这里是全类唯一的注入 + 清理点,新增入口只要走这个方法就不可能漏掉清理。
|
||||
*
|
||||
* @param wrap 脚本执行元参数
|
||||
* @param action 要在脚本组件上执行的动作
|
||||
* @param <T> 动作的返回值类型
|
||||
* @param <E> 动作抛出的异常类型(不抛受检异常时会推断为 RuntimeException)
|
||||
*/
|
||||
private <T, E extends Exception> T withExecutableCmp(ScriptExecuteWrap wrap, CmpFunction<T, E> action) throws E {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
cmp.rollback();
|
||||
try {
|
||||
return action.apply(cmp);
|
||||
} finally {
|
||||
cmp.removeRefNode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在编译出的脚本组件上执行一段无返回值的动作,语义同 {@link #withExecutableCmp(ScriptExecuteWrap, CmpFunction)}
|
||||
* <p>
|
||||
* 这里没有和上面的方法同名重载,是因为 lambda 形态的实参(如 {@code cmp -> cmp.onError(e)})
|
||||
* 无法靠返回类型在两个函数式接口之间消歧,会导致编译期歧义。
|
||||
*/
|
||||
private <E extends Exception> void runWithExecutableCmp(ScriptExecuteWrap wrap, CmpConsumer<E> action) throws E {
|
||||
NodeComponent cmp = getExecutableCmp(wrap);
|
||||
try {
|
||||
action.accept(cmp);
|
||||
} finally {
|
||||
cmp.removeRefNode();
|
||||
}
|
||||
}
|
||||
|
||||
private NodeComponent getExecutableCmp(ScriptExecuteWrap wrap){
|
||||
|
|
@ -222,6 +251,16 @@ public class JavaxProExecutor extends ScriptExecutor {
|
|||
return cmp;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface CmpFunction<T, E extends Exception> {
|
||||
T apply(NodeComponent cmp) throws E;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface CmpConsumer<E extends Exception> {
|
||||
void accept(NodeComponent cmp) throws E;
|
||||
}
|
||||
|
||||
private String convertScript(String script){
|
||||
//替换掉public,private,protected等修饰词
|
||||
String script1 = script.replaceAll("public class", "class")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
package com.yomahub.liteflow.test.script.javaxpro.refnode;
|
||||
|
||||
import com.yomahub.liteflow.core.FlowExecutor;
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import com.yomahub.liteflow.flow.LiteflowResponse;
|
||||
import com.yomahub.liteflow.script.ScriptExecutor;
|
||||
import com.yomahub.liteflow.script.ScriptExecutorFactory;
|
||||
import com.yomahub.liteflow.test.BaseTest;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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 org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* 验证 javax-pro(liquor) 编译出的脚本组件上的 refNodeStackTL 不会无限增长
|
||||
* <p>
|
||||
* 场景:预创建有限条 chain(各含同一 java 脚本节点),同一线程轮询执行不同 chain。
|
||||
* EL 构建时每个节点出现位置都会 clone 出一个新的 Node 对象,JavaxProExecutor.getExecutableCmp
|
||||
* 对编译出的 NodeComponent 调用 setRefNode(仅与栈顶做引用相等比较)却从不 removeRefNode,
|
||||
* 导致编译组件上的 refNodeStackTL 中的 Stack 随调用次数线性增长(内存泄漏)。
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@TestPropertySource(value = "classpath:/refnode/application.properties")
|
||||
@SpringBootTest(classes = RefNodeStackLeakTest.class)
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan({ "com.yomahub.liteflow.test.script.javaxpro.refnode.cmp" })
|
||||
public class RefNodeStackLeakTest extends BaseTest {
|
||||
|
||||
@Resource
|
||||
private FlowExecutor flowExecutor;
|
||||
|
||||
@Test
|
||||
public void testRefNodeStackNotLeak() throws Exception {
|
||||
// 同一线程轮询执行两条 chain(两条 chain 中的 s1 是不同的 Node 克隆对象)
|
||||
for (int i = 0; i < 100; i++) {
|
||||
LiteflowResponse respA = flowExecutor.execute2Resp("chainA", "arg");
|
||||
Assertions.assertTrue(respA.isSuccess());
|
||||
LiteflowResponse respB = flowExecutor.execute2Resp("chainB", "arg");
|
||||
Assertions.assertTrue(respB.isSuccess());
|
||||
}
|
||||
|
||||
Stack<?> refNodeStack = getRefNodeStack("s1");
|
||||
|
||||
// 执行结束后,编译组件的 refNode 栈应当被清理干净(ThreadLocal 被 remove 或栈为空)
|
||||
// 泄漏情况下这里会随执行次数线性增长(本用例中为 200)
|
||||
Assertions.assertTrue(refNodeStack == null || refNodeStack.isEmpty(),
|
||||
"refNodeStackTL leaked, size=" + (refNodeStack == null ? 0 : refNodeStack.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 脚本抛异常时,refNode 同样要被清理(清理动作放在 finally 中的意义)
|
||||
*/
|
||||
@Test
|
||||
public void testRefNodeStackNotLeakOnScriptError() throws Exception {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Assertions.assertFalse(flowExecutor.execute2Resp("errChainA", "arg").isSuccess());
|
||||
Assertions.assertFalse(flowExecutor.execute2Resp("errChainB", "arg").isSuccess());
|
||||
}
|
||||
|
||||
Stack<?> refNodeStack = getRefNodeStack("s2");
|
||||
|
||||
Assertions.assertTrue(refNodeStack == null || refNodeStack.isEmpty(),
|
||||
"refNodeStackTL leaked on error path, size=" + (refNodeStack == null ? 0 : refNodeStack.size()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Stack<?> getRefNodeStack(String nodeId) throws Exception {
|
||||
ScriptExecutor scriptExecutor = ScriptExecutorFactory.loadInstance().getScriptExecutor("java");
|
||||
|
||||
Field mapField = scriptExecutor.getClass().getDeclaredField("compiledScriptMap");
|
||||
mapField.setAccessible(true);
|
||||
Map<String, NodeComponent> compiledScriptMap = (Map<String, NodeComponent>) mapField.get(scriptExecutor);
|
||||
NodeComponent cmp = compiledScriptMap.get(nodeId);
|
||||
Assertions.assertNotNull(cmp, "compiled script component not found for node " + nodeId);
|
||||
|
||||
Field tlField = NodeComponent.class.getDeclaredField("refNodeStackTL");
|
||||
tlField.setAccessible(true);
|
||||
ThreadLocal<Stack<?>> tl = (ThreadLocal<Stack<?>>) tlField.get(cmp);
|
||||
return tl.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
liteflow.rule-source=refnode/flow.xml
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
|
||||
<flow>
|
||||
<nodes>
|
||||
<node id="s1" name="脚本S1" type="script" language="java">
|
||||
<![CDATA[
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import com.yomahub.liteflow.slot.DefaultContext;
|
||||
|
||||
public class LeakDemo extends NodeComponent {
|
||||
@Override
|
||||
public void process() throws Exception {
|
||||
DefaultContext ctx = this.getFirstContextBean();
|
||||
ctx.setData(this.getNodeId(), "executed by " + this.getNodeId());
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</node>
|
||||
<!-- 抛异常的脚本节点,用于验证异常路径下 refNode 也会被清理 -->
|
||||
<node id="s2" name="脚本S2" type="script" language="java">
|
||||
<![CDATA[
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
|
||||
public class ErrorDemo extends NodeComponent {
|
||||
@Override
|
||||
public void process() throws Exception {
|
||||
throw new RuntimeException("script error on purpose");
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</node>
|
||||
</nodes>
|
||||
|
||||
<!-- 两条 chain 引用同一个脚本节点 s1,EL 构建时会出现两个不同的 Node 克隆对象 -->
|
||||
<chain name="chainA">
|
||||
THEN(s1);
|
||||
</chain>
|
||||
|
||||
<chain name="chainB">
|
||||
THEN(s1);
|
||||
</chain>
|
||||
|
||||
<!-- 异常场景:两条 chain 引用同一个会抛错的脚本节点 s2 -->
|
||||
<chain name="errChainA">
|
||||
THEN(s2);
|
||||
</chain>
|
||||
|
||||
<chain name="errChainB">
|
||||
THEN(s2);
|
||||
</chain>
|
||||
</flow>
|
||||
Loading…
Reference in New Issue