diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java index 62e7c24b1a..807c43ffa7 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java @@ -15,21 +15,33 @@ */ package org.thingsboard.server.service.script; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptException; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; @Slf4j public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeService { @@ -37,9 +49,46 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer private NashornSandbox sandbox; private ScriptEngine engine; private ExecutorService monitorExecutorService; + private ScheduledExecutorService timeoutExecutorService; + + private final AtomicInteger jsPushedMsgs = new AtomicInteger(0); + private final AtomicInteger jsInvokeMsgs = new AtomicInteger(0); + private final AtomicInteger jsEvalMsgs = new AtomicInteger(0); + private final AtomicInteger jsFailedMsgs = new AtomicInteger(0); + private final AtomicInteger jsTimeoutMsgs = new AtomicInteger(0); + private final FutureCallback evalCallback = new JsStatCallback(jsEvalMsgs, jsTimeoutMsgs, jsFailedMsgs); + private final FutureCallback invokeCallback = new JsStatCallback(jsInvokeMsgs, jsTimeoutMsgs, jsFailedMsgs); + + @Autowired + @Getter + private JsExecutorService jsExecutor; + + @Value("${js.local.max_requests_timeout:0}") + private long maxRequestsTimeout; + + @Value("${js.local.stats.enabled:false}") + private boolean statsEnabled; + + @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms:10000}") + public void printStats() { + if (statsEnabled) { + int pushedMsgs = jsPushedMsgs.getAndSet(0); + int invokeMsgs = jsInvokeMsgs.getAndSet(0); + int evalMsgs = jsEvalMsgs.getAndSet(0); + int failed = jsFailedMsgs.getAndSet(0); + int timedOut = jsTimeoutMsgs.getAndSet(0); + if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) { + log.info("Nashorn JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]", + pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut); + } + } + } @PostConstruct public void init() { + if (maxRequestsTimeout > 0) { + timeoutExecutorService = Executors.newSingleThreadScheduledExecutor(); + } if (useJsSandbox()) { sandbox = NashornSandboxes.create(); monitorExecutorService = Executors.newWorkStealingPool(getMonitorThreadPoolSize()); @@ -59,6 +108,9 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer if (monitorExecutorService != null) { monitorExecutorService.shutdownNow(); } + if (timeoutExecutorService != null) { + timeoutExecutorService.shutdownNow(); + } } protected abstract boolean useJsSandbox(); @@ -69,34 +121,49 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer @Override protected ListenableFuture doEval(UUID scriptId, String functionName, String jsScript) { - try { - if (useJsSandbox()) { - sandbox.eval(jsScript); - } else { - engine.eval(jsScript); + jsPushedMsgs.incrementAndGet(); + ListenableFuture result = jsExecutor.executeAsync(() -> { + try { + if (useJsSandbox()) { + sandbox.eval(jsScript); + } else { + engine.eval(jsScript); + } + scriptIdToNameMap.put(scriptId, functionName); + return scriptId; + } catch (Exception e) { + log.warn("Failed to compile JS script: {}", e.getMessage(), e); + throw new ExecutionException(e); } - scriptIdToNameMap.put(scriptId, functionName); - } catch (Exception e) { - log.warn("Failed to compile JS script: {}", e.getMessage(), e); - return Futures.immediateFailedFuture(e); + }); + if (maxRequestsTimeout > 0) { + result = Futures.withTimeout(result, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); } - return Futures.immediateFuture(scriptId); + Futures.addCallback(result, evalCallback); + return result; } @Override protected ListenableFuture doInvokeFunction(UUID scriptId, String functionName, Object[] args) { - try { - Object result; - if (useJsSandbox()) { - result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); - } else { - result = ((Invocable) engine).invokeFunction(functionName, args); + jsPushedMsgs.incrementAndGet(); + ListenableFuture result = jsExecutor.executeAsync(() -> { + try { + if (useJsSandbox()) { + return sandbox.getSandboxedInvocable().invokeFunction(functionName, args); + } else { + return ((Invocable) engine).invokeFunction(functionName, args); + } + } catch (Exception e) { + onScriptExecutionError(scriptId); + throw new ExecutionException(e); } - return Futures.immediateFuture(result); - } catch (Exception e) { - onScriptExecutionError(scriptId); - return Futures.immediateFailedFuture(e); + }); + + if (maxRequestsTimeout > 0) { + result = Futures.withTimeout(result, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); } + Futures.addCallback(result, invokeCallback); + return result; } protected void doRelease(UUID scriptId, String functionName) throws ScriptException { diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsStatCallback.java b/application/src/main/java/org/thingsboard/server/service/script/JsStatCallback.java new file mode 100644 index 0000000000..cceba04f4f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/script/JsStatCallback.java @@ -0,0 +1,31 @@ +package org.thingsboard.server.service.script; + +import com.google.common.util.concurrent.FutureCallback; +import lombok.AllArgsConstructor; + +import javax.annotation.Nullable; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +@AllArgsConstructor +public class JsStatCallback implements FutureCallback { + + private final AtomicInteger jsSuccessMsgs; + private final AtomicInteger jsTimeoutMsgs; + private final AtomicInteger jsFailedMsgs; + + + @Override + public void onSuccess(@Nullable T result) { + jsSuccessMsgs.incrementAndGet(); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { + jsTimeoutMsgs.incrementAndGet(); + } else { + jsFailedMsgs.incrementAndGet(); + } + } +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index fcf62a659b..c03240e17f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -464,6 +464,11 @@ js: max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:3000}" # 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 + max_requests_timeout: "${LOCAL_JS_MAX_REQUEST_TIMEOUT:0}" + stats: + enabled: "${TB_JS_LOCAL_STATS_ENABLED:false}" + print_interval_ms: "${TB_JS_LOCAL_STATS_PRINT_INTERVAL_MS:10000}" # Remote JavaScript environment properties remote: # JS Eval request topic