diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngine.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngine.java index 21a853e..e949fda 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngine.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngine.java @@ -20,6 +20,37 @@ import com.easyagents.flow.core.node.CodeNode; import java.util.Map; +/** + * 代码节点运行时引擎。 + */ public interface CodeRuntimeEngine { + + /** + * 执行代码节点。 + * + * @param code 解析模板后的代码 + * @param node 当前代码节点 + * @param chain 当前工作流 + * @return 节点输出 + */ Map execute(String code, CodeNode node, Chain chain); + + /** + * 使用已经解析的节点参数执行代码。 + * + *

默认委托旧接口,保持自定义运行时引擎兼容。内置引擎可覆盖该方法, + * 避免重复解析节点参数。

+ * + * @param code 解析模板后的代码 + * @param node 当前代码节点 + * @param chain 当前工作流 + * @param parameterValues 已解析的节点参数 + * @return 节点输出 + */ + default Map execute(String code, + CodeNode node, + Chain chain, + Map parameterValues) { + return execute(code, node, chain); + } } diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngineManager.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngineManager.java index 23d6b9a..0769d3f 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngineManager.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/CodeRuntimeEngineManager.java @@ -20,16 +20,23 @@ import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine; import java.util.ArrayList; import java.util.List; +/** + * 管理代码节点运行时引擎提供者。 + */ public class CodeRuntimeEngineManager { public List providers = new ArrayList<>(); + /** + * JavaScript 引擎实例,可在业务启动阶段配置超时后替换。 + */ + private volatile JavascriptRuntimeEngine javascriptRuntimeEngine; private static class ManagerHolder { private static final CodeRuntimeEngineManager INSTANCE = new CodeRuntimeEngineManager(); } private CodeRuntimeEngineManager() { - JavascriptRuntimeEngine javascriptRuntimeEngine = new JavascriptRuntimeEngine(); + javascriptRuntimeEngine = new JavascriptRuntimeEngine(); providers.add(engineId -> { if ("js".equals(engineId) || "javascript".equals(engineId)) { return javascriptRuntimeEngine; @@ -38,18 +45,50 @@ public class CodeRuntimeEngineManager { }); } + /** + * 获取全局代码运行时管理器。 + * + * @return 代码运行时管理器 + */ public static CodeRuntimeEngineManager getInstance() { return ManagerHolder.INSTANCE; } + /** + * 配置 JavaScript 代码执行超时。 + * + * @param timeoutMs 超时时间;0 表示关闭超时 + * @throws IllegalArgumentException 超时时间为负数时抛出 + */ + public void configureJavascriptRuntimeEngine(long timeoutMs) { + javascriptRuntimeEngine = + new JavascriptRuntimeEngine(timeoutMs); + } + + /** + * 注册代码运行时提供者。 + * + * @param provider 代码运行时提供者 + */ public void registerProvider(CodeRuntimeEngineProvider provider) { providers.add(provider); } + /** + * 移除代码运行时提供者。 + * + * @param provider 代码运行时提供者 + */ public void removeProvider(CodeRuntimeEngineProvider provider) { providers.remove(provider); } + /** + * 按引擎标识获取代码运行时。 + * + * @param engineId 引擎标识 + * @return 匹配的代码运行时;未找到时返回 {@code null} + */ public CodeRuntimeEngine getCodeRuntimeEngine(Object engineId) { for (CodeRuntimeEngineProvider provider : providers) { CodeRuntimeEngine codeRuntimeEngine = provider.getCodeRuntimeEngine(engineId); diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptExecutionException.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptExecutionException.java new file mode 100644 index 0000000..3758c5a --- /dev/null +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptExecutionException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com). + *

+ * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl-3.0.txt + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.easyagents.flow.core.code.impl; + +/** + * 可直接反馈给工作流使用者的 JavaScript 执行异常。 + */ +public class JavascriptExecutionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * 创建 JavaScript 执行异常。 + * + * @param message 面向使用者的错误信息 + * @param cause 原始执行异常 + */ + public JavascriptExecutionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java index 6496981..76a8a69 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java @@ -23,11 +23,23 @@ import com.easyagents.flow.core.code.CodeRuntimeEngine; import com.easyagents.flow.core.node.CodeNode; import com.easyagents.flow.core.util.graalvm.JsInteropUtils; import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.SourceSection; import org.graalvm.polyglot.Value; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * 基于 GraalVM 的 JavaScript 代码节点执行器。 @@ -38,26 +50,93 @@ import java.util.Map; */ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { - // 使用 Context.Builder 构建上下文,线程安全 - private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js") + private static final int SOURCE_CACHE_LIMIT = 512; + private static final int SOURCE_CACHE_SEGMENT_COUNT = 16; + private static final int SOURCE_CACHE_SEGMENT_LIMIT = + SOURCE_CACHE_LIMIT / SOURCE_CACHE_SEGMENT_COUNT; + private static final int MAX_CACHEABLE_SOURCE_CHARS = 16 * 1024; + private static final int TIMEOUT_EXECUTOR_THREADS = + Math.max(2, Math.min( + Runtime.getRuntime().availableProcessors(), 8)); + private static final String SOURCE_NAME = + "workflow-code-node.js"; + private static final AtomicInteger TIMEOUT_THREAD_SEQUENCE = + new AtomicInteger(); + /** + * Engine 跨隔离 Context 共享编译缓存。 + */ + private static final Engine ENGINE = Engine.newBuilder() .option("engine.WarnInterpreterOnly", "false") - .allowHostAccess(HostAccess.ALL) // 允许访问 Java 对象的方法和字段 - .allowHostClassLookup(className -> false) // 禁止动态加载任意 Java 类 - .option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本 - + .build(); + private static final Source RESULT_INIT_SOURCE = + Source.create("js", "var _result = {};"); + private static final List> SOURCE_CACHE_SEGMENTS = + createSourceCacheSegments(); + private static final ScheduledThreadPoolExecutor + TIMEOUT_EXECUTOR = createTimeoutExecutor(); /** - * 执行 JavaScript 代码并返回节点输出。 + * 单次 JavaScript 执行超时,0 表示关闭。 + */ + private final long timeoutMs; + + /** + * 创建不启用超时的 JavaScript 执行器。 + */ + public JavascriptRuntimeEngine() { + this(0L); + } + + /** + * 创建 JavaScript 执行器。 + * + * @param timeoutMs 单次执行超时,0 表示关闭 + * @throws IllegalArgumentException 超时时间为负数时抛出 + */ + public JavascriptRuntimeEngine(long timeoutMs) { + if (timeoutMs < 0L) { + throw new IllegalArgumentException( + "JavaScript 执行超时时间不能为负数"); + } + this.timeoutMs = timeoutMs; + } + + /** + * 执行 JavaScript 代码并返回节点输出,兼容直接调用旧接口的场景。 * * @param code 用户代码 * @param node 当前代码节点 * @param chain 当前工作流 * @return 代码节点输出 - * @throws RuntimeException JavaScript 执行失败或 main 返回值不是对象时抛出 + * @throws JavascriptExecutionException JavaScript 执行失败时抛出 */ @Override public Map execute(String code, CodeNode node, Chain chain) { - try (Context context = CONTEXT_BUILDER.build()) { + Map parameterValues = + chain.getExecutionState().resolveParameters(node); + return execute(code, node, chain, parameterValues); + } + + /** + * 使用已经解析的节点参数执行 JavaScript 代码。 + * + * @param code 用户代码 + * @param node 当前代码节点 + * @param chain 当前工作流 + * @param parameterValues 已解析的节点参数 + * @return 代码节点输出 + * @throws JavascriptExecutionException JavaScript 执行失败时抛出 + */ + @Override + public Map execute(String code, + CodeNode node, + Chain chain, + Map parameterValues) { + Context context = createContext(); + AtomicBoolean timedOut = new AtomicBoolean(false); + ScheduledFuture timeoutFuture = + scheduleTimeout(context, timedOut); + try (Context ignored = context) { Value bindings = context.getBindings("js"); ChainState chainState = chain.getExecutionState(); @@ -72,9 +151,6 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { } }); - // 注入参数 - Map parameterValues = - chainState.resolveParameters(node); if (parameterValues != null) { for (Map.Entry entry : parameterValues.entrySet()) { bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue())); @@ -82,22 +158,232 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { } // 在 JS 中创建 _result 对象 - context.eval("js", "var _result = {};"); + context.eval(RESULT_INIT_SOURCE); // 注入 _chain 和 _context bindings.putMember("_chain", chain); bindings.putMember("_state", nodeState); // 执行用户脚本 - context.eval("js", code); + context.eval(source(code, node)); return resolveResult(context, bindings, parameterValues); - + } catch (PolyglotException e) { + throw executionException(e, timedOut.get()); } catch (Exception e) { - throw new RuntimeException("Polyglot JS 脚本执行失败: " + e.getMessage(), e); + if (timedOut.get()) { + throw timeoutException(e); + } + throw new JavascriptExecutionException( + "JavaScript 执行失败:" + safeMessage(e), + e); + } finally { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } } } + /** + * 创建一次隔离的 JavaScript Context。 + * + * @return 新的 JavaScript Context + */ + private Context createContext() { + return Context.newBuilder("js") + .engine(ENGINE) + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup(className -> false) + .option("js.ecmascript-version", "2021") + .build(); + } + + /** + * 获取可复用的用户脚本 Source。 + * + *

模板化脚本可能随输入生成大量不同源码,因此不进入共享缓存。 + * 超长脚本同样跳过缓存,限制源码与编译元数据占用。

+ * + * @param code 解析模板后的代码 + * @param node 当前代码节点 + * @return GraalVM Source + */ + private Source source(String code, CodeNode node) { + String originalCode = + node == null ? null : node.getCode(); + boolean dynamicTemplate = originalCode != null + && originalCode.contains("{{"); + if (dynamicTemplate + || code.length() > MAX_CACHEABLE_SOURCE_CHARS) { + return Source.newBuilder("js", code, SOURCE_NAME) + .cached(false) + .buildLiteral(); + } + Map sourceCache = + SOURCE_CACHE_SEGMENTS.get( + Math.floorMod( + code.hashCode(), + SOURCE_CACHE_SEGMENT_COUNT)); + synchronized (sourceCache) { + return sourceCache.computeIfAbsent( + code, + script -> Source.newBuilder( + "js", + script, + SOURCE_NAME) + .cached(true) + .buildLiteral()); + } + } + + /** + * 创建分段有界的 Source 缓存。 + * + *

每个分段独立维护 LRU 淘汰,避免所有工作流竞争同一把锁, + * 同时保证总缓存条目不超过全局上限。

+ * + * @return Source 缓存分段 + */ + private static List> + createSourceCacheSegments() { + List> segments = + new ArrayList<>(SOURCE_CACHE_SEGMENT_COUNT); + for (int index = 0; + index < SOURCE_CACHE_SEGMENT_COUNT; + index++) { + segments.add(new LinkedHashMap<>( + SOURCE_CACHE_SEGMENT_LIMIT + 1, + 0.75F, + true) { + @Override + protected boolean removeEldestEntry( + Map.Entry eldest) { + return size() + > SOURCE_CACHE_SEGMENT_LIMIT; + } + }); + } + return Collections.unmodifiableList(segments); + } + + /** + * 安排单次执行超时。 + * + * @param context 待取消的 Context + * @param timedOut 超时状态 + * @return 超时任务;关闭超时时返回 {@code null} + */ + private ScheduledFuture scheduleTimeout( + Context context, + AtomicBoolean timedOut) { + if (timeoutMs <= 0L) { + return null; + } + return TIMEOUT_EXECUTOR.schedule(() -> { + timedOut.set(true); + context.close(true); + }, timeoutMs, TimeUnit.MILLISECONDS); + } + + /** + * 将 GraalVM 异常转换为用户可定位的脚本异常。 + * + * @param error GraalVM 异常 + * @param timedOut 是否由超时任务取消 + * @return JavaScript 执行异常 + */ + private JavascriptExecutionException executionException( + PolyglotException error, + boolean timedOut) { + if (timedOut) { + return timeoutException(error); + } + String errorType = error.isSyntaxError() + ? "JavaScript 语法错误" + : "JavaScript 执行失败"; + SourceSection location = sourceLocation(error); + String locationText = location == null + ? "" + : "(第 " + location.getStartLine() + + " 行,第 " + location.getStartColumn() + " 列)"; + return new JavascriptExecutionException( + errorType + locationText + ":" + safeMessage(error), + error); + } + + /** + * 获取异常对应的用户脚本位置。 + * + *

部分 GraalVM 版本不会在运行时异常上直接设置位置, + * 此时从首个 guest stack frame 补取。

+ * + * @param error GraalVM 异常 + * @return 用户脚本位置;无法定位时返回 {@code null} + */ + private SourceSection sourceLocation(PolyglotException error) { + SourceSection location = error.getSourceLocation(); + if (location != null) { + return location; + } + for (PolyglotException.StackFrame frame + : error.getPolyglotStackTrace()) { + if (frame.isGuestFrame() + && frame.getSourceLocation() != null) { + return frame.getSourceLocation(); + } + } + return null; + } + + /** + * 创建 JavaScript 执行超时异常。 + * + * @param cause 超时取消产生的异常 + * @return JavaScript 执行异常 + */ + private JavascriptExecutionException timeoutException( + Throwable cause) { + return new JavascriptExecutionException( + "JavaScript 执行超时(" + timeoutMs + "ms)", + cause); + } + + /** + * 获取稳定的异常消息。 + * + * @param error 原始异常 + * @return 非空异常消息 + */ + private String safeMessage(Throwable error) { + String message = error.getMessage(); + return message == null || message.isBlank() + ? error.getClass().getSimpleName() + : message; + } + + /** + * 创建超时调度器。 + * + * @return 有界多线程守护调度器 + */ + private static ScheduledThreadPoolExecutor + createTimeoutExecutor() { + ScheduledThreadPoolExecutor executor = + new ScheduledThreadPoolExecutor( + TIMEOUT_EXECUTOR_THREADS, + task -> { + Thread thread = new Thread( + task, + "javascript-runtime-timeout-" + + TIMEOUT_THREAD_SEQUENCE + .incrementAndGet()); + thread.setDaemon(true); + return thread; + }); + executor.setRemoveOnCancelPolicy(true); + return executor; + } + /** * 解析脚本输出,兼容历史 _result 写法并自动调用 main 函数。 * diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java index 09e1f3f..e49d839 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java @@ -63,7 +63,11 @@ public class CodeNode extends BaseNode { if (codeRuntimeEngine == null) { throw new IllegalArgumentException("code runtime engine not found: " + this.engine); } - return codeRuntimeEngine.execute(newCode, this, chain); + return codeRuntimeEngine.execute( + newCode, + this, + chain, + parameterValues); } } diff --git a/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java b/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java index cf675e4..982b534 100644 --- a/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java +++ b/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java @@ -6,14 +6,25 @@ import com.easyagents.flow.core.chain.ChainState; import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository; import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository; +import com.easyagents.flow.core.code.impl.JavascriptExecutionException; +import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine; import com.easyagents.flow.core.node.CodeNode; import org.junit.Assert; import org.junit.Test; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -82,6 +93,252 @@ public class JavascriptRuntimeEngineTest { } } + /** + * 验证语法错误包含可定位的脚本行列信息。 + */ + @Test + public void shouldExposeSyntaxErrorLocation() { + CodeNode node = codeNode("const answer = ;"); + Chain chain = chain(Collections.emptyMap()); + + try { + node.execute(chain); + Assert.fail("JavaScript 语法错误应执行失败"); + } catch (JavascriptExecutionException exception) { + Assert.assertTrue(exception.getMessage().contains( + "JavaScript 语法错误")); + Assert.assertTrue(exception.getMessage().contains("第 1 行")); + } + } + + /** + * 验证运行时异常包含原始错误和脚本位置。 + */ + @Test + public void shouldExposeRuntimeErrorLocation() { + CodeNode node = codeNode("throw new Error('boom');"); + Chain chain = chain(Collections.emptyMap()); + + try { + node.execute(chain); + Assert.fail("JavaScript 运行时异常应执行失败"); + } catch (JavascriptExecutionException exception) { + Assert.assertTrue(exception.getMessage().contains( + "JavaScript 执行失败")); + Assert.assertTrue(exception.getMessage().contains("第 1 行")); + Assert.assertTrue(exception.getMessage().contains("boom")); + } + } + + /** + * 验证无限循环会在配置时间内被取消。 + */ + @Test + public void shouldCancelExecutionAfterTimeout() { + JavascriptRuntimeEngine engine = + new JavascriptRuntimeEngine(100L); + CodeNode node = codeNode("while (true) {}"); + Chain chain = chain(Collections.emptyMap()); + long startedAt = System.currentTimeMillis(); + + try { + engine.execute( + node.getCode(), + node, + chain, + Collections.emptyMap()); + Assert.fail("JavaScript 超时应执行失败"); + } catch (JavascriptExecutionException exception) { + Assert.assertTrue(exception.getMessage().contains( + "JavaScript 执行超时(100ms)")); + Assert.assertTrue( + "超时取消应在合理时间内完成", + System.currentTimeMillis() - startedAt < 3000L); + } + } + + /** + * 验证负数超时不会静默关闭执行保护。 + */ + @Test + public void shouldRejectNegativeTimeout() { + try { + new JavascriptRuntimeEngine(-1L); + Assert.fail("负数超时应被拒绝"); + } catch (IllegalArgumentException exception) { + Assert.assertTrue(exception.getMessage().contains( + "不能为负数")); + } + } + + /** + * 验证多个无限循环可并发触发超时取消。 + * + * @throws Exception 并发任务执行失败时抛出 + */ + @Test + public void shouldCancelConcurrentExecutionsAfterTimeout() + throws Exception { + int executionCount = 8; + JavascriptRuntimeEngine engine = + new JavascriptRuntimeEngine(100L); + ExecutorService executor = + Executors.newFixedThreadPool(executionCount); + CountDownLatch ready = + new CountDownLatch(executionCount); + CountDownLatch start = new CountDownLatch(1); + List> futures = + new ArrayList<>(executionCount); + try { + for (int index = 0; + index < executionCount; + index++) { + final int executionIndex = index; + futures.add(executor.submit(() -> { + CodeNode node = codeNode("while (true) {}"); + node.setId("timeout-node-" + executionIndex); + Chain chain = chain(Collections.emptyMap()); + ready.countDown(); + start.await(); + try { + engine.execute( + node.getCode(), + node, + chain, + Collections.emptyMap()); + return "未触发超时"; + } catch (JavascriptExecutionException exception) { + return exception.getMessage(); + } + })); + } + Assert.assertTrue( + "并发执行未及时就绪", + ready.await(2L, TimeUnit.SECONDS)); + long startedAt = System.currentTimeMillis(); + start.countDown(); + for (Future future : futures) { + Assert.assertTrue( + future.get(3L, TimeUnit.SECONDS) + .contains("JavaScript 执行超时(100ms)")); + } + Assert.assertTrue( + "并发超时取消应在合理时间内完成", + System.currentTimeMillis() - startedAt < 3000L); + } finally { + start.countDown(); + executor.shutdownNow(); + } + } + + /** + * 验证分段 Source 缓存在并发未命中时保持结果正确。 + * + * @throws Exception 并发任务执行失败时抛出 + */ + @Test + public void shouldExecuteCacheMissesConcurrently() + throws Exception { + int executionCount = 32; + JavascriptRuntimeEngine engine = + new JavascriptRuntimeEngine(); + ExecutorService executor = + Executors.newFixedThreadPool(8); + List> futures = + new ArrayList<>(executionCount); + try { + for (int index = 0; + index < executionCount; + index++) { + final int expected = index; + futures.add(executor.submit(() -> { + CodeNode node = codeNode( + "_result.value = " + expected + ";"); + node.setId("cache-node-" + expected); + Map result = engine.execute( + node.getCode(), + node, + chain(Collections.emptyMap()), + Collections.emptyMap()); + return ((Number) result.get("value")) + .longValue(); + })); + } + for (int index = 0; + index < executionCount; + index++) { + Assert.assertEquals( + index, + futures.get(index) + .get(3L, TimeUnit.SECONDS) + .longValue()); + } + } finally { + executor.shutdownNow(); + } + } + + /** + * 验证分段缓存总条目数始终受上限约束。 + * + * @throws Exception 反射访问缓存失败时抛出 + */ + @Test + @SuppressWarnings("unchecked") + public void shouldKeepSourceCacheBounded() + throws Exception { + JavascriptRuntimeEngine engine = + new JavascriptRuntimeEngine(); + Method sourceMethod = JavascriptRuntimeEngine.class + .getDeclaredMethod( + "source", + String.class, + CodeNode.class); + sourceMethod.setAccessible(true); + CodeNode node = codeNode(""); + for (int index = 0; index < 1024; index++) { + String code = "_result.value = " + index + ";"; + node.setCode(code); + sourceMethod.invoke(engine, code, node); + } + + Field cacheField = JavascriptRuntimeEngine.class + .getDeclaredField("SOURCE_CACHE_SEGMENTS"); + cacheField.setAccessible(true); + List> segments = + (List>) cacheField.get(null); + int cachedSourceCount = segments.stream() + .mapToInt(Map::size) + .sum(); + + Assert.assertTrue(cachedSourceCount <= 512); + Assert.assertTrue( + segments.stream() + .allMatch(segment -> + segment.size() <= 32)); + } + + /** + * 验证共享 Engine 下不同执行仍保持全局变量隔离。 + */ + @Test + public void shouldKeepExecutionContextsIsolated() { + CodeNode node = codeNode(String.join("\n", + "if (typeof globalCounter === 'undefined') {", + " globalCounter = 0;", + "}", + "globalCounter += 1;", + "_result.counter = globalCounter;")); + + Map first = + node.execute(chain(Collections.emptyMap())); + Map second = + node.execute(chain(Collections.emptyMap())); + + Assert.assertEquals(1L, first.get("counter")); + Assert.assertEquals(1L, second.get("counter")); + } + /** * 创建 JavaScript 代码节点。 *