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
+ * 这里没有和上面的方法同名重载,是因为 lambda 形态的实参(如 {@code cmp -> cmp.onError(e)})
+ * 无法靠返回类型在两个函数式接口之间消歧,会导致编译期歧义。
+ */
+ private
+ * 场景:预创建有限条 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