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