Skip to content
Draft
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 @@ -13,6 +13,7 @@
package org.openhab.core.automation.module.script.rulesupport.internal.delegates;

import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -82,4 +83,15 @@ public void runNow(String uid) {
public void runNow(String uid, boolean considerConditions, @Nullable Map<String, @Nullable Object> context) {
callback.runNow(uid, considerConditions, context);
}

@Override
public Future<Map<String, @Nullable Object>> runAsync(String ruleUID) {
return callback.runAsync(ruleUID);
}

@Override
public Future<Map<String, @Nullable Object>> runAsync(String ruleUID, boolean considerConditions,
@Nullable Map<String, @Nullable Object> context) {
return callback.runAsync(ruleUID, considerConditions, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

import javax.script.Invocable;
import javax.script.ScriptContext;
Expand Down Expand Up @@ -55,6 +56,7 @@
@Component(service = ScriptEngineManager.class)
public class ScriptEngineManagerImpl implements ScriptEngineManager {

private static final long LOCK_ACQUISITION_TIMEOUT_S = 60L;
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);

Expand Down Expand Up @@ -159,32 +161,61 @@ public boolean loadScript(String engineIdentifier, InputStreamReader scriptData)
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
if (container == null) {
logger.error("Could not load script, as no ScriptEngine has been created");
} else {
ScriptEngine engine = container.getScriptEngine();
return false;
}
ScriptEngine engine = container.getScriptEngine();
if (engine instanceof Lock lock) {
boolean locked;
try {
engine.eval(scriptData);
if (engine instanceof Invocable inv) {
try {
inv.invokeFunction("scriptLoaded", engineIdentifier);
} catch (NoSuchMethodException e) {
logger.trace("scriptLoaded() is not defined in the script: {}", engineIdentifier);
}
} else {
logger.trace("ScriptEngine does not support Invocable interface");
locked = lock.tryLock(LOCK_ACQUISITION_TIMEOUT_S, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("Interrupted while waiting to acquire the lock while loading script for engine '{}'",
engineIdentifier);
logger.trace("", e);
return false;
}
if (locked) {
try {
return runScript(engineIdentifier, engine, scriptData);
} finally {
lock.unlock();
}
return true;
} catch (Exception ex) {
logger.error("Error during evaluation of script '{}': {}", engineIdentifier, ex.getMessage());
// Only call logger if debug level is actually enabled, because OPS4J Pax Logging holds (at least for
// some time) a reference to the exception and its cause, which may hold a reference to the script
// engine.
// This prevents garbage collection (at least for some time) to remove the script engine from heap.
if (logger.isDebugEnabled()) {
logger.debug("", ex);
} else {
logger.error(
"Failed to acquire the lock while loading script for engine '{}' within {} seconds. Aborting loading of script.",
engineIdentifier, LOCK_ACQUISITION_TIMEOUT_S);
return false;
}
} else {
return runScript(engineIdentifier, engine, scriptData);
}
}

private boolean runScript(String engineIdentifier, ScriptEngine engine, InputStreamReader scriptData) {
try {
engine.eval(scriptData);
if (engine instanceof Invocable inv) {
try {
inv.invokeFunction("scriptLoaded", engineIdentifier);
} catch (NoSuchMethodException e) {
logger.trace("scriptLoaded() is not defined in the script: {}", engineIdentifier);
}
} else {
logger.trace("ScriptEngine does not support Invocable interface");
}
return true;
} catch (Exception ex) {
logger.error("Error during evaluation of script '{}': {}", engineIdentifier, ex.getMessage());
// Only call logger if debug level is actually enabled, because OPS4J Pax Logging holds (at least for
// some time) a reference to the exception and its cause, which may hold a reference to the script
// engine.
// This prevents garbage collection (at least for some time) to remove the script engine from heap.
if (logger.isDebugEnabled()) {
logger.debug("", ex);
}
return false;
}
return false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.core.automation;

import java.util.Map;
import java.util.concurrent.Future;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand Down Expand Up @@ -92,4 +93,37 @@ public interface ModuleHandlerCallback {
* @param context the context that is passed to the conditions and the actions of the rule.
*/
void runNow(String uid, boolean considerConditions, @Nullable Map<String, @Nullable Object> context);

/**
* The method skips triggers and conditions and executes the actions of the rule asynchronously.
* This should always be possible unless an action has a mandatory input that is linked to a trigger.
* In that case the action is skipped and the rule engine continues execution of remaining actions.
* <p>
* <b>Note:</b> Unlike {@link #runNow(String)}, this method will return immediately. To wait for the execution to be
* completed, call {@link Future#get()} on the returned {@link Future}.
*
* @param ruleUID uid of the rule whose actions should be executed.
* @return A {@link Future} containing the copy of the rule context after completion, including possible return
* values.
* @throws UnsupportedOperationException If asynchronous execution isn't supported by the {@link RuleManager}
* implementation.
*/
Future<Map<String, @Nullable Object>> runAsync(String ruleUID);

/**
* Same as {@link #runAsync(String)} with the additional option to enable/disable evaluation of
* conditions defined in the target rule. The context can be set here, too, but might also be {@code null}.
* <p>
* <b>Note:</b> Unlike {@link #runNow(String, boolean, Map)}, this method will return immediately. To wait for the
* execution to be completed, call {@link Future#get()} on the returned {@link Future}.
*
* @param ruleUID uid of the rule whose actions should be executed.
* @param considerConditions if {@code true} the conditions of the rule will be checked.
* @param context the context that is passed to the conditions and the actions of the rule.
* @return a copy of the rule context, including possible return values
* @throws UnsupportedOperationException If asynchronous execution isn't supported by the {@link RuleManager}
* implementation.
*/
Future<Map<String, @Nullable Object>> runAsync(String ruleUID, boolean considerConditions,
@Nullable Map<String, @Nullable Object> context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.time.ZonedDateTime;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -100,6 +101,47 @@ public interface RuleManager {
Map<String, @Nullable Object> runNow(String uid, boolean considerConditions,
@Nullable Map<String, @Nullable Object> context);

/**
* The method skips triggers and conditions and executes the actions of the rule asynchronously.
* This should always be possible unless an action has a mandatory input that is linked to a trigger.
* In that case the action is skipped and the rule engine continues execution of remaining actions.
* <p>
* <b>Note:</b> Unlike {@link #runNow(String)}, this method will return immediately. To wait for the execution to be
* completed, call {@link Future#get()} on the returned {@link Future}.
*
* @param ruleUID uid of the rule whose actions should be executed.
* @return A {@link Future} containing the copy of the rule context after completion, including possible return
* values.
* @throws UnsupportedOperationException If asynchronous execution isn't supported by the {@link RuleManager}
* implementation.
*
* @implNote The default implementation simply calls {@link #runAsync(String, boolean, Map)}.
*/
default Future<Map<String, @Nullable Object>> runAsync(String ruleUID) {
return runAsync(ruleUID, false, null);
}

/**
* Same as {@link #runAsync(String)} with the additional option to enable/disable evaluation of
* conditions defined in the target rule. The context can be set here, too, but might also be {@code null}.
* <p>
* <b>Note:</b> Unlike {@link #runNow(String, boolean, Map)}, this method will return immediately. To wait for the
* execution to be completed, call {@link Future#get()} on the returned {@link Future}.
*
* @param ruleUID uid of the rule whose actions should be executed.
* @param considerConditions if {@code true} the conditions of the rule will be checked.
* @param context the context that is passed to the conditions and the actions of the rule.
* @return a copy of the rule context, including possible return values
* @throws UnsupportedOperationException If asynchronous execution isn't supported by the {@link RuleManager}
* implementation.
*
* @implNote The default implementation throws an {@link UnsupportedOperationException}.
*/
default Future<Map<String, @Nullable Object>> runAsync(String ruleUID, boolean considerConditions,
@Nullable Map<String, @Nullable Object> context) {
throw new UnsupportedOperationException("runAsync() isn't implemented by " + getClass().getName());
}

/**
* Simulates the execution of all rules with tag 'Schedule' for the given time interval.
* The result is sorted ascending by execution time.
Expand Down
Loading