@ -15,6 +15,10 @@
* /
* /
package org.thingsboard.script.api.mvel ;
package org.thingsboard.script.api.mvel ;
import com.github.benmanes.caffeine.cache.Cache ;
import com.github.benmanes.caffeine.cache.Caffeine ;
import com.google.common.hash.Hasher ;
import com.google.common.hash.Hashing ;
import com.google.common.util.concurrent.ListenableFuture ;
import com.google.common.util.concurrent.ListenableFuture ;
import com.google.common.util.concurrent.ListeningExecutorService ;
import com.google.common.util.concurrent.ListeningExecutorService ;
import com.google.common.util.concurrent.MoreExecutors ;
import com.google.common.util.concurrent.MoreExecutors ;
@ -42,12 +46,15 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import javax.annotation.PostConstruct ;
import javax.annotation.PostConstruct ;
import javax.annotation.PreDestroy ;
import javax.annotation.PreDestroy ;
import java.io.Serializable ;
import java.io.Serializable ;
import java.nio.charset.StandardCharsets ;
import java.util.Collections ;
import java.util.Collections ;
import java.util.Map ;
import java.util.Map ;
import java.util.Optional ;
import java.util.Optional ;
import java.util.UUID ;
import java.util.UUID ;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.ConcurrentHashMap ;
import java.util.concurrent.Executor ;
import java.util.concurrent.Executor ;
import java.util.concurrent.locks.Lock ;
import java.util.concurrent.locks.ReentrantLock ;
import java.util.regex.Pattern ;
import java.util.regex.Pattern ;
@Slf4j
@Slf4j
@ -55,7 +62,10 @@ import java.util.regex.Pattern;
@Service
@Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
protected Map < UUID , MvelScript > scriptMap = new ConcurrentHashMap < > ( ) ;
protected final Map < UUID , String > scriptIdToHash = new ConcurrentHashMap < > ( ) ;
protected final Map < String , MvelScript > scriptMap = new ConcurrentHashMap < > ( ) ;
protected Cache < String , Serializable > compiledScriptsCache ;
private SandboxedParserConfiguration parserConfig ;
private SandboxedParserConfiguration parserConfig ;
private static final Pattern NEW_KEYWORD_PATTERN = Pattern . compile ( "new\\s" ) ;
private static final Pattern NEW_KEYWORD_PATTERN = Pattern . compile ( "new\\s" ) ;
@ -92,8 +102,13 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Value ( "${mvel.max_memory_limit_mb:8}" )
@Value ( "${mvel.max_memory_limit_mb:8}" )
private long maxMemoryLimitMb ;
private long maxMemoryLimitMb ;
@Value ( "${mvel.compiled_scripts_cache_size:1000}" )
private int compiledScriptsCacheSize ;
private ListeningExecutorService executor ;
private ListeningExecutorService executor ;
private final Lock lock = new ReentrantLock ( ) ;
protected DefaultMvelInvokeService ( Optional < TbApiUsageStateClient > apiUsageStateClient , Optional < TbApiUsageReportClient > apiUsageReportClient ) {
protected DefaultMvelInvokeService ( Optional < TbApiUsageStateClient > apiUsageStateClient , Optional < TbApiUsageReportClient > apiUsageReportClient ) {
super ( apiUsageStateClient , apiUsageReportClient ) ;
super ( apiUsageStateClient , apiUsageReportClient ) ;
}
}
@ -115,11 +130,14 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
executor = MoreExecutors . listeningDecorator ( ThingsBoardExecutors . newWorkStealingPool ( threadPoolSize , "mvel-executor" ) ) ;
executor = MoreExecutors . listeningDecorator ( ThingsBoardExecutors . newWorkStealingPool ( threadPoolSize , "mvel-executor" ) ) ;
try {
try {
// Special command to warm up MVEL engine
// Special command to warm up MVEL engine
Serializable script = MVEL . compileExpression ( "var warmUp = {}; warmUp" , new SandboxedParserContext ( parserConfig ) ) ;
Serializable script = compileScript ( "var warmUp = {}; warmUp" ) ;
MVEL . executeTbExpression ( script , new ExecutionContext ( parserConfig ) , Collections . emptyMap ( ) ) ;
MVEL . executeTbExpression ( script , new ExecutionContext ( parserConfig ) , Collections . emptyMap ( ) ) ;
} catch ( Exception e ) {
} catch ( Exception e ) {
// do nothing
// do nothing
}
}
compiledScriptsCache = Caffeine . newBuilder ( )
. maximumSize ( compiledScriptsCacheSize )
. build ( ) ;
}
}
@PreDestroy
@PreDestroy
@ -141,16 +159,26 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override
@Override
protected boolean isScriptPresent ( UUID scriptId ) {
protected boolean isScriptPresent ( UUID scriptId ) {
return scriptMap . containsKey ( scriptId ) ;
return scriptIdToHash . containsKey ( scriptId ) ;
}
}
@Override
@Override
protected ListenableFuture < UUID > doEvalScript ( TenantId tenantId , ScriptType scriptType , String scriptBody , UUID scriptId , String [ ] argNames ) {
protected ListenableFuture < UUID > doEvalScript ( TenantId tenantId , ScriptType scriptType , String scriptBody , UUID scriptId , String [ ] argNames ) {
return executor . submit ( ( ) - > {
return executor . submit ( ( ) - > {
try {
try {
Serializable compiledScript = MVEL . compileExpression ( scriptBody , new SandboxedParserContext ( parserConfig ) ) ;
String scriptHash = hash ( scriptBody , argNames ) ;
MvelScript script = new MvelScript ( compiledScript , scriptBody , argNames ) ;
compiledScriptsCache . get ( scriptHash , k - > {
scriptMap . put ( scriptId , script ) ;
return compileScript ( scriptBody ) ;
} ) ;
lock . lock ( ) ;
try {
scriptIdToHash . put ( scriptId , scriptHash ) ;
scriptMap . computeIfAbsent ( scriptHash , k - > {
return new MvelScript ( scriptBody , argNames ) ;
} ) ;
} finally {
lock . unlock ( ) ;
}
return scriptId ;
return scriptId ;
} catch ( Exception e ) {
} catch ( Exception e ) {
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . COMPILATION , scriptBody , e ) ;
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . COMPILATION , scriptBody , e ) ;
@ -162,12 +190,16 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
protected MvelScriptExecutionTask doInvokeFunction ( UUID scriptId , Object [ ] args ) {
protected MvelScriptExecutionTask doInvokeFunction ( UUID scriptId , Object [ ] args ) {
ExecutionContext executionContext = new ExecutionContext ( this . parserConfig , maxMemoryLimitMb * 1024 * 1024 ) ;
ExecutionContext executionContext = new ExecutionContext ( this . parserConfig , maxMemoryLimitMb * 1024 * 1024 ) ;
return new MvelScriptExecutionTask ( executionContext , executor . submit ( ( ) - > {
return new MvelScriptExecutionTask ( executionContext , executor . submit ( ( ) - > {
MvelScript script = scriptMap . get ( scriptId ) ;
String scriptHash = scriptIdToHash . get ( scriptId ) ;
if ( script = = null ) {
if ( scriptHash = = null ) {
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . OTHER , null , new RuntimeException ( "Script not found!" ) ) ;
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . OTHER , null , new RuntimeException ( "Script not found!" ) ) ;
}
}
MvelScript script = scriptMap . get ( scriptHash ) ;
Serializable compiledScript = compiledScriptsCache . get ( scriptHash , k - > {
return compileScript ( script . getScriptBody ( ) ) ;
} ) ;
try {
try {
return MVEL . executeTbExpression ( script . getCompiledScript ( ) , executionContext , script . createVars ( args ) ) ;
return MVEL . executeTbExpression ( compiledScript , executionContext , script . createVars ( args ) ) ;
} catch ( ScriptMemoryOverflowException e ) {
} catch ( ScriptMemoryOverflowException e ) {
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . OTHER , script . getScriptBody ( ) , new RuntimeException ( "Script memory overflow!" ) ) ;
throw new TbScriptException ( scriptId , TbScriptException . ErrorCode . OTHER , script . getScriptBody ( ) , new RuntimeException ( "Script memory overflow!" ) ) ;
} catch ( Exception e ) {
} catch ( Exception e ) {
@ -178,6 +210,32 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override
@Override
protected void doRelease ( UUID scriptId ) throws Exception {
protected void doRelease ( UUID scriptId ) throws Exception {
scriptMap . remove ( scriptId ) ;
String scriptHash = scriptIdToHash . remove ( scriptId ) ;
if ( scriptHash ! = null ) {
lock . lock ( ) ;
try {
if ( ! scriptIdToHash . containsValue ( scriptHash ) ) {
scriptMap . remove ( scriptHash ) ;
compiledScriptsCache . invalidate ( scriptHash ) ;
}
} finally {
lock . unlock ( ) ;
}
}
}
}
private Serializable compileScript ( String scriptBody ) {
return MVEL . compileExpression ( scriptBody , new SandboxedParserContext ( parserConfig ) ) ;
}
@SuppressWarnings ( "UnstableApiUsage" )
protected String hash ( String scriptBody , String [ ] argNames ) {
Hasher hasher = Hashing . murmur3_128 ( ) . newHasher ( ) ;
hasher . putUnencodedChars ( scriptBody ) ;
for ( String argName : argNames ) {
hasher . putString ( argName , StandardCharsets . UTF_8 ) ;
}
return hasher . hash ( ) . toString ( ) ;
}
}
}