From eba645c542e4f06ff67ce8f3bf7154f3ba4e51fc Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 26 Mar 2024 19:25:44 +0100 Subject: [PATCH 1/5] Nashorn LOCAL_JS_SANDBOX_MAX_MEMORY introduced --- application/src/main/resources/thingsboard.yml | 2 ++ .../script/api/AbstractScriptInvokeService.java | 4 ++-- .../thingsboard/script/api/js/NashornJsInvokeService.java | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index daf9f5d1c1..01f0d83771 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -866,6 +866,8 @@ js: monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:8000}" + # Maximum memory in Bytes which JS executor thread can allocate (approximate calculation). A zero memory limit in combination with a non-zero CPU limit is not recommended due to the implementation of Nashorn 0.4.2. 100MiB is effectively unlimited for most cases + max_memory: "${LOCAL_JS_SANDBOX_MAX_MEMORY:104857600}" # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}" # JS Eval max request timeout. 0 - no timeout diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index f88625f7fd..32e209b2ea 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -145,14 +145,14 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService log.trace("[{}] InvokeScript uuid {} with timeout {}ms", tenantId, scriptId, getMaxInvokeRequestsTimeout()); var task = doInvokeFunction(scriptId, args); - var resultFuture = Futures.transformAsync(task.getResultFuture(), output -> { + var resultFuture = Futures.transform(task.getResultFuture(), output -> { String result = JacksonUtil.toString(output); if (resultSizeExceeded(result)) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException( format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()) )); } - return Futures.immediateFuture(output); + return output; }, MoreExecutors.directExecutor()); return withTimeoutAndStatsCallback(scriptId, task, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout()); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java index 0e37bd89d6..6a07e596ef 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java @@ -41,7 +41,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.locks.ReentrantLock; @Slf4j @@ -65,6 +64,9 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { @Value("${js.local.max_cpu_time}") private long maxCpuTime; + @Value("${js.local.max_memory}") + private long maxMemory; + @Getter @Value("${js.local.max_errors}") private int maxErrors; @@ -107,12 +109,13 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { @Override public void init() { super.init(); - jsExecutor = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(jsExecutorThreadPoolSize)); + jsExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(jsExecutorThreadPoolSize, "nashorn-js-executor")); if (useJsSandbox) { sandbox = NashornSandboxes.create(); monitorExecutorService = ThingsBoardExecutors.newWorkStealingPool(monitorThreadPoolSize, "nashorn-js-monitor"); sandbox.setExecutor(monitorExecutorService); sandbox.setMaxCPUTime(maxCpuTime); + sandbox.setMaxMemory(maxMemory); sandbox.allowNoBraces(false); sandbox.allowLoadFunctions(true); sandbox.setMaxPreparedStatements(30); From 3d1f1e4af0ccc844fd9c752af1db26df99a943ee Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 26 Mar 2024 19:27:23 +0100 Subject: [PATCH 2/5] NashornJsInvokeServiceTest refactored --- .../script/NashornJsInvokeServiceTest.java | 31 +++++++++++-------- .../src/test/resources/logback-test.xml | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 55c521c309..bb9dc57d3c 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.script; -import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; -import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.js.NashornJsInvokeService; import org.thingsboard.server.common.data.id.TenantId; @@ -32,6 +32,7 @@ import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @@ -41,8 +42,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE "js.max_script_body_size=50", "js.max_total_args_size=50", "js.max_result_size=50", - "js.local.max_errors=2" + "js.local.max_errors=2", }) +@Slf4j class NashornJsInvokeServiceTest extends AbstractControllerTest { @Autowired @@ -56,23 +58,26 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { int iterations = 1000; UUID scriptId = evalScript("return msg.temperature > 20"); // warmup - ObjectNode msg = JacksonUtil.newObjectNode(); - for (int i = 0; i < 100; i++) { - msg.put("temperature", i); + log.info("Warming up 1000 times..."); + var warmupWatch = TbStopWatch.create(); + for (int i = 0; i < 1000; i++) { boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, "{\"temperature\":" + i + "}")); Assert.assertEquals(expected, result); } - long startTs = System.currentTimeMillis(); + log.info("Warming up finished in {} ms", warmupWatch.stopAndGetTotalTimeMillis()); + log.info("Starting performance test..."); + var watch = TbStopWatch.create(); for (int i = 0; i < iterations; i++) { - msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScript(scriptId, "{\"temperature\":" + i + "}")); + log.debug("asserting result"); Assert.assertEquals(expected, result); } - long duration = System.currentTimeMillis() - startTs; - System.out.println(iterations + " invocations took: " + duration + "ms"); - Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(4)); + long duration = watch.stopAndGetTotalTimeMillis(); + log.info("Performance test with {} invocations took: {} ms", iterations, duration); + assertThat(duration).as("duration ms") + .isLessThan(TimeUnit.MINUTES.toMillis(1)); // effective exec time is about 500ms } @Test diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index 981bcab132..d72bccb7a6 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -16,7 +16,7 @@ - + From cd722a11484bf3f54cdeb628135af7018ac36f65 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 26 Mar 2024 19:34:13 +0100 Subject: [PATCH 3/5] bump delight-nashorn-sandbox.version to the latest 0.4.2. As it supports Java 17 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d2450e0a65..f8e6c1ec1f 100755 --- a/pom.xml +++ b/pom.xml @@ -103,7 +103,7 @@ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 - 0.2.1 + 0.4.2 15.4