11 changed files with 332 additions and 211 deletions
@ -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; |
|||
} |
|||
} |
|||
@ -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 { |
|||
} |
|||
@ -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 { |
|||
} |
|||
|
|||
Loading…
Reference in new issue