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.stereotype.Service;
|
||||||
import org.springframework.util.StopWatch;
|
import org.springframework.util.StopWatch;
|
||||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||||
|
import org.thingsboard.script.api.TbScriptException;
|
||||||
import org.thingsboard.script.api.js.AbstractJsInvokeService;
|
import org.thingsboard.script.api.js.AbstractJsInvokeService;
|
||||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
||||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
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.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnExpression("'${js.evaluator:null}'=='remote' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine')")
|
@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;
|
return compiledScriptId;
|
||||||
} else {
|
} else {
|
||||||
log.debug("[{}] Failed to compile script due to [{}]: {}", compiledScriptId, compilationResult.getErrorCode().name(), compilationResult.getErrorDetails());
|
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);
|
}, callbackExecutor);
|
||||||
}
|
}
|
||||||
@ -182,15 +184,14 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
|
|||||||
return invokeResult.getResult();
|
return invokeResult.getResult();
|
||||||
} else {
|
} else {
|
||||||
final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
|
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());
|
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);
|
}, 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.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
import org.thingsboard.common.util.ThingsBoardThreadFactory;
|
||||||
import org.thingsboard.server.common.data.ApiUsageRecordKey;
|
import org.thingsboard.server.common.data.ApiUsageRecordKey;
|
||||||
import org.thingsboard.server.common.data.id.CustomerId;
|
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.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
@ -42,7 +44,7 @@ import static java.lang.String.format;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractScriptInvokeService implements ScriptInvokeService {
|
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<TbApiUsageStateClient> apiUsageStateClient;
|
||||||
private final Optional<TbApiUsageReportClient> apiUsageReportClient;
|
private final Optional<TbApiUsageReportClient> apiUsageReportClient;
|
||||||
@ -118,7 +120,6 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
|
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
|
||||||
if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) {
|
if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) {
|
||||||
@ -127,7 +128,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
}
|
}
|
||||||
UUID scriptId = UUID.randomUUID();
|
UUID scriptId = UUID.randomUUID();
|
||||||
pushedMsgs.incrementAndGet();
|
pushedMsgs.incrementAndGet();
|
||||||
return withTimeoutAndStatsCallback(doEvalScript(scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
|
return withTimeoutAndStatsCallback(scriptId, doEvalScript(scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
|
||||||
} else {
|
} else {
|
||||||
return error("Script Execution is disabled due to API limits!");
|
return error("Script Execution is disabled due to API limits!");
|
||||||
}
|
}
|
||||||
@ -141,20 +142,26 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
}
|
}
|
||||||
if (!isDisabled(scriptId)) {
|
if (!isDisabled(scriptId)) {
|
||||||
if (argsSizeExceeded(args)) {
|
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));
|
apiUsageReportClient.ifPresent(client -> client.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1));
|
||||||
pushedMsgs.incrementAndGet();
|
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 -> {
|
var resultFuture = Futures.transformAsync(doInvokeFunction(scriptId, args), output -> {
|
||||||
String result = output.toString();
|
String result = output.toString();
|
||||||
if (resultSizeExceeded(result)) {
|
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);
|
return Futures.immediateFuture(result);
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
|
|
||||||
return withTimeoutAndStatsCallback(resultFuture, invokeCallback, getMaxInvokeRequestsTimeout());
|
return withTimeoutAndStatsCallback(scriptId, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout());
|
||||||
} else {
|
} else {
|
||||||
String message = "Script invocation is blocked due to maximum error count "
|
String message = "Script invocation is blocked due to maximum error count "
|
||||||
+ getMaxErrors() + ", scriptId " + scriptId + "!";
|
+ getMaxErrors() + ", scriptId " + scriptId + "!";
|
||||||
@ -162,18 +169,63 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
return error(message);
|
return error(message);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if (timeout > 0) {
|
||||||
future = Futures.withTimeout(future, timeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
|
future = Futures.withTimeout(future, timeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
|
||||||
}
|
}
|
||||||
Futures.addCallback(future, statsCallback, getCallbackExecutor());
|
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;
|
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
|
@Override
|
||||||
public ListenableFuture<Void> release(UUID scriptId) {
|
public ListenableFuture<Void> release(UUID scriptId) {
|
||||||
if (isScriptPresent(scriptId)) {
|
if (isScriptPresent(scriptId)) {
|
||||||
@ -188,7 +240,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDisabled(UUID scriptId) {
|
private boolean isDisabled(UUID scriptId) {
|
||||||
DisableListInfo errorCount = disabledScripts.get(scriptId);
|
BlockedScriptInfo errorCount = disabledScripts.get(scriptId);
|
||||||
if (errorCount != null) {
|
if (errorCount != null) {
|
||||||
if (errorCount.getExpirationTime() <= System.currentTimeMillis()) {
|
if (errorCount.getExpirationTime() <= System.currentTimeMillis()) {
|
||||||
disabledScripts.remove(scriptId);
|
disabledScripts.remove(scriptId);
|
||||||
@ -225,41 +277,4 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
|
|||||||
private <T> ListenableFuture<T> error(String message) {
|
private <T> ListenableFuture<T> error(String message) {
|
||||||
return Futures.immediateFailedFuture(new RuntimeException(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 com.google.common.util.concurrent.FutureCallback;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ScriptStatCallback<T> implements FutureCallback<T> {
|
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.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.thingsboard.common.util.ThingsBoardExecutors;
|
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.TbApiUsageReportClient;
|
||||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||||
|
|
||||||
@ -38,7 +39,6 @@ import javax.script.ScriptEngineManager;
|
|||||||
import javax.script.ScriptException;
|
import javax.script.ScriptException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@ -146,8 +146,7 @@ public class NashornJsInvokeService extends AbstractJsInvokeService {
|
|||||||
scriptIdToNameMap.put(scriptId, functionName);
|
scriptIdToNameMap.put(scriptId, functionName);
|
||||||
return scriptId;
|
return scriptId;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Failed to compile JS script: {}", e.getMessage(), e);
|
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e);
|
||||||
throw new ExecutionException(e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -162,10 +161,9 @@ public class NashornJsInvokeService extends AbstractJsInvokeService {
|
|||||||
return ((Invocable) engine).invokeFunction(functionName, args);
|
return ((Invocable) engine).invokeFunction(functionName, args);
|
||||||
}
|
}
|
||||||
} catch (ScriptException e) {
|
} catch (ScriptException e) {
|
||||||
throw new ExecutionException(e);
|
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
onScriptExecutionError(scriptId, e, functionName);
|
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, e);
|
||||||
throw new ExecutionException(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 @@
|
|||||||
/**
|
package org.thingsboard.script.api.mvel;public interface MvelInvokeService {
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import java.util.Map;
|
|||||||
public class MvelScript {
|
public class MvelScript {
|
||||||
|
|
||||||
private final Serializable compiledScript;
|
private final Serializable compiledScript;
|
||||||
|
private final String scriptBody;
|
||||||
private final String[] argNames;
|
private final String[] argNames;
|
||||||
|
|
||||||
public Map createVars(Object[] args) {
|
public Map createVars(Object[] args) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user