[jsscripting] Reimplement timer creation method of ScriptExecution (#13695)

* [jsscripting] Refactor ThreadsafeTimers to create futures inline instead of in an extra methods
* [jsscripting] Introduce utility class for providing easy access to script services
* [jsscripting] Reimplement timer creation methods from ScriptExecution for thread-safety
* [jsscripting] Add missing JavaDoc for reimplement timer creation methods
* [jsscripting] Remove the future from the map when setTimeout expires
* [jsscripting] Rename `GraalJSScriptServiceUtil` to `JSScriptServiceUtil`
* [jsscripting] Remove the `createTimerWithArgument` method
* [jsscripting] Replace the OSGi workaround of `JSScriptServiceUtil` with an injection mechanism
* [jsscripting] Use constructor to inject `JSScriptServiceUtil` into `GraalJSScriptEngineFactory`
* [jsscripting] Minor improvements by @J-N-K (#1)
* [jsscripting] Minor changes related to last commit to keep flexibility of `JSRuntimeFeatures`
* [jsscripting] Upgrade openhab-js to v2.1.1
* [jsscripting] Remove unused code

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
Co-authored-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
Florian Hotze
2022-11-20 22:08:19 +01:00
committed by GitHub
parent d0736bdea9
commit bfff07bb01
7 changed files with 164 additions and 57 deletions

View File

@@ -21,11 +21,11 @@ import javax.script.ScriptEngine;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.config.core.ConfigurableService;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
/**
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
@@ -42,6 +42,14 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
private boolean injectionEnabled = true;
public static final String MIME_TYPE = "application/javascript;version=ECMAScript-2021";
private final JSScriptServiceUtil jsScriptServiceUtil;
@Activate
public GraalJSScriptEngineFactory(final @Reference JSScriptServiceUtil jsScriptServiceUtil,
Map<String, Object> config) {
this.jsScriptServiceUtil = jsScriptServiceUtil;
modified(config);
}
@Override
public List<String> getScriptTypes() {
@@ -71,12 +79,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
@Override
public ScriptEngine createScriptEngine(String scriptType) {
return new DebuggingGraalScriptEngine<>(
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null));
}
@Activate
protected void activate(BundleContext context, Map<String, ?> config) {
modified(config);
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
}
@Modified

View File

@@ -31,8 +31,9 @@ public class JSRuntimeFeatures {
private final Map<String, Object> features = new HashMap<>();
public final ThreadsafeTimers threadsafeTimers;
JSRuntimeFeatures(Object lock) {
this.threadsafeTimers = new ThreadsafeTimers(lock);
JSRuntimeFeatures(Object lock, JSScriptServiceUtil jsScriptServiceUtil) {
this.threadsafeTimers = new ThreadsafeTimers(lock, jsScriptServiceUtil.getScriptExecution(),
jsScriptServiceUtil.getScheduler());
features.put("ThreadsafeTimers", threadsafeTimers);
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.module.script.action.ScriptExecution;
import org.openhab.core.scheduler.Scheduler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* OSGi utility service for providing easy access to script services.
*
* @author Florian Hotze - Initial contribution
*/
@Component(immediate = true, service = JSScriptServiceUtil.class)
@NonNullByDefault
public class JSScriptServiceUtil {
private final Scheduler scheduler;
private final ScriptExecution scriptExecution;
@Activate
public JSScriptServiceUtil(final @Reference Scheduler scheduler, final @Reference ScriptExecution scriptExecution) {
this.scheduler = scheduler;
this.scriptExecution = scriptExecution;
}
public Scheduler getScheduler() {
return scheduler;
}
public ScriptExecution getScriptExecution() {
return scriptExecution;
}
public JSRuntimeFeatures getJSRuntimeFeatures(Object lock) {
return new JSRuntimeFeatures(lock, this);
}
}

View File

@@ -71,22 +71,23 @@ public class OpenhabGraalJSScriptEngine
// shared lock object for synchronization of multi-thread access
private final Object lock = new Object();
private final JSRuntimeFeatures jsRuntimeFeatures = new JSRuntimeFeatures(lock);
private final JSRuntimeFeatures jsRuntimeFeatures;
// these fields start as null because they are populated on first use
private String engineIdentifier;
private Consumer<String> scriptDependencyListener;
private boolean initialized = false;
private String globalScript;
private final String globalScript;
/**
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
* lifecycle and provides hooks for scripts to do so too.
*/
public OpenhabGraalJSScriptEngine(@Nullable String injectionCode) {
public OpenhabGraalJSScriptEngine(@Nullable String injectionCode, JSScriptServiceUtil jsScriptServiceUtil) {
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : "");
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
LOGGER.debug("Initializing GraalJS script engine...");

View File

@@ -13,6 +13,7 @@
package org.openhab.automation.jsscripting.internal.threading;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.util.Map;
@@ -20,7 +21,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.script.ScriptServiceUtil;
import org.openhab.core.automation.module.script.action.ScriptExecution;
import org.openhab.core.automation.module.script.action.Timer;
import org.openhab.core.scheduler.ScheduledCompletableFuture;
import org.openhab.core.scheduler.Scheduler;
import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
@@ -29,20 +31,22 @@ import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
* A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
* the cancel methods) which controls multithreaded execution access to the single-threaded GraalJS contexts.
*
* @author Florian Hotze - Initial contribution
* @author Florian Hotze - Reimplementation to conform standard JS setTimeout and setInterval
* @author Florian Hotze - Initial contribution; Reimplementation to conform standard JS setTimeout and setInterval;
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
*/
public class ThreadsafeTimers {
private final Object lock;
private final Scheduler scheduler;
private final ScriptExecution scriptExecution;
// Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
private final Map<Long, ScheduledCompletableFuture<Object>> idSchedulerMapping = new ConcurrentHashMap<>();
private AtomicLong lastId = new AtomicLong();
private String identifier = "noIdentifier";
public ThreadsafeTimers(Object lock) {
public ThreadsafeTimers(Object lock, ScriptExecution scriptExecution, Scheduler scheduler) {
this.lock = lock;
this.scheduler = ScriptServiceUtil.getScheduler();
this.scheduler = scheduler;
this.scriptExecution = scriptExecution;
}
/**
@@ -55,19 +59,30 @@ public class ThreadsafeTimers {
}
/**
* Schedules a callback to run at a given time.
* Schedules a block of code for later execution.
*
* @param id timerId to append to the identifier base for naming the scheduled job
* @param zdt time to schedule the job
* @param callback function to run at the given time
* @return a {@link ScheduledCompletableFuture}
* @param instant the point in time when the code should be executed
* @param closure the code block to execute
* @return a handle to the created timer, so that it can be canceled or rescheduled
*/
private ScheduledCompletableFuture<Object> createFuture(long id, ZonedDateTime zdt, Runnable callback) {
return scheduler.schedule(() -> {
public Timer createTimer(ZonedDateTime instant, Runnable closure) {
return createTimer(identifier, instant, closure);
}
/**
* Schedules a block of code for later execution.
*
* @param identifier an optional identifier
* @param instant the point in time when the code should be executed
* @param closure the code block to execute
* @return a handle to the created timer, so that it can be canceled or rescheduled
*/
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
return scriptExecution.createTimer(identifier, instant, () -> {
synchronized (lock) {
callback.run();
closure.run();
}
}, identifier + ".timeout." + id, zdt.toInstant());
});
}
/**
@@ -95,8 +110,12 @@ public class ThreadsafeTimers {
*/
public long setTimeout(Runnable callback, Long delay, Object... args) {
long id = lastId.incrementAndGet();
ScheduledCompletableFuture<Object> future = createFuture(id, ZonedDateTime.now().plusNanos(delay * 1000000),
callback);
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
synchronized (lock) {
callback.run();
idSchedulerMapping.remove(id);
}
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
idSchedulerMapping.put(id, future);
return id;
}
@@ -115,22 +134,6 @@ public class ThreadsafeTimers {
}
}
/**
* Schedules a callback to run in a loop with a given delay between the executions.
*
* @param id timerId to append to the identifier base for naming the scheduled job
* @param delay time in milliseconds that the timer should delay in between executions of the callback
* @param callback function to run
*/
private void createLoopingFuture(long id, Long delay, Runnable callback) {
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
synchronized (lock) {
callback.run();
}
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
idSchedulerMapping.put(id, future);
}
/**
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
* Repeatedly calls a function with a fixed time delay between each call.
@@ -156,7 +159,12 @@ public class ThreadsafeTimers {
*/
public long setInterval(Runnable callback, Long delay, Object... args) {
long id = lastId.incrementAndGet();
createLoopingFuture(id, delay, callback);
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
synchronized (lock) {
callback.run();
}
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
idSchedulerMapping.put(id, future);
return id;
}