mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[jsscripting] Fix multi-threading issues in UI-based scripts (#17510)
* [jsscripting] Make logger non-static * [jsscripting] Log lock acquisition and release * [jsscripting] Implement Lock in DebuggingGraalScriptEngine so openHAB Core can lock and unlock when executing the pre-compiled script * [jsscripting] Improve comments in ThreadsafeTimers * [jsscripting] Add a short sleep after unlocking the lock to fix multi-threading issues with UI-based scripts Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
parent
a6bb5373ea
commit
e6056d3142
@ -15,6 +15,8 @@ package org.openhab.automation.jsscripting.internal;
|
|||||||
import static org.openhab.core.automation.module.script.ScriptTransformationService.OPENHAB_TRANSFORMATION_SCRIPT;
|
import static org.openhab.core.automation.module.script.ScriptTransformationService.OPENHAB_TRANSFORMATION_SCRIPT;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Condition;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -36,7 +38,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
* @author Florian Hotze - Improve logger name, Fix memory leak caused by exception logging
|
* @author Florian Hotze - Improve logger name, Fix memory leak caused by exception logging
|
||||||
*/
|
*/
|
||||||
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoCloseable & Compilable & Lock>
|
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoCloseable & Compilable & Lock>
|
||||||
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T> {
|
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T> implements Lock {
|
||||||
|
|
||||||
private static final int STACK_TRACE_LENGTH = 5;
|
private static final int STACK_TRACE_LENGTH = 5;
|
||||||
|
|
||||||
@ -111,4 +113,34 @@ class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoClosea
|
|||||||
|
|
||||||
logger = LoggerFactory.getLogger("org.openhab.automation.script.javascript." + identifier);
|
logger = LoggerFactory.getLogger("org.openhab.automation.script.javascript." + identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lock() {
|
||||||
|
delegate.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lockInterruptibly() throws InterruptedException {
|
||||||
|
delegate.lockInterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tryLock() {
|
||||||
|
return delegate.tryLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
|
||||||
|
return delegate.tryLock(l, timeUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unlock() {
|
||||||
|
delegate.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Condition newCondition() {
|
||||||
|
return delegate.newCondition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,6 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<GraalJSScriptEngine>
|
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<GraalJSScriptEngine>
|
||||||
implements Lock {
|
implements Lock {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
|
|
||||||
private static final Source GLOBAL_SOURCE;
|
private static final Source GLOBAL_SOURCE;
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
@ -132,6 +131,8 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
v -> v.getMember("rawQtyType").as(QuantityType.class), HostAccess.TargetMappingPrecedence.LOW)
|
v -> v.getMember("rawQtyType").as(QuantityType.class), HostAccess.TargetMappingPrecedence.LOW)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
|
||||||
|
|
||||||
/** {@link Lock} synchronization of multi-thread access */
|
/** {@link Lock} synchronization of multi-thread access */
|
||||||
private final Lock lock = new ReentrantLock();
|
private final Lock lock = new ReentrantLock();
|
||||||
private final JSRuntimeFeatures jsRuntimeFeatures;
|
private final JSRuntimeFeatures jsRuntimeFeatures;
|
||||||
@ -155,8 +156,6 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
this.injectionCachingEnabled = injectionCachingEnabled;
|
this.injectionCachingEnabled = injectionCachingEnabled;
|
||||||
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
|
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
|
||||||
|
|
||||||
LOGGER.debug("Initializing GraalJS script engine...");
|
|
||||||
|
|
||||||
delegate = GraalJSScriptEngine.create(ENGINE,
|
delegate = GraalJSScriptEngine.create(ENGINE,
|
||||||
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
|
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
|
||||||
.allowHostAccess(HOST_ACCESS)
|
.allowHostAccess(HOST_ACCESS)
|
||||||
@ -230,7 +229,10 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
protected void beforeInvocation() {
|
protected void beforeInvocation() {
|
||||||
super.beforeInvocation();
|
super.beforeInvocation();
|
||||||
|
|
||||||
|
logger.debug("Initializing GraalJS script engine...");
|
||||||
|
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
logger.debug("Lock acquired before invocation.");
|
||||||
|
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return;
|
||||||
@ -257,7 +259,7 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
Consumer<String> localScriptDependencyListener = (Consumer<String>) ctx
|
Consumer<String> localScriptDependencyListener = (Consumer<String>) ctx
|
||||||
.getAttribute(CONTEXT_KEY_DEPENDENCY_LISTENER);
|
.getAttribute(CONTEXT_KEY_DEPENDENCY_LISTENER);
|
||||||
if (localScriptDependencyListener == null) {
|
if (localScriptDependencyListener == null) {
|
||||||
LOGGER.warn(
|
logger.warn(
|
||||||
"Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
|
"Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
|
||||||
}
|
}
|
||||||
scriptDependencyListener = localScriptDependencyListener;
|
scriptDependencyListener = localScriptDependencyListener;
|
||||||
@ -274,33 +276,41 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
|
|
||||||
// Injections into the JS runtime
|
// Injections into the JS runtime
|
||||||
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
|
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
|
||||||
LOGGER.debug("Injecting {} into the JS runtime...", key);
|
logger.debug("Injecting {} into the JS runtime...", key);
|
||||||
delegate.put(key, obj);
|
delegate.put(key, obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LOGGER.debug("Evaluating cached global script...");
|
logger.debug("Evaluating cached global script...");
|
||||||
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
|
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
|
||||||
if (this.injectionEnabled) {
|
if (this.injectionEnabled) {
|
||||||
if (this.injectionCachingEnabled) {
|
if (this.injectionCachingEnabled) {
|
||||||
LOGGER.debug("Evaluating cached openhab-js injection...");
|
logger.debug("Evaluating cached openhab-js injection...");
|
||||||
delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
|
delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
|
||||||
} else {
|
} else {
|
||||||
LOGGER.debug("Evaluating openhab-js injection from the file system...");
|
logger.debug("Evaluating openhab-js injection from the file system...");
|
||||||
eval(OPENHAB_JS_INJECTION_CODE);
|
eval(OPENHAB_JS_INJECTION_CODE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOGGER.debug("Successfully initialized GraalJS script engine.");
|
logger.debug("Successfully initialized GraalJS script engine.");
|
||||||
} catch (ScriptException e) {
|
} catch (ScriptException e) {
|
||||||
LOGGER.error("Could not inject global script", e);
|
logger.error("Could not inject global script", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Object afterInvocation(Object obj) {
|
protected Object afterInvocation(Object obj) {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
logger.debug("Lock released after invocation.");
|
||||||
|
// Fixes illegal multi-thread access requested in UI-based scripts, where the script is running and a timer is
|
||||||
|
// waiting to acquire the lock.
|
||||||
|
try {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException("Thread interrupted while sleeping", e);
|
||||||
|
}
|
||||||
return super.afterInvocation(obj);
|
return super.afterInvocation(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,6 +363,7 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
@Override
|
@Override
|
||||||
public void lock() {
|
public void lock() {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
logger.debug("Lock acquired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -373,6 +384,14 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
@Override
|
@Override
|
||||||
public void unlock() {
|
public void unlock() {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
logger.debug("Lock released.");
|
||||||
|
// Fixes illegal multi-thread access requested in UI-based scripts, where the script is running and a timer is
|
||||||
|
// waiting to acquire the lock.
|
||||||
|
try {
|
||||||
|
Thread.sleep(10);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException("Thread interrupted while sleeping", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -27,6 +27,8 @@ import org.openhab.core.automation.module.script.action.Timer;
|
|||||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||||
import org.openhab.core.scheduler.Scheduler;
|
import org.openhab.core.scheduler.Scheduler;
|
||||||
import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
|
* A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
|
||||||
@ -36,6 +38,8 @@ import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
|||||||
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
|
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
|
||||||
*/
|
*/
|
||||||
public class ThreadsafeTimers {
|
public class ThreadsafeTimers {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ThreadsafeTimers.class);
|
||||||
|
|
||||||
private final Lock lock;
|
private final Lock lock;
|
||||||
private final Scheduler scheduler;
|
private final Scheduler scheduler;
|
||||||
private final ScriptExecution scriptExecution;
|
private final ScriptExecution scriptExecution;
|
||||||
@ -81,11 +85,13 @@ public class ThreadsafeTimers {
|
|||||||
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
|
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
|
||||||
return scriptExecution.createTimer(identifier, instant, () -> {
|
return scriptExecution.createTimer(identifier, instant, () -> {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
logger.debug("Lock acquired before timer execution");
|
||||||
try {
|
try {
|
||||||
closure.run();
|
closure.run();
|
||||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||||
// deadlocks
|
// deadlocks
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
logger.debug("Lock released after timer execution");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -103,12 +109,14 @@ public class ThreadsafeTimers {
|
|||||||
long id = lastId.incrementAndGet();
|
long id = lastId.incrementAndGet();
|
||||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
logger.debug("Lock acquired before timeout execution");
|
||||||
try {
|
try {
|
||||||
callback.run();
|
callback.run();
|
||||||
idSchedulerMapping.remove(id);
|
idSchedulerMapping.remove(id);
|
||||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||||
// deadlocks
|
// deadlocks
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
logger.debug("Lock released after timeout execution");
|
||||||
}
|
}
|
||||||
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
|
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
|
||||||
idSchedulerMapping.put(id, future);
|
idSchedulerMapping.put(id, future);
|
||||||
@ -146,11 +154,13 @@ public class ThreadsafeTimers {
|
|||||||
long id = lastId.incrementAndGet();
|
long id = lastId.incrementAndGet();
|
||||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
logger.debug("Lock acquired before interval execution");
|
||||||
try {
|
try {
|
||||||
callback.run();
|
callback.run();
|
||||||
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
|
||||||
// deadlocks
|
// deadlocks
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
logger.debug("Lock released after interval execution");
|
||||||
}
|
}
|
||||||
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
|
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
|
||||||
idSchedulerMapping.put(id, future);
|
idSchedulerMapping.put(id, future);
|
||||||
|
Loading…
Reference in New Issue
Block a user