Skip to content

Commit 918b4fa

Browse files
authored
[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 <[email protected]>
1 parent ea7d61b commit 918b4fa

File tree

6 files changed

+219
-19
lines changed

6 files changed

+219
-19
lines changed

bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/AbstractScriptModuleHandler.java

+56
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
import java.util.Optional;
1919
import java.util.UUID;
2020

21+
import javax.script.Compilable;
22+
import javax.script.CompiledScript;
2123
import javax.script.ScriptContext;
2224
import javax.script.ScriptEngine;
25+
import javax.script.ScriptException;
2326

2427
import org.eclipse.jdt.annotation.NonNullByDefault;
28+
import org.eclipse.jdt.annotation.Nullable;
2529
import org.openhab.core.automation.Module;
2630
import org.openhab.core.automation.handler.BaseModuleHandler;
2731
import org.openhab.core.automation.module.script.ScriptEngineContainer;
@@ -35,6 +39,7 @@
3539
*
3640
* @author Kai Kreuzer - Initial contribution
3741
* @author Simon Merschjohann - Initial contribution
42+
* @author Florian Hotze - Add support for script pre-compilation
3843
*
3944
* @param <T> the type of module the concrete handler can handle
4045
*/
@@ -54,6 +59,7 @@ public abstract class AbstractScriptModuleHandler<T extends Module> extends Base
5459
private final String engineIdentifier;
5560

5661
private Optional<ScriptEngine> scriptEngine = Optional.empty();
62+
private Optional<CompiledScript> compiledScript = Optional.empty();
5763
private final String type;
5864
protected final String script;
5965

@@ -80,6 +86,34 @@ private static String getValidConfigParameter(String parameter, Configuration co
8086
}
8187
}
8288

89+
/**
90+
* Creates the {@link ScriptEngine} and compiles the script if the {@link ScriptEngine} implements
91+
* {@link Compilable}.
92+
*/
93+
protected void compileScript() throws ScriptException {
94+
if (compiledScript.isPresent()) {
95+
return;
96+
}
97+
if (!scriptEngineManager.isSupported(this.type)) {
98+
logger.debug(
99+
"ScriptEngine for language '{}' could not be found, skipping compilation of script for identifier: {}",
100+
type, engineIdentifier);
101+
return;
102+
}
103+
Optional<ScriptEngine> engine = getScriptEngine();
104+
if (engine.isPresent()) {
105+
ScriptEngine scriptEngine = engine.get();
106+
if (scriptEngine instanceof Compilable) {
107+
logger.debug("Pre-compiling script of rule with UID '{}'", ruleUID);
108+
compiledScript = Optional.ofNullable(((Compilable) scriptEngine).compile(script));
109+
} else {
110+
logger.error(
111+
"Script engine of rule with UID '{}' does not implement Compilable but claims to support pre-compilation",
112+
module.getId());
113+
}
114+
}
115+
}
116+
83117
@Override
84118
public void dispose() {
85119
scriptEngineManager.removeEngine(engineIdentifier);
@@ -169,4 +203,26 @@ protected void resetExecutionContext(ScriptEngine engine, Map<String, ?> context
169203
executionContext.removeAttribute(key, ScriptContext.ENGINE_SCOPE);
170204
}
171205
}
206+
207+
/**
208+
* Evaluates the passed script with the ScriptEngine.
209+
*
210+
* @param engine the script engine that is used
211+
* @param script the script to evaluate
212+
* @return the value returned from the execution of the script
213+
*/
214+
protected @Nullable Object eval(ScriptEngine engine, String script) {
215+
try {
216+
if (compiledScript.isPresent()) {
217+
logger.debug("Executing pre-compiled script of rule with UID '{}'", ruleUID);
218+
return compiledScript.get().eval(engine.getContext());
219+
}
220+
logger.debug("Executing script of rule with UID '{}'", ruleUID);
221+
return engine.eval(script);
222+
} catch (ScriptException e) {
223+
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
224+
logger.isDebugEnabled() ? e : null);
225+
return null;
226+
}
227+
}
172228
}

bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptActionHandler.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
*
3232
* @author Kai Kreuzer - Initial contribution
3333
* @author Simon Merschjohann - Initial contribution
34+
* @author Florian Hotze - Add support for script pre-compilation
3435
*/
3536
@NonNullByDefault
3637
public class ScriptActionHandler extends AbstractScriptModuleHandler<Action> implements ActionHandler {
@@ -61,6 +62,11 @@ public void dispose() {
6162
super.dispose();
6263
}
6364

65+
@Override
66+
public void compile() throws ScriptException {
67+
super.compileScript();
68+
}
69+
6470
@Override
6571
public @Nullable Map<String, Object> execute(final Map<String, Object> context) {
6672
Map<String, Object> resultMap = new HashMap<>();
@@ -71,13 +77,8 @@ public void dispose() {
7177

7278
getScriptEngine().ifPresent(scriptEngine -> {
7379
setExecutionContext(scriptEngine, context);
74-
try {
75-
Object result = scriptEngine.eval(script);
76-
resultMap.put("result", result);
77-
} catch (ScriptException e) {
78-
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
79-
logger.isDebugEnabled() ? e : null);
80-
}
80+
Object result = eval(scriptEngine, script);
81+
resultMap.put("result", result);
8182
resetExecutionContext(scriptEngine, context);
8283
});
8384

bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/handler/ScriptConditionHandler.java

+13-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*
3131
* @author Kai Kreuzer - Initial contribution
3232
* @author Simon Merschjohann - Initial contribution
33+
* @author Florian Hotze - Add support for script pre-compilation
3334
*/
3435
@NonNullByDefault
3536
public class ScriptConditionHandler extends AbstractScriptModuleHandler<Condition> implements ConditionHandler {
@@ -42,6 +43,11 @@ public ScriptConditionHandler(Condition module, String ruleUID, ScriptEngineMana
4243
super(module, ruleUID, scriptEngineManager);
4344
}
4445

46+
@Override
47+
public void compile() throws ScriptException {
48+
super.compileScript();
49+
}
50+
4551
@Override
4652
public boolean isSatisfied(final Map<String, Object> context) {
4753
boolean result = false;
@@ -55,18 +61,14 @@ public boolean isSatisfied(final Map<String, Object> context) {
5561
if (engine.isPresent()) {
5662
ScriptEngine scriptEngine = engine.get();
5763
setExecutionContext(scriptEngine, context);
58-
try {
59-
Object returnVal = scriptEngine.eval(script);
60-
if (returnVal instanceof Boolean boolean1) {
61-
result = boolean1;
62-
} else {
63-
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID,
64-
returnVal);
65-
}
66-
} catch (ScriptException e) {
67-
logger.error("Script execution of rule with UID '{}' failed: {}", ruleUID, e.getMessage(),
68-
logger.isDebugEnabled() ? e : null);
64+
Object returnVal = eval(scriptEngine, script);
65+
if (returnVal instanceof Boolean boolean1) {
66+
result = boolean1;
67+
} else {
68+
logger.error("Script of rule with UID '{}' did not return a boolean value, but '{}'", ruleUID,
69+
returnVal);
6970
}
71+
resetExecutionContext(scriptEngine, context);
7072
}
7173

7274
return result;

bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ActionHandler.java

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
*/
3232
@NonNullByDefault
3333
public interface ActionHandler extends ModuleHandler {
34+
/**
35+
* Called to compile an {@link Action} of the {@link Rule} when the rule is initialized.
36+
*
37+
* @throws Exception if the compilation fails
38+
*/
39+
default void compile() throws Exception {
40+
// Do nothing by default
41+
}
3442

3543
/**
3644
* Called to execute an {@link Action} of the {@link Rule} when it is needed.

bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/handler/ConditionHandler.java

+8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
*/
3030
@NonNullByDefault
3131
public interface ConditionHandler extends ModuleHandler {
32+
/**
33+
* Called to compile the {@link Condition} when the {@link Rule} is initialized.
34+
*
35+
* @throws Exception if the compilation fails
36+
*/
37+
default void compile() throws Exception {
38+
// Do nothing by default
39+
}
3240

3341
/**
3442
* Checks if the Condition is satisfied in the given {@code context}.

bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/RuleEngineImpl.java

+126-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
* @author Benedikt Niehues - change behavior for unregistering ModuleHandler
111111
* @author Markus Rathgeb - use a managed rule
112112
* @author Ana Dimova - new reference syntax: list[index], map["key"], bean.field
113+
* @author Florian Hotze - add support for script condition/action compilation
113114
*/
114115
@Component(immediate = true, service = { RuleManager.class })
115116
@NonNullByDefault
@@ -819,6 +820,8 @@ public synchronized void setEnabled(String uid, boolean enable) {
819820
* <ul>
820821
* <li>Set the module handlers. If there are errors, set the rule status (handler error) and return with error
821822
* indication.
823+
* <li>Compile the conditions and actions. If there are errors, set the rule status (handler error) and return with
824+
* indication.
822825
* <li>Register the rule. Set the rule status and return with success indication.
823826
* </ul>
824827
*
@@ -845,6 +848,11 @@ private boolean activateRule(final WrappedRule rule) {
845848
return false;
846849
}
847850

851+
// Compile the conditions and actions and so check if they are valid.
852+
if (!compileRule(rule)) {
853+
return false;
854+
}
855+
848856
// Register the rule and set idle status.
849857
register(rule);
850858
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
@@ -862,6 +870,58 @@ private boolean activateRule(final WrappedRule rule) {
862870
return true;
863871
}
864872

873+
/**
874+
* Compile the conditions and actions of the given rule.
875+
* If there are errors, set the rule status (handler error) and return with indication.
876+
*
877+
* @param rule the rule whose conditions and actions should be compiled
878+
* @return true if compilation succeeded, otherwise false
879+
*/
880+
private boolean compileRule(final WrappedRule rule) {
881+
try {
882+
compileConditions(rule);
883+
compileActions(rule);
884+
return true;
885+
} catch (Throwable t) {
886+
setStatus(rule.getUID(), new RuleStatusInfo(RuleStatus.UNINITIALIZED,
887+
RuleStatusDetail.HANDLER_INITIALIZING_ERROR, t.getMessage()));
888+
unregister(rule);
889+
return false;
890+
}
891+
}
892+
893+
/**
894+
* Compile the conditions and actions of the given rule.
895+
* If there are errors, set the rule status (handler error).
896+
*
897+
* @param ruleUID the UID of the rule whose conditions and actions should be compiled
898+
*/
899+
private void compileRule(String ruleUID) {
900+
final WrappedRule rule = getManagedRule(ruleUID);
901+
if (rule == null) {
902+
logger.warn("Failed to compile rule '{}': Invalid Rule UID", ruleUID);
903+
return;
904+
}
905+
synchronized (this) {
906+
final RuleStatus ruleStatus = getRuleStatus(ruleUID);
907+
if (ruleStatus != null && ruleStatus != RuleStatus.IDLE) {
908+
logger.error("Failed to compile rule ‘{}' with status '{}'", ruleUID, ruleStatus.name());
909+
return;
910+
}
911+
// change state to INITIALIZING
912+
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.INITIALIZING));
913+
}
914+
if (!compileRule(rule)) {
915+
return;
916+
}
917+
// change state to IDLE only if the rule has not been DISABLED.
918+
synchronized (this) {
919+
if (getRuleStatus(ruleUID) == RuleStatus.INITIALIZING) {
920+
setStatus(ruleUID, new RuleStatusInfo(RuleStatus.IDLE));
921+
}
922+
}
923+
}
924+
865925
@Override
866926
public @Nullable RuleStatusInfo getStatusInfo(String ruleUID) {
867927
final WrappedRule rule = managedRules.get(ruleUID);
@@ -1134,6 +1194,32 @@ private Map<String, Object> getContext(String ruleUID, @Nullable Set<Connection>
11341194
return context;
11351195
}
11361196

1197+
/**
1198+
* This method compiles conditions of the {@link Rule} when they exist.
1199+
* It is called when the rule is initialized.
1200+
*
1201+
* @param rule compiled rule.
1202+
*/
1203+
private void compileConditions(WrappedRule rule) {
1204+
final Collection<WrappedCondition> conditions = rule.getConditions();
1205+
if (conditions.isEmpty()) {
1206+
return;
1207+
}
1208+
for (WrappedCondition wrappedCondition : conditions) {
1209+
final Condition condition = wrappedCondition.unwrap();
1210+
ConditionHandler cHandler = wrappedCondition.getModuleHandler();
1211+
if (cHandler != null) {
1212+
try {
1213+
cHandler.compile();
1214+
} catch (Throwable t) {
1215+
String errMessage = "Failed to pre-compile condition: " + condition.getId() + "(" + t.getMessage()
1216+
+ ")";
1217+
throw new RuntimeException(errMessage, t);
1218+
}
1219+
}
1220+
}
1221+
}
1222+
11371223
/**
11381224
* This method checks if all rule's condition are satisfied or not.
11391225
*
@@ -1163,6 +1249,31 @@ private boolean calculateConditions(WrappedRule rule) {
11631249
return true;
11641250
}
11651251

1252+
/**
1253+
* This method compiles actions of the {@link Rule} when they exist.
1254+
* It is called when the rule is initialized.
1255+
*
1256+
* @param rule compiled rule.
1257+
*/
1258+
private void compileActions(WrappedRule rule) {
1259+
final Collection<WrappedAction> actions = rule.getActions();
1260+
if (actions.isEmpty()) {
1261+
return;
1262+
}
1263+
for (WrappedAction wrappedAction : actions) {
1264+
final Action action = wrappedAction.unwrap();
1265+
ActionHandler aHandler = wrappedAction.getModuleHandler();
1266+
if (aHandler != null) {
1267+
try {
1268+
aHandler.compile();
1269+
} catch (Throwable t) {
1270+
String errMessage = "Failed to pre-compile action: " + action.getId() + "(" + t.getMessage() + ")";
1271+
throw new RuntimeException(errMessage, t);
1272+
}
1273+
}
1274+
}
1275+
}
1276+
11661277
/**
11671278
* This method evaluates actions of the {@link Rule} and set their {@link Output}s when they exist.
11681279
*
@@ -1435,14 +1546,28 @@ public String getOutputName() {
14351546

14361547
@Override
14371548
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
1438-
executeRulesWithStartLevel();
1549+
compileRules();
14391550
}
14401551

14411552
@Override
14421553
public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
14431554
started = false;
14441555
}
14451556

1557+
/**
1558+
* This method compiles the conditions and actions of all rules. It is called when the rule engine is started.
1559+
* By compiling when the rule engine is started, we make sure all conditions and actions are compiled, even if their
1560+
* handlers weren't available when the rule was added to the rule engine.
1561+
*/
1562+
private void compileRules() {
1563+
getScheduledExecutor().submit(() -> {
1564+
ruleRegistry.getAll().forEach(r -> {
1565+
compileRule(r.getUID());
1566+
});
1567+
executeRulesWithStartLevel();
1568+
});
1569+
}
1570+
14461571
private void executeRulesWithStartLevel() {
14471572
getScheduledExecutor().submit(() -> {
14481573
ruleRegistry.getAll().stream() //

0 commit comments

Comments
 (0)