Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
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;
Expand All @@ -35,6 +39,7 @@
*
* @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
*/
Expand All @@ -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;

Expand All @@ -80,6 +86,34 @@ private static String getValidConfigParameter(String parameter, Configuration co
}
}

/**
* 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);
Expand Down Expand Up @@ -169,4 +203,26 @@ protected void resetExecutionContext(ScriptEngine engine, Map<String, ?> context
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
*
* @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 {
Expand Down Expand Up @@ -61,6 +62,11 @@ public void dispose() {
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<>();
Expand All @@ -71,13 +77,8 @@ public void dispose() {

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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*
* @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 {
Expand All @@ -42,6 +43,11 @@ public ScriptConditionHandler(Condition module, String ruleUID, ScriptEngineMana
super(module, ruleUID, scriptEngineManager);
}

@Override
public void compile() throws ScriptException {
super.compileScript();
}

@Override
public boolean isSatisfied(final Map<String, Object> context) {
boolean result = false;
Expand All @@ -55,18 +61,14 @@ public boolean isSatisfied(final Map<String, Object> context) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
*/
@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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
*/
@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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
* @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
Expand Down Expand Up @@ -819,6 +820,8 @@ public synchronized void setEnabled(String uid, boolean enable) {
* <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>
*
Expand All @@ -845,6 +848,11 @@ private boolean activateRule(final WrappedRule rule) {
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));
Expand All @@ -862,6 +870,58 @@ private boolean activateRule(final WrappedRule rule) {
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);
Expand Down Expand Up @@ -1134,6 +1194,32 @@ private Map<String, Object> getContext(String ruleUID, @Nullable Set<Connection>
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.
*
Expand Down Expand Up @@ -1163,6 +1249,31 @@ private boolean calculateConditions(WrappedRule rule) {
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.
*
Expand Down Expand Up @@ -1435,14 +1546,28 @@ public String getOutputName() {

@Override
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
executeRulesWithStartLevel();
compileRules();
}

@Override
public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
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() //
Expand Down