diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java similarity index 100% rename from common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java rename to application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java index dd32920443..884fa2a4ee 100644 --- a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java +++ b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java @@ -26,6 +26,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.AbstractJsInvokeService; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -44,6 +45,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; @Slf4j @ConditionalOnExpression("'${js.evaluator:null}'=='remote' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine')") @@ -137,7 +139,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { return compiledScriptId; } else { log.debug("[{}] Failed to compile script due to [{}]: {}", compiledScriptId, compilationResult.getErrorCode().name(), compilationResult.getErrorDetails()); - throw new RuntimeException(compilationResult.getErrorDetails()); + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, new RuntimeException(compilationResult.getErrorDetails())); } }, callbackExecutor); } @@ -182,15 +184,14 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { return invokeResult.getResult(); } else { final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails()); - if (JsInvokeProtos.JsInvokeErrorCode.TIMEOUT_ERROR.equals(invokeResult.getErrorCode())) { - onScriptExecutionError(scriptId, e, scriptBody); - timeoutMsgs.incrementAndGet(); - } else if (JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR.equals(invokeResult.getErrorCode())) { - onScriptExecutionError(scriptId, e, scriptBody); - } - failedMsgs.incrementAndGet(); log.debug("[{}] Failed to invoke function due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails()); - throw e; + if (JsInvokeProtos.JsInvokeErrorCode.TIMEOUT_ERROR.equals(invokeResult.getErrorCode())) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.TIMEOUT, scriptBody, new TimeoutException()); + } else if (JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR.equals(invokeResult.getErrorCode())) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); + } else { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, scriptBody, e); + } } }, callbackExecutor); } 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 b035f6549e..634205f9c9 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 @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.id.CustomerId; @@ -35,6 +36,7 @@ import java.util.concurrent.Executor; 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; import static java.lang.String.format; @@ -42,7 +44,7 @@ import static java.lang.String.format; @Slf4j public abstract class AbstractScriptInvokeService implements ScriptInvokeService { - protected Map disabledScripts = new ConcurrentHashMap<>(); + protected Map disabledScripts = new ConcurrentHashMap<>(); private final Optional apiUsageStateClient; private final Optional apiUsageReportClient; @@ -118,7 +120,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } } - @Override public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) { @@ -127,7 +128,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } UUID scriptId = UUID.randomUUID(); pushedMsgs.incrementAndGet(); - return withTimeoutAndStatsCallback(doEvalScript(scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); + return withTimeoutAndStatsCallback(scriptId, doEvalScript(scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } else { return error("Script Execution is disabled due to API limits!"); } @@ -141,20 +142,26 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } if (!isDisabled(scriptId)) { if (argsSizeExceeded(args)) { - return scriptExecutionError(scriptId, format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize())); + TbScriptException t = new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new IllegalArgumentException( + format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize()) + )); + handleScriptException(scriptId, t); + return Futures.immediateFailedFuture(t); } apiUsageReportClient.ifPresent(client -> client.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1)); pushedMsgs.incrementAndGet(); - log.trace("invokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout()); + log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout()); var resultFuture = Futures.transformAsync(doInvokeFunction(scriptId, args), output -> { String result = output.toString(); if (resultSizeExceeded(result)) { - return scriptExecutionError(scriptId, format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize())); + 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(result); }, MoreExecutors.directExecutor()); - return withTimeoutAndStatsCallback(resultFuture, invokeCallback, getMaxInvokeRequestsTimeout()); + return withTimeoutAndStatsCallback(scriptId, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout()); } else { String message = "Script invocation is blocked due to maximum error count " + getMaxErrors() + ", scriptId " + scriptId + "!"; @@ -162,18 +169,63 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return error(message); } } else { - return error("JS Execution is disabled due to API limits!"); + return error("Script execution is disabled due to API limits!"); } } - private ListenableFuture withTimeoutAndStatsCallback(ListenableFuture future, FutureCallback statsCallback, long timeout) { + private ListenableFuture withTimeoutAndStatsCallback(UUID scriptId, ListenableFuture future, FutureCallback statsCallback, long timeout) { if (timeout > 0) { future = Futures.withTimeout(future, timeout, TimeUnit.MILLISECONDS, timeoutExecutorService); } Futures.addCallback(future, statsCallback, getCallbackExecutor()); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable T result) { + //do nothing + } + + @Override + public void onFailure(Throwable t) { + handleScriptException(scriptId, t); + } + }, getCallbackExecutor()); return future; } + private void handleScriptException(UUID scriptId, Throwable t) { + boolean blockList = t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException); + String scriptBody = null; + if (t instanceof TbScriptException) { + var scriptException = (TbScriptException) t; + scriptBody = scriptException.getBody(); + var cause = scriptException.getCause(); + switch (scriptException.getErrorCode()) { + case COMPILATION: + log.debug("[{}] Failed to compile script: {}", scriptId, scriptException.getBody(), cause); + break; + case TIMEOUT: + log.debug("[{}] Timeout to execute script: {}", scriptId, scriptException.getBody(), cause); + break; + case OTHER: + case RUNTIME: + log.debug("[{}] Failed to execute script: {}", scriptId, scriptException.getBody(), cause); + break; + } + blockList = blockList || scriptException.getErrorCode() != TbScriptException.ErrorCode.RUNTIME; + } + if (blockList) { + BlockedScriptInfo disableListInfo = disabledScripts.computeIfAbsent(scriptId, key -> new BlockedScriptInfo(getMaxBlackListDurationSec())); + if (log.isDebugEnabled()) { + log.debug("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}", + disableListInfo.get(), scriptId, t, t.getCause(), scriptBody); + } else { + log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}", + disableListInfo.get(), scriptId, t.getMessage()); + } + disableListInfo.incrementAndGet(); + } + } + @Override public ListenableFuture release(UUID scriptId) { if (isScriptPresent(scriptId)) { @@ -188,7 +240,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } private boolean isDisabled(UUID scriptId) { - DisableListInfo errorCount = disabledScripts.get(scriptId); + BlockedScriptInfo errorCount = disabledScripts.get(scriptId); if (errorCount != null) { if (errorCount.getExpirationTime() <= System.currentTimeMillis()) { disabledScripts.remove(scriptId); @@ -225,41 +277,4 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService private ListenableFuture error(String message) { return Futures.immediateFailedFuture(new RuntimeException(message)); } - - protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) { - DisableListInfo disableListInfo = disabledScripts.computeIfAbsent(scriptId, key -> new DisableListInfo()); - log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}", - disableListInfo.get(), scriptId, t, t.getCause(), scriptBody); - disableListInfo.incrementAndGet(); - } - - private ListenableFuture scriptExecutionError(UUID scriptId, String errorMsg) { - RuntimeException error = new RuntimeException(errorMsg); - onScriptExecutionError(scriptId, error, null); - return Futures.immediateFailedFuture(error); - } - - private class DisableListInfo { - private final AtomicInteger counter; - private long expirationTime; - - private DisableListInfo() { - this.counter = new AtomicInteger(0); - } - - public int get() { - return counter.get(); - } - - public int incrementAndGet() { - int result = counter.incrementAndGet(); - expirationTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(getMaxBlackListDurationSec()); - return result; - } - - public long getExpirationTime() { - return expirationTime; - } - } - } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/BlockedScriptInfo.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/BlockedScriptInfo.java new file mode 100644 index 0000000000..22d98e2035 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/BlockedScriptInfo.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.thingsboard.script.api; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class BlockedScriptInfo { + private final long maxScriptBlockDurationMs; + private final AtomicInteger counter; + private long expirationTime; + + BlockedScriptInfo(int maxScriptBlockDuration) { + this.maxScriptBlockDurationMs = TimeUnit.SECONDS.toMillis(maxScriptBlockDuration); + this.counter = new AtomicInteger(0); + } + + public int get() { + return counter.get(); + } + + public int incrementAndGet() { + int result = counter.incrementAndGet(); + expirationTime = System.currentTimeMillis() + maxScriptBlockDurationMs; + return result; + } + + public long getExpirationTime() { + return expirationTime; + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java index dbf31e9ec0..aa32875ebe 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java @@ -17,11 +17,13 @@ package org.thingsboard.script.api; import com.google.common.util.concurrent.FutureCallback; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +@Slf4j @AllArgsConstructor public class ScriptStatCallback implements FutureCallback { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java new file mode 100644 index 0000000000..8c6bcb61bb --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.thingsboard.script.api; + +import lombok.Getter; + +import java.util.UUID; + +public class TbScriptException extends RuntimeException { + private static final long serialVersionUID = -1958193538782818284L; + + public static enum ErrorCode {COMPILATION, TIMEOUT, RUNTIME, OTHER} + + @Getter + private final UUID scriptId; + @Getter + private final ErrorCode errorCode; + @Getter + private final String body; + + public TbScriptException(UUID scriptId, ErrorCode errorCode, String body, Exception cause) { + super(cause); + this.scriptId = scriptId; + this.errorCode = errorCode; + this.body = body; + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java new file mode 100644 index 0000000000..3be5bbc474 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java @@ -0,0 +1,6 @@ +package org.thingsboard.script.api.mvel; + +import org.thingsboard.script.api.ScriptInvokeService; + +public interface MvelInvokeService extends ScriptInvokeService { +} 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 bb31a6c155..559731c025 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 @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient; @@ -38,7 +39,6 @@ import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -146,8 +146,7 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { scriptIdToNameMap.put(scriptId, functionName); return scriptId; } catch (Exception e) { - log.debug("Failed to compile JS script: {}", e.getMessage(), e); - throw new ExecutionException(e); + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e); } }); } @@ -162,10 +161,9 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { return ((Invocable) engine).invokeFunction(functionName, args); } } catch (ScriptException e) { - throw new ExecutionException(e); + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, e); } catch (Exception e) { - onScriptExecutionError(scriptId, e, functionName); - throw new ExecutionException(e); + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, e); } }); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java new file mode 100644 index 0000000000..158ede21e8 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java @@ -0,0 +1,164 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * 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 org.thingsboard.script.api.mvel; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.mvel2.MVEL; +import org.mvel2.ParserContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.script.api.AbstractScriptInvokeService; +import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; +import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.common.stats.TbApiUsageStateClient; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.regex.Pattern; + +@Slf4j +@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "enabled", matchIfMissing = true) +@Service +public class DefaultMvelInvokeService extends AbstractScriptInvokeService { + + protected Map scriptMap = new ConcurrentHashMap<>(); + private ParserContext parserContext; + + private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s"); + + @Getter + @Value("${mvel.max_total_args_size:100000}") + private long maxTotalArgsSize; + @Getter + @Value("${mvel.max_result_size:300000}") + private long maxResultSize; + @Getter + @Value("${mvel.max_script_body_size:50000}") + private long maxScriptBodySize; + + @Getter + @Value("${mvel.max_errors:3}") + private int maxErrors; + + @Getter + @Value("${mvel.max_black_list_duration_sec:60}") + private int maxBlackListDurationSec; + + @Getter + @Value("${mvel.max_requests_timeout:0}") + private long maxInvokeRequestsTimeout; + + @Getter + @Value("${mvel.stats.enabled:false}") + private boolean statsEnabled; + + private ListeningExecutorService executor; + + protected DefaultMvelInvokeService(Optional apiUsageStateClient, Optional apiUsageReportClient) { + super(apiUsageStateClient, apiUsageReportClient); + } + + @Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}") + public void printStats() { + super.printStats(); + } + + @PostConstruct + public void init() { + super.init(); + parserContext = new ParserContext(new TbMvelParserConfiguration()); + executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(2, "mvel-executor")); + } + + @PreDestroy + public void destroy() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Override + protected String getStatsName() { + return "MVEL Scripts Stats"; + } + + @Override + protected Executor getCallbackExecutor() { + return MoreExecutors.directExecutor(); + } + + @Override + protected boolean isScriptPresent(UUID scriptId) { + return scriptMap.containsKey(scriptId); + } + + @Override + protected ListenableFuture doEvalScript(ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { + if (NEW_KEYWORD_PATTERN.matcher(scriptBody).matches()) { + //TODO: output line number and char pos. + return Futures.immediateFailedFuture(new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, + new IllegalArgumentException("Keyword 'new' is forbidden!"))); + } + return executor.submit(() -> { + try { + Serializable compiledScript = MVEL.compileExpression(scriptBody, parserContext); + MvelScript script = new MvelScript(compiledScript, scriptBody, argNames); + scriptMap.put(scriptId, script); + return scriptId; + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); + } + }); + } + + @Override + protected ListenableFuture doInvokeFunction(UUID scriptId, Object[] args) { + return executor.submit(() -> { + MvelScript script = scriptMap.get(scriptId); + if (script == null) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!")); + } + try { + return MVEL.executeExpression(script.getCompiledScript(), script.createVars(args)); + } catch (OutOfMemoryError e) { + Runtime.getRuntime().gc(); + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Memory error!")); + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, script.getScriptBody(), e); + } + }); + } + + @Override + protected void doRelease(UUID scriptId) throws Exception { + scriptMap.remove(scriptId); + } +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java index 52dc3df180..f39b71bd53 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java @@ -1,152 +1,2 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 - * - * 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 org.thingsboard.script.api.mvel; - -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.mvel2.MVEL; -import org.mvel2.ParserContext; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.script.api.AbstractScriptInvokeService; -import org.thingsboard.script.api.ScriptType; -import org.thingsboard.server.common.stats.TbApiUsageReportClient; -import org.thingsboard.server.common.stats.TbApiUsageStateClient; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; - -@Slf4j -public class MvelInvokeService extends AbstractScriptInvokeService { - - protected Map scriptMap = new ConcurrentHashMap<>(); - private ParserContext parserContext; - - @Getter - @Value("${mvel.max_total_args_size:100000}") - private long maxTotalArgsSize; - @Getter - @Value("${mvel.max_result_size:300000}") - private long maxResultSize; - @Getter - @Value("${mvel.max_script_body_size:50000}") - private long maxScriptBodySize; - - @Getter - @Value("${mvel.max_errors:3}") - private int maxErrors; - - @Getter - @Value("${mvel.max_black_list_duration_sec:60}") - private int maxBlackListDurationSec; - - @Getter - @Value("${mvel.max_requests_timeout:0}") - private long maxInvokeRequestsTimeout; - - @Getter - @Value("${mvel.stats.enabled:false}") - private boolean statsEnabled; - - private ListeningExecutorService executor; - - protected MvelInvokeService(Optional apiUsageStateClient, Optional apiUsageReportClient) { - super(apiUsageStateClient, apiUsageReportClient); - } - - @Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}") - public void printStats() { - super.printStats(); - } - - @PostConstruct - public void init() { - super.init(); - parserContext = new ParserContext(new TbMvelParserConfiguration()); - executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(2, "mvel-executor")); - } - - @PreDestroy - public void destroy() { - if (executor != null) { - executor.shutdownNow(); - } - } - - @Override - protected String getStatsName() { - return "MVEL Scripts Stats"; - } - - @Override - protected Executor getCallbackExecutor() { - return MoreExecutors.directExecutor(); - } - - @Override - protected boolean isScriptPresent(UUID scriptId) { - return scriptMap.containsKey(scriptId); - } - - @Override - protected ListenableFuture doEvalScript(ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { - //TODO: executor, check expression for "new" and ? - return executor.submit(() -> { - try { - Serializable compiledScript = MVEL.compileExpression(scriptBody, parserContext); - MvelScript script = new MvelScript(compiledScript, argNames); - scriptMap.put(scriptId, script); - return scriptId; - } catch (Exception e) { - log.debug("Failed to compile MVEL script: {}", scriptBody, e); - throw new ExecutionException(e); - } - }); - } - - @Override - protected ListenableFuture doInvokeFunction(UUID scriptId, Object[] args) { - return executor.submit(() -> { - MvelScript script = scriptMap.get(scriptId); - if (script == null) { - throw new RuntimeException("Script not found!"); - } - try { - return MVEL.executeExpression(script.getCompiledScript(), script.createVars(args)); - } catch (OutOfMemoryError e) { - Runtime.getRuntime().gc(); - throw new RuntimeException("Memory error!"); - } - }); - } - - @Override - protected void doRelease(UUID scriptId) throws Exception { - scriptMap.remove(scriptId); - } +package org.thingsboard.script.api.mvel;public interface MvelInvokeService { } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java index 508e61fd24..7a84c7b0af 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java @@ -25,6 +25,7 @@ import java.util.Map; public class MvelScript { private final Serializable compiledScript; + private final String scriptBody; private final String[] argNames; public Map createVars(Object[] args) {