Fix CME when creating SCRIPT transformations (#3188)

When updating file-based script transformations used in item state-descripting a CME can occur during reload. This is fixed by using thread-safe implementation of `HashMap`.

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3202/head
J-N-K 2022-12-04 17:56:20 +01:00 committed by GitHub
parent 61bb5b3a31
commit b481ee73c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 91 additions and 69 deletions

View File

@ -12,10 +12,12 @@
*/ */
package org.openhab.core.automation.module.script; package org.openhab.core.automation.module.script;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -60,9 +62,7 @@ public class ScriptTransformationService implements TransformationService, Regis
private final ScheduledExecutorService scheduler = ThreadPoolManager private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
private final Map<String, ScriptEngineContainer> scriptEngineContainers = new HashMap<>(); private final Map<String, ScriptRecord> scriptCache = new ConcurrentHashMap<>();
private final Map<String, CompiledScript> compiledScripts = new HashMap<>();
private final Map<String, String> scriptCache = new HashMap<>();
private final TransformationRegistry transformationRegistry; private final TransformationRegistry transformationRegistry;
private final ScriptEngineManager scriptEngineManager; private final ScriptEngineManager scriptEngineManager;
@ -80,9 +80,7 @@ public class ScriptTransformationService implements TransformationService, Regis
transformationRegistry.removeRegistryChangeListener(this); transformationRegistry.removeRegistryChangeListener(this);
// cleanup script engines // cleanup script engines
scriptEngineContainers.values().stream().map(ScriptEngineContainer::getScriptEngine) scriptCache.values().forEach(this::disposeScriptRecord);
.forEach(this::disposeScriptEngine);
compiledScripts.values().stream().map(CompiledScript::getEngine).forEach(this::disposeScriptEngine);
} }
@Override @Override
@ -94,69 +92,79 @@ public class ScriptTransformationService implements TransformationService, Regis
String scriptType = configMatcher.group("scriptType"); String scriptType = configMatcher.group("scriptType");
String scriptUid = configMatcher.group("scriptUid"); String scriptUid = configMatcher.group("scriptUid");
String script = scriptCache.get(scriptUid); ScriptRecord scriptRecord = scriptCache.computeIfAbsent(scriptUid, k -> new ScriptRecord());
if (script == null) { scriptRecord.lock.lock();
Transformation transformation = transformationRegistry.get(scriptUid);
if (transformation != null) {
if (!SUPPORTED_CONFIGURATION_TYPE.equals(transformation.getType())) {
throw new TransformationException("Configuration does not have correct type 'script' but '"
+ transformation.getType() + "'.");
}
script = transformation.getConfiguration().get(Transformation.FUNCTION);
}
if (script == null) {
throw new TransformationException("Could not get script for UID '" + scriptUid + "'.");
}
scriptCache.put(scriptUid, script);
}
if (!scriptEngineManager.isSupported(scriptType)) {
// language has been removed, clear container and compiled scripts if found
if (scriptEngineContainers.containsKey(scriptUid)) {
scriptEngineManager.removeEngine(OPENHAB_TRANSFORMATION_SCRIPT + scriptUid);
}
clearCache(scriptUid);
throw new TransformationException(
"Script type '" + scriptType + "' is not supported by any available script engine.");
}
ScriptEngineContainer scriptEngineContainer = scriptEngineContainers.computeIfAbsent(scriptUid,
k -> scriptEngineManager.createScriptEngine(scriptType, OPENHAB_TRANSFORMATION_SCRIPT + k));
if (scriptEngineContainer == null) {
throw new TransformationException("Failed to create script engine container for '" + function + "'.");
}
try { try {
CompiledScript compiledScript = this.compiledScripts.get(scriptUid); if (scriptRecord.script.isBlank()) {
Transformation transformation = transformationRegistry.get(scriptUid);
if (compiledScript == null && scriptEngineContainer.getScriptEngine() instanceof Compilable) { if (transformation != null) {
// no compiled script available but compiling is supported if (!SUPPORTED_CONFIGURATION_TYPE.equals(transformation.getType())) {
compiledScript = ((Compilable) scriptEngineContainer.getScriptEngine()).compile(script); throw new TransformationException("Configuration does not have correct type 'script' but '"
this.compiledScripts.put(scriptUid, compiledScript); + transformation.getType() + "'.");
}
scriptRecord.script = transformation.getConfiguration().getOrDefault(Transformation.FUNCTION, "");
}
if (scriptRecord.script.isBlank()) {
throw new TransformationException("Could not get script for UID '" + scriptUid + "'.");
}
scriptCache.put(scriptUid, scriptRecord);
} }
ScriptEngine engine = compiledScript != null ? compiledScript.getEngine() if (!scriptEngineManager.isSupported(scriptType)) {
: scriptEngineContainer.getScriptEngine(); // language has been removed, clear container and compiled scripts if found
ScriptContext executionContext = engine.getContext(); if (scriptRecord.scriptEngineContainer != null) {
executionContext.setAttribute("input", source, ScriptContext.ENGINE_SCOPE); scriptEngineManager.removeEngine(OPENHAB_TRANSFORMATION_SCRIPT + scriptUid);
}
clearCache(scriptUid);
throw new TransformationException(
"Script type '" + scriptType + "' is not supported by any available script engine.");
}
String params = configMatcher.group("params"); if (scriptRecord.scriptEngineContainer == null) {
if (params != null) { scriptRecord.scriptEngineContainer = scriptEngineManager.createScriptEngine(scriptType,
for (String param : params.split("&")) { OPENHAB_TRANSFORMATION_SCRIPT + scriptUid);
String[] splitString = param.split("="); }
if (splitString.length != 2) { ScriptEngineContainer scriptEngineContainer = scriptRecord.scriptEngineContainer;
logger.warn("Parameter '{}' does not consist of two parts for configuration UID {}, skipping.",
param, scriptUid); if (scriptEngineContainer == null) {
} else { throw new TransformationException("Failed to create script engine container for '" + function + "'.");
executionContext.setAttribute(splitString[0], splitString[1], ScriptContext.ENGINE_SCOPE); }
try {
CompiledScript compiledScript = scriptRecord.compiledScript;
if (compiledScript == null && scriptEngineContainer.getScriptEngine() instanceof Compilable) {
// no compiled script available but compiling is supported
compiledScript = ((Compilable) scriptEngineContainer.getScriptEngine())
.compile(scriptRecord.script);
scriptRecord.compiledScript = compiledScript;
}
ScriptEngine engine = compiledScript != null ? compiledScript.getEngine()
: scriptEngineContainer.getScriptEngine();
ScriptContext executionContext = engine.getContext();
executionContext.setAttribute("input", source, ScriptContext.ENGINE_SCOPE);
String params = configMatcher.group("params");
if (params != null) {
for (String param : params.split("&")) {
String[] splitString = param.split("=");
if (splitString.length != 2) {
logger.warn(
"Parameter '{}' does not consist of two parts for configuration UID {}, skipping.",
param, scriptUid);
} else {
executionContext.setAttribute(splitString[0], splitString[1], ScriptContext.ENGINE_SCOPE);
}
} }
} }
}
Object result = compiledScript != null ? compiledScript.eval() : engine.eval(script); Object result = compiledScript != null ? compiledScript.eval() : engine.eval(scriptRecord.script);
return result == null ? null : result.toString(); return result == null ? null : result.toString();
} catch (ScriptException e) { } catch (ScriptException e) {
throw new TransformationException("Failed to execute script.", e); throw new TransformationException("Failed to execute script.", e);
}
} finally {
scriptRecord.lock.unlock();
} }
} }
@ -176,15 +184,21 @@ public class ScriptTransformationService implements TransformationService, Regis
} }
private void clearCache(String uid) { private void clearCache(String uid) {
CompiledScript compiledScript = compiledScripts.remove(uid); ScriptRecord scriptRecord = scriptCache.remove(uid);
if (scriptRecord != null) {
disposeScriptRecord(scriptRecord);
}
}
private void disposeScriptRecord(ScriptRecord scriptRecord) {
ScriptEngineContainer scriptEngineContainer = scriptRecord.scriptEngineContainer;
if (scriptEngineContainer != null) {
disposeScriptEngine(scriptEngineContainer.getScriptEngine());
}
CompiledScript compiledScript = scriptRecord.compiledScript;
if (compiledScript != null) { if (compiledScript != null) {
disposeScriptEngine(compiledScript.getEngine()); disposeScriptEngine(compiledScript.getEngine());
} }
ScriptEngineContainer container = scriptEngineContainers.remove(uid);
if (container != null) {
disposeScriptEngine(container.getScriptEngine());
}
scriptCache.remove(uid);
} }
private void disposeScriptEngine(ScriptEngine scriptEngine) { private void disposeScriptEngine(ScriptEngine scriptEngine) {
@ -204,4 +218,12 @@ public class ScriptTransformationService implements TransformationService, Regis
logger.trace("ScriptEngine does not support AutoCloseable interface"); logger.trace("ScriptEngine does not support AutoCloseable interface");
} }
} }
private static class ScriptRecord {
public String script = "";
public @Nullable ScriptEngineContainer scriptEngineContainer;
public @Nullable CompiledScript compiledScript;
public final Lock lock = new ReentrantLock();
}
} }