MVEL Script Service implementation

This commit is contained in:
Andrii Shvaika 2022-10-18 15:32:22 +03:00
parent 813f632deb
commit 1593c5b92e
11 changed files with 334 additions and 213 deletions

View File

@ -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);
}

View File

@ -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<UUID, DisableListInfo> disabledScripts = new ConcurrentHashMap<>();
protected Map<UUID, BlockedScriptInfo> disabledScripts = new ConcurrentHashMap<>();
private final Optional<TbApiUsageStateClient> apiUsageStateClient;
private final Optional<TbApiUsageReportClient> apiUsageReportClient;
@ -118,7 +120,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
}
}
@Override
public ListenableFuture<UUID> 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 <T extends V, V> ListenableFuture<T> withTimeoutAndStatsCallback(ListenableFuture<T> future, FutureCallback<V> statsCallback, long timeout) {
private <T extends V, V> ListenableFuture<T> withTimeoutAndStatsCallback(UUID scriptId, ListenableFuture<T> future, FutureCallback<V> statsCallback, long timeout) {
if (timeout > 0) {
future = Futures.withTimeout(future, timeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
Futures.addCallback(future, statsCallback, getCallbackExecutor());
Futures.addCallback(future, new FutureCallback<T>() {
@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<Void> 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 <T> ListenableFuture<T> 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 <T> ListenableFuture<T> 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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<T> implements FutureCallback<T> {

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
package org.thingsboard.script.api.mvel;
import org.thingsboard.script.api.ScriptInvokeService;
public interface MvelInvokeService extends ScriptInvokeService {
}

View File

@ -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);
}
});
}

View File

@ -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<UUID, MvelScript> 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<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> 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<UUID> 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<Object> 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);
}
}

View File

@ -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<UUID, MvelScript> 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<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> 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<UUID> 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<Object> 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 {
}

View File

@ -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) {