Skip to content

Commit e6056d3

Browse files
authored
[jsscripting] Fix multi-threading issues in UI-based scripts (openhab#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 <[email protected]>
1 parent a6bb537 commit e6056d3

File tree

3 files changed

+75
-14
lines changed

3 files changed

+75
-14
lines changed

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import static org.openhab.core.automation.module.script.ScriptTransformationService.OPENHAB_TRANSFORMATION_SCRIPT;
1616

1717
import java.util.Arrays;
18+
import java.util.concurrent.TimeUnit;
19+
import java.util.concurrent.locks.Condition;
1820
import java.util.concurrent.locks.Lock;
1921
import java.util.stream.Collectors;
2022

@@ -36,7 +38,7 @@
3638
* @author Florian Hotze - Improve logger name, Fix memory leak caused by exception logging
3739
*/
3840
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoCloseable & Compilable & Lock>
39-
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T> {
41+
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<T> implements Lock {
4042

4143
private static final int STACK_TRACE_LENGTH = 5;
4244

@@ -111,4 +113,34 @@ private void initializeLogger() {
111113

112114
logger = LoggerFactory.getLogger("org.openhab.automation.script.javascript." + identifier);
113115
}
116+
117+
@Override
118+
public void lock() {
119+
delegate.lock();
120+
}
121+
122+
@Override
123+
public void lockInterruptibly() throws InterruptedException {
124+
delegate.lockInterruptibly();
125+
}
126+
127+
@Override
128+
public boolean tryLock() {
129+
return delegate.tryLock();
130+
}
131+
132+
@Override
133+
public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
134+
return delegate.tryLock(l, timeUnit);
135+
}
136+
137+
@Override
138+
public void unlock() {
139+
delegate.unlock();
140+
}
141+
142+
@Override
143+
public Condition newCondition() {
144+
return delegate.newCondition();
145+
}
114146
}

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java

+29-10
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ public class OpenhabGraalJSScriptEngine
7474
extends InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable<GraalJSScriptEngine>
7575
implements Lock {
7676

77-
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
7877
private static final Source GLOBAL_SOURCE;
7978
static {
8079
try {
@@ -132,6 +131,8 @@ public class OpenhabGraalJSScriptEngine
132131
v -> v.getMember("rawQtyType").as(QuantityType.class), HostAccess.TargetMappingPrecedence.LOW)
133132
.build();
134133

134+
private final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
135+
135136
/** {@link Lock} synchronization of multi-thread access */
136137
private final Lock lock = new ReentrantLock();
137138
private final JSRuntimeFeatures jsRuntimeFeatures;
@@ -155,8 +156,6 @@ public OpenhabGraalJSScriptEngine(boolean injectionEnabled, boolean injectionCac
155156
this.injectionCachingEnabled = injectionCachingEnabled;
156157
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
157158

158-
LOGGER.debug("Initializing GraalJS script engine...");
159-
160159
delegate = GraalJSScriptEngine.create(ENGINE,
161160
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
162161
.allowHostAccess(HOST_ACCESS)
@@ -230,7 +229,10 @@ public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException
230229
protected void beforeInvocation() {
231230
super.beforeInvocation();
232231

232+
logger.debug("Initializing GraalJS script engine...");
233+
233234
lock.lock();
235+
logger.debug("Lock acquired before invocation.");
234236

235237
if (initialized) {
236238
return;
@@ -257,7 +259,7 @@ protected void beforeInvocation() {
257259
Consumer<String> localScriptDependencyListener = (Consumer<String>) ctx
258260
.getAttribute(CONTEXT_KEY_DEPENDENCY_LISTENER);
259261
if (localScriptDependencyListener == null) {
260-
LOGGER.warn(
262+
logger.warn(
261263
"Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
262264
}
263265
scriptDependencyListener = localScriptDependencyListener;
@@ -274,33 +276,41 @@ protected void beforeInvocation() {
274276

275277
// Injections into the JS runtime
276278
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
277-
LOGGER.debug("Injecting {} into the JS runtime...", key);
279+
logger.debug("Injecting {} into the JS runtime...", key);
278280
delegate.put(key, obj);
279281
});
280282

281283
initialized = true;
282284

283285
try {
284-
LOGGER.debug("Evaluating cached global script...");
286+
logger.debug("Evaluating cached global script...");
285287
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
286288
if (this.injectionEnabled) {
287289
if (this.injectionCachingEnabled) {
288-
LOGGER.debug("Evaluating cached openhab-js injection...");
290+
logger.debug("Evaluating cached openhab-js injection...");
289291
delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
290292
} else {
291-
LOGGER.debug("Evaluating openhab-js injection from the file system...");
293+
logger.debug("Evaluating openhab-js injection from the file system...");
292294
eval(OPENHAB_JS_INJECTION_CODE);
293295
}
294296
}
295-
LOGGER.debug("Successfully initialized GraalJS script engine.");
297+
logger.debug("Successfully initialized GraalJS script engine.");
296298
} catch (ScriptException e) {
297-
LOGGER.error("Could not inject global script", e);
299+
logger.error("Could not inject global script", e);
298300
}
299301
}
300302

301303
@Override
302304
protected Object afterInvocation(Object obj) {
303305
lock.unlock();
306+
logger.debug("Lock released after invocation.");
307+
// Fixes illegal multi-thread access requested in UI-based scripts, where the script is running and a timer is
308+
// waiting to acquire the lock.
309+
try {
310+
Thread.sleep(10);
311+
} catch (InterruptedException e) {
312+
throw new RuntimeException("Thread interrupted while sleeping", e);
313+
}
304314
return super.afterInvocation(obj);
305315
}
306316

@@ -353,6 +363,7 @@ private static Reader getFileAsReader(String fileName) throws IOException {
353363
@Override
354364
public void lock() {
355365
lock.lock();
366+
logger.debug("Lock acquired.");
356367
}
357368

358369
@Override
@@ -373,6 +384,14 @@ public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {
373384
@Override
374385
public void unlock() {
375386
lock.unlock();
387+
logger.debug("Lock released.");
388+
// Fixes illegal multi-thread access requested in UI-based scripts, where the script is running and a timer is
389+
// waiting to acquire the lock.
390+
try {
391+
Thread.sleep(10);
392+
} catch (InterruptedException e) {
393+
throw new RuntimeException("Thread interrupted while sleeping", e);
394+
}
376395
}
377396

378397
@Override

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeTimers.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.openhab.core.scheduler.ScheduledCompletableFuture;
2828
import org.openhab.core.scheduler.Scheduler;
2929
import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
3032

3133
/**
3234
* A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
@@ -36,6 +38,8 @@
3638
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
3739
*/
3840
public class ThreadsafeTimers {
41+
private final Logger logger = LoggerFactory.getLogger(ThreadsafeTimers.class);
42+
3943
private final Lock lock;
4044
private final Scheduler scheduler;
4145
private final ScriptExecution scriptExecution;
@@ -81,11 +85,13 @@ public Timer createTimer(ZonedDateTime instant, Runnable closure) {
8185
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
8286
return scriptExecution.createTimer(identifier, instant, () -> {
8387
lock.lock();
88+
logger.debug("Lock acquired before timer execution");
8489
try {
8590
closure.run();
86-
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
91+
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
8792
// deadlocks
8893
lock.unlock();
94+
logger.debug("Lock released after timer execution");
8995
}
9096
});
9197
}
@@ -103,12 +109,14 @@ public long setTimeout(Runnable callback, long delay) {
103109
long id = lastId.incrementAndGet();
104110
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
105111
lock.lock();
112+
logger.debug("Lock acquired before timeout execution");
106113
try {
107114
callback.run();
108115
idSchedulerMapping.remove(id);
109-
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
116+
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
110117
// deadlocks
111118
lock.unlock();
119+
logger.debug("Lock released after timeout execution");
112120
}
113121
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
114122
idSchedulerMapping.put(id, future);
@@ -146,11 +154,13 @@ public long setInterval(Runnable callback, long delay) {
146154
long id = lastId.incrementAndGet();
147155
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
148156
lock.lock();
157+
logger.debug("Lock acquired before interval execution");
149158
try {
150159
callback.run();
151-
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
160+
} finally { // Make sure that Lock is unlocked regardless of an exception being thrown or not to avoid
152161
// deadlocks
153162
lock.unlock();
163+
logger.debug("Lock released after interval execution");
154164
}
155165
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
156166
idSchedulerMapping.put(id, future);

0 commit comments

Comments
 (0)