MVEL Script Service implementation
This commit is contained in:
parent
813f632deb
commit
1593c5b92e
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package org.thingsboard.script.api.mvel;
|
||||
|
||||
import org.thingsboard.script.api.ScriptInvokeService;
|
||||
|
||||
public interface MvelInvokeService extends ScriptInvokeService {
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user