[rules] Add support for pre-compilation of conditions and actions (#4289)

* ScriptConditionHandler/ScriptActionHandler: Add support for pre-compilation of scripts

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
Florian Hotze 2024-07-09 19:12:33 +02:00 committed by GitHub
parent ea7d61b199
commit 918b4faa3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 219 additions and 19 deletions

View File

@ -18,10 +18,14 @@ import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.Module;
import org.openhab.core.automation.handler.BaseModuleHandler;
import org.openhab.core.automation.module.script.ScriptEngineContainer;
@ -35,6 +39,7 @@ import org.slf4j.LoggerFactory;
*
* @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution
* @author Florian Hotze - Add support for script pre-compilation
*
* @param <T> the type of module the concrete handler can handle
*/
@ -54,6 +59,7 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
private final String engineIdentifier;
private Optional<ScriptEngine> scriptEngine = Optional.empty();
private Optional<CompiledScript> compiledScript = Optional.empty();
private final String type;
protected final String script;
@ -80,6 +86,34 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
}
}
/**
* Creates the {@link ScriptEngine} and compiles the script if the {@link ScriptEngine} implements
* {@link Compilable}.
*/
protected void compileScript() throws ScriptException {
if (compiledScript.isPresent()) {
return;
}
if (!scriptEngineManager.isSupported(this.type)) {
logger.debug(
"ScriptEngine for language '{}' could not be found, skipping compilation of script for identifier: {}",
type, engineIdentifier);
return;
}
Optional<ScriptEngine> engine = getScriptEngine();
if (engine.isPresent()) {
ScriptEngine scriptEngine = engine.get();
if (scriptEngine instanceof Compilable) {
logger.debug("Pre-compiling script of rule with UID '{}'", ruleUID);
compiledScript = Optional.ofNullable(((Compilable) scriptEngine).compile(script));
} else {
logger.error(
"Script engine of rule with UID '{}' does not implement Compilable but claims to support pre-compilation",
module.getId());
}
}
}
@Override
public void dispose() {
scriptEngineManager.removeEngine(engineIdentifier);
@ -169,4 +203,26 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
executionContext.removeAttribute(key, ScriptContext.ENGINE_SCOPE);
}
}
/**
* Evaluates the passed script with the ScriptEngine.
*
* @param engine the script engine that is used
* @param script the script to evaluate
* @return the value returned from the execution of the script
*/
protected @Nullable Object eval(ScriptEngine engine, String script) {
try {
if (compiledScript.isPresent()) {
logger.debug("Executing pre-compiled script of rule with UID '{}'", ruleUID);
return compiledScript.get().eval(engine.getContext());
}
logger.debug("Executing script of rule with UID '{}'", ruleUID);
return engine.eval(script);
} catch (ScriptException e) {
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
logger.isDebugEnabled() ? e : null);
return null;
}
}
}

View File

@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
*
* @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution
* @author Florian Hotze - Add support for script pre-compilation
*/
@NonNullByDefault
public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> implements ActionHandler {
@ -61,6 +62,11 @@ public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> imp
super.dispose();
}
@Override
public void compile() throws ScriptException {
super.compileScript();
}
@Override
public @Nullable Map<String, Object> execute(final Map<String, Object> context) {
Map<String, Object> resultMap = new HashMap<>();
@ -71,13 +77,8 @@ public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> imp
getScriptEngine().ifPresent(scriptEngine -> {
setExecutionContext(scriptEngine, context);
try {
Object result = scriptEngine.eval(script);
resultMap.put("result", result);
} catch (ScriptException e) {
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
logger.isDebugEnabled() ? e : null);
}
Object result = eval(scriptEngine, script);
resultMap.put("result", result);
resetExecutionContext(scriptEngine, context);
});

View File

@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
*
* @author Kai Kreuzer - Initial contribution
* @author Simon Merschjohann - Initial contribution
* @author Florian Hotze - Add support for script pre-compilation
*/
@NonNullByDefault
public class ScriptConditionHandler extends AbstractScriptModuleHandler<Condition> implements ConditionHandler {
@ -42,6 +43,11 @@ public class ScriptConditionHandler extends AbstractScriptModuleHandler<Conditio
super(module, ruleUID, scriptEngineManager);
}
@Override
public void compile() throws ScriptException {
super.compileScript();
}
@Override
public boolean isSatisfied(final Map<String, Object> context) {
boolean result = false;
@ -55,18 +61,14 @@ public class ScriptConditionHandler extends AbstractScriptModuleHandler<Conditio
if (engine.isPresent()) {
ScriptEngine scriptEngine = engine.get();
setExecutionContext(scriptEngine, context);
try {
Object returnVal = scriptEngine.eval(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);
}
} catch (ScriptException e) {
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
logger.isDebugEnabled() ? e : null);
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);
}
return result;

View File

@ -31,6 +31,14 @@ import org.openhab.core.automation.Trigger;
*/
@NonNullByDefault
public interface ActionHandler extends ModuleHandler {
/**
* Called to compile an {@link Action} of the {@link Rule} when the rule is initialized.
*
* @throws Exception if the compilation fails
*/
default void compile() throws Exception {
// Do nothing by default
}
/**
* Called to execute an {@link Action} of the {@link Rule} when it is needed.

View File

@ -29,6 +29,14 @@ import org.openhab.core.automation.Trigger;
*/
@NonNullByDefault
public interface ConditionHandler extends ModuleHandler {
/**
* Called to compile the {@link Condition} when the {@link Rule} is initialized.
*
* @throws Exception if the compilation fails
*/
default void compile() throws Exception {
// Do nothing by default
}
/**
* Checks if the Condition is satisfied in the given {@code context}.

View File

@ -110,6 +110,7 @@ import org.slf4j.LoggerFactory;
* @author Benedikt Niehues - change behavior for unregistering ModuleHandler
* @author Markus Rathgeb - use a managed rule
* @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field
* @author Florian Hotze - add support for script condition/action compilation
*/
@Component(immediate = true, service = { RuleManager.class })
@NonNullByDefault
@ -819,6 +820,8 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
* <ul>
* <li>Set the module handlers. If there are errors, set the rule status (handler error) and return with error
* indication.
* <li>Compile the conditions and actions. If there are errors, set the rule status (handler error) and return with
* indication.
* <li>Register the rule. Set the rule status and return with success indication.
* </ul>
*
@ -845,6 +848,11 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
return false;
}
// Compile the conditions and actions and so check if they are valid.
if (!compileRule(rule)) {
return false;
}
// Register the rule and set idle status.
register(rule);
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
@ -862,6 +870,58 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
return true;
}
/**
* Compile the conditions and actions of the given rule.
* If there are errors, set the rule status (handler error) and return with indication.
*
* @param rule the rule whose conditions and actions should be compiled
* @return true if compilation succeeded, otherwise false
*/
private boolean compileRule(final WrappedRule rule) {
try {
compileConditions(rule);
compileActions(rule);
return true;
} catch (Throwable t) {
setStatus(rule.getUID(), new RuleStatusInfo(RuleStatus.UNINITIALIZED,
RuleStatusDetail.HANDLER_INITIALIZING_ERROR, t.getMessage()));
unregister(rule);
return false;
}
}
/**
* Compile the conditions and actions of the given rule.
* If there are errors, set the rule status (handler error).
*
* @param ruleUID the UID of the rule whose conditions and actions should be compiled
*/
private void compileRule(String ruleUID) {
final WrappedRule rule = getManagedRule(ruleUID);
if (rule == null) {
logger.warn("Failed to compile rule '{}': Invalid Rule UID", ruleUID);
return;
}
synchronized (this) {
final RuleStatus ruleStatus = getRuleStatus(ruleUID);
if (ruleStatus != null && ruleStatus != RuleStatus.IDLE) {
logger.error("Failed to compile rule {}' with status '{}'", ruleUID, ruleStatus.name());
return;
}
// change state to INITIALIZING
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.INITIALIZING));
}
if (!compileRule(rule)) {
return;
}
// change state to IDLE only if the rule has not been DISABLED.
synchronized (this) {
if (getRuleStatus(ruleUID) == RuleStatus.INITIALIZING) {
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
}
}
}
@Override
public @Nullable RuleStatusInfo getStatusInfo(String ruleUID) {
final WrappedRule rule = managedRules.get(ruleUID);
@ -1134,6 +1194,32 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
return context;
}
/**
* This method compiles conditions of the {@link Rule} when they exist.
* It is called when the rule is initialized.
*
* @param rule compiled rule.
*/
private void compileConditions(WrappedRule rule) {
final Collection<WrappedCondition> conditions = rule.getConditions();
if (conditions.isEmpty()) {
return;
}
for (WrappedCondition wrappedCondition : conditions) {
final Condition condition = wrappedCondition.unwrap();
ConditionHandler cHandler = wrappedCondition.getModuleHandler();
if (cHandler != null) {
try {
cHandler.compile();
} catch (Throwable t) {
String errMessage = "Failed to pre-compile condition: " + condition.getId() + "(" + t.getMessage()
+ ")";
throw new RuntimeException(errMessage, t);
}
}
}
}
/**
* This method checks if all rule's condition are satisfied or not.
*
@ -1163,6 +1249,31 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
return true;
}
/**
* This method compiles actions of the {@link Rule} when they exist.
* It is called when the rule is initialized.
*
* @param rule compiled rule.
*/
private void compileActions(WrappedRule rule) {
final Collection<WrappedAction> actions = rule.getActions();
if (actions.isEmpty()) {
return;
}
for (WrappedAction wrappedAction : actions) {
final Action action = wrappedAction.unwrap();
ActionHandler aHandler = wrappedAction.getModuleHandler();
if (aHandler != null) {
try {
aHandler.compile();
} catch (Throwable t) {
String errMessage = "Failed to pre-compile action: " + action.getId() + "(" + t.getMessage() + ")";
throw new RuntimeException(errMessage, t);
}
}
}
}
/**
* This method evaluates actions of the {@link Rule} and set their {@link Output}s when they exist.
*
@ -1435,7 +1546,7 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
@Override
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
executeRulesWithStartLevel();
compileRules();
}
@Override
@ -1443,6 +1554,20 @@ public class RuleEngineImpl implements RuleManager, RegistryChangeListener<Modul
started = false;
}
/**
* This method compiles the conditions and actions of all rules. It is called when the rule engine is started.
* By compiling when the rule engine is started, we make sure all conditions and actions are compiled, even if their
* handlers weren't available when the rule was added to the rule engine.
*/
private void compileRules() {
getScheduledExecutor().submit(() -> {
ruleRegistry.getAll().forEach(r -> {
compileRule(r.getUID());
});
executeRulesWithStartLevel();
});
}
private void executeRulesWithStartLevel() {
getScheduledExecutor().submit(() -> {
ruleRegistry.getAll().stream() //