[automation] Synchronize script action/condition execution if engine implements Lock (#4426)

This moves the locking mechanism added in #4402 to the inheritors of AbstractScriptModuleHandler
to synchronize execution context access as well.

This fixes the problem thathttps://github.com/openhab/openhab-addons/pull/17510 worked around by using Thread.sleep.

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze 2024-10-26 23:26:20 +02:00 committed by GitHub
parent 52b26baf17
commit 922a2068c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 31 deletions

View File

@ -17,8 +17,6 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import javax.script.Compilable; import javax.script.Compilable;
import javax.script.CompiledScript; import javax.script.CompiledScript;
@ -38,6 +36,8 @@ import org.slf4j.LoggerFactory;
/** /**
* This is an abstract class that can be used when implementing any module handler that handles scripts. * This is an abstract class that can be used when implementing any module handler that handles scripts.
* <p>
* Remember to implement multi-thread synchronization in the concrete handler if the script engine is not thread-safe!
* *
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution * @author Simon Merschjohann - Initial contribution
@ -212,28 +212,14 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
protected @Nullable Object eval(ScriptEngine engine, String script) { protected @Nullable Object eval(ScriptEngine engine, String script) {
try { try {
if (compiledScript.isPresent()) { if (compiledScript.isPresent()) {
if (engine instanceof Lock lock && !lock.tryLock(1, TimeUnit.MINUTES)) {
logger.error("Failed to acquire lock within one minute for script module of rule with UID '{}'",
ruleUID);
return null;
}
logger.debug("Executing pre-compiled script of rule with UID '{}'", ruleUID); logger.debug("Executing pre-compiled script of rule with UID '{}'", ruleUID);
try { return compiledScript.get().eval(engine.getContext());
return compiledScript.get().eval(engine.getContext());
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
// deadlocks
if (engine instanceof Lock lock) {
lock.unlock();
}
}
} }
logger.debug("Executing script of rule with UID '{}'", ruleUID); logger.debug("Executing script of rule with UID '{}'", ruleUID);
return engine.eval(script); return engine.eval(script);
} catch (ScriptException e) { } catch (ScriptException e) {
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(), logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
logger.isDebugEnabled() ? e : null); logger.isDebugEnabled() ? e : null);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} }
return null; return null;
} }

View File

@ -14,6 +14,8 @@ package org.openhab.core.automation.module.script.internal.handler;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.script.ScriptException; import javax.script.ScriptException;
@ -31,7 +33,8 @@ import org.slf4j.LoggerFactory;
* *
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution * @author Simon Merschjohann - Initial contribution
* @author Florian Hotze - Add support for script pre-compilation * @author Florian Hotze - Add support for script pre-compilation, Synchronize script context access if the ScriptEngine
* implements locking
*/ */
@NonNullByDefault @NonNullByDefault
public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> implements ActionHandler { public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> implements ActionHandler {
@ -76,10 +79,27 @@ public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> imp
} }
getScriptEngine().ifPresent(scriptEngine -> { getScriptEngine().ifPresent(scriptEngine -> {
setExecutionContext(scriptEngine, context); try {
Object result = eval(scriptEngine, script); if (scriptEngine instanceof Lock lock && !lock.tryLock(1, TimeUnit.MINUTES)) {
resultMap.put("result", result); logger.error(
resetExecutionContext(scriptEngine, context); "Failed to acquire lock within one minute for script module '{}' of rule with UID '{}'",
module.getId(), ruleUID);
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
setExecutionContext(scriptEngine, context);
Object result = eval(scriptEngine, script);
resultMap.put("result", result);
resetExecutionContext(scriptEngine, context);
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
// deadlocks
if (scriptEngine instanceof Lock lock) {
lock.unlock();
}
}
}); });
return resultMap; return resultMap;

View File

@ -14,6 +14,8 @@ package org.openhab.core.automation.module.script.internal.handler;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
import javax.script.ScriptException; import javax.script.ScriptException;
@ -30,7 +32,8 @@ import org.slf4j.LoggerFactory;
* *
* @author Kai Kreuzer - Initial contribution * @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution * @author Simon Merschjohann - Initial contribution
* @author Florian Hotze - Add support for script pre-compilation * @author Florian Hotze - Add support for script pre-compilation, Synchronize script context access if the ScriptEngine
* implements locking
*/ */
@NonNullByDefault @NonNullByDefault
public class ScriptConditionHandler extends AbstractScriptModuleHandler<Condition> implements ConditionHandler { public class ScriptConditionHandler extends AbstractScriptModuleHandler<Condition> implements ConditionHandler {
@ -60,15 +63,32 @@ public class ScriptConditionHandler extends AbstractScriptModuleHandler<Conditio
if (engine.isPresent()) { if (engine.isPresent()) {
ScriptEngine scriptEngine = engine.get(); ScriptEngine scriptEngine = engine.get();
setExecutionContext(scriptEngine, context); try {
Object returnVal = eval(scriptEngine, script); if (scriptEngine instanceof Lock lock && !lock.tryLock(1, TimeUnit.MINUTES)) {
if (returnVal instanceof Boolean boolean1) { logger.error(
result = boolean1; "Failed to acquire lock within one minute for script module '{}' of rule with UID '{}'",
} else { module.getId(), ruleUID);
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID, return result;
returnVal); }
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
setExecutionContext(scriptEngine, context);
Object returnVal = eval(scriptEngine, script);
if (returnVal instanceof Boolean boolean1) {
result = boolean1;
} else {
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID,
returnVal);
}
resetExecutionContext(scriptEngine, context);
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
// deadlocks
if (scriptEngine instanceof Lock lock) {
lock.unlock();
}
} }
resetExecutionContext(scriptEngine, context);
} }
return result; return result;