From 5e1e9dab2bd9e37a4f2203ef12b65844da76f7e2 Mon Sep 17 00:00:00 2001 From: "everywhere.z" Date: Fri, 7 Aug 2026 14:08:05 +0800 Subject: [PATCH] =?UTF-8?q?bug=20#IK6XVN=20liteflow=E2=80=91script?= =?UTF-8?q?=E2=80=91javax=E2=80=91pro=20=E4=B8=AD=20JavaxProExecutor=20?= =?UTF-8?q?=E7=9A=84=20ThreadLocal>=20=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/javaxpro/JavaxProExecutor.java | 75 ++++++++++++---- .../refnode/RefNodeStackLeakTest.java | 90 +++++++++++++++++++ .../resources/refnode/application.properties | 1 + .../src/test/resources/refnode/flow.xml | 51 +++++++++++ 4 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/java/com/yomahub/liteflow/test/script/javaxpro/refnode/RefNodeStackLeakTest.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/application.properties create mode 100644 liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/flow.xml diff --git a/liteflow-script-plugin/liteflow-script-javax-pro/src/main/java/com/yomahub/liteflow/script/javaxpro/JavaxProExecutor.java b/liteflow-script-plugin/liteflow-script-javax-pro/src/main/java/com/yomahub/liteflow/script/javaxpro/JavaxProExecutor.java index 591ae3edc..1b97adba3 100644 --- a/liteflow-script-plugin/liteflow-script-javax-pro/src/main/java/com/yomahub/liteflow/script/javaxpro/JavaxProExecutor.java +++ b/liteflow-script-plugin/liteflow-script-javax-pro/src/main/java/com/yomahub/liteflow/script/javaxpro/JavaxProExecutor.java @@ -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); + } + + /** + * 在编译出的脚本组件上执行一段有返回值的动作。 + *

+ * 编译产物是跨执行共享的单例,每次调用前都要把当前执行现场(refNode 等)注入进去, + * 因此用完必须成对地清理,否则 refNodeStackTL 会随调用次数无界增长(内存泄漏)。 + * 这里是全类唯一的注入 + 清理点,新增入口只要走这个方法就不可能漏掉清理。 + * + * @param wrap 脚本执行元参数 + * @param action 要在脚本组件上执行的动作 + * @param 动作的返回值类型 + * @param 动作抛出的异常类型(不抛受检异常时会推断为 RuntimeException) + */ + private T withExecutableCmp(ScriptExecuteWrap wrap, CmpFunction action) throws E { NodeComponent cmp = getExecutableCmp(wrap); - cmp.rollback(); + try { + return action.apply(cmp); + } finally { + cmp.removeRefNode(); + } + } + + /** + * 在编译出的脚本组件上执行一段无返回值的动作,语义同 {@link #withExecutableCmp(ScriptExecuteWrap, CmpFunction)} + *

+ * 这里没有和上面的方法同名重载,是因为 lambda 形态的实参(如 {@code cmp -> cmp.onError(e)}) + * 无法靠返回类型在两个函数式接口之间消歧,会导致编译期歧义。 + */ + private void runWithExecutableCmp(ScriptExecuteWrap wrap, CmpConsumer 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 apply(NodeComponent cmp) throws E; + } + + @FunctionalInterface + private interface CmpConsumer { + void accept(NodeComponent cmp) throws E; + } + private String convertScript(String script){ //替换掉public,private,protected等修饰词 String script1 = script.replaceAll("public class", "class") diff --git a/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/java/com/yomahub/liteflow/test/script/javaxpro/refnode/RefNodeStackLeakTest.java b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/java/com/yomahub/liteflow/test/script/javaxpro/refnode/RefNodeStackLeakTest.java new file mode 100644 index 000000000..bd338b10d --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/java/com/yomahub/liteflow/test/script/javaxpro/refnode/RefNodeStackLeakTest.java @@ -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 不会无限增长 + *

+ * 场景:预创建有限条 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 compiledScriptMap = (Map) 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> tl = (ThreadLocal>) tlField.get(cmp); + return tl.get(); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/application.properties b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/application.properties new file mode 100644 index 000000000..a65c7811e --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/application.properties @@ -0,0 +1 @@ +liteflow.rule-source=refnode/flow.xml diff --git a/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/flow.xml b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/flow.xml new file mode 100644 index 000000000..f1a49bd21 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-script-javaxpro-springboot/src/test/resources/refnode/flow.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + THEN(s1); + + + + THEN(s1); + + + + + THEN(s2); + + + + THEN(s2); + +