*/
package org.openhab.automation.jsscripting.internal.threading;
+import java.time.Duration;
import java.time.ZonedDateTime;
-import java.util.concurrent.TimeUnit;
+import java.time.temporal.Temporal;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
-import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.script.ScriptServiceUtil;
-import org.openhab.core.model.script.actions.Timer;
import org.openhab.core.scheduler.ScheduledCompletableFuture;
import org.openhab.core.scheduler.Scheduler;
-import org.openhab.core.scheduler.SchedulerRunnable;
+import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
/**
- * A replacement for the timer functionality of {@link org.openhab.core.model.script.actions.ScriptExecution
- * ScriptExecution} which controls multithreaded execution access to the single-threaded GraalJS contexts.
+ * 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
*/
public class ThreadsafeTimers {
private final Object lock;
+ private final Scheduler scheduler;
+ // 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) {
this.lock = lock;
+ this.scheduler = ScriptServiceUtil.getScheduler();
}
- public Timer createTimer(ZonedDateTime instant, Runnable callable) {
- return createTimer(null, instant, callable);
+ /**
+ * Set the identifier base string used for naming scheduled jobs.
+ *
+ * @param identifier identifier to use
+ */
+ public void setIdentifier(String identifier) {
+ this.identifier = identifier;
}
- public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable callable) {
- Scheduler scheduler = ScriptServiceUtil.getScheduler();
-
- return new TimerImpl(scheduler, instant, () -> {
+ /**
+ * Schedules a callback to run at a given time.
+ *
+ * @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}
+ */
+ private ScheduledCompletableFuture<Object> createFuture(long id, ZonedDateTime zdt, Runnable callback) {
+ return scheduler.schedule(() -> {
synchronized (lock) {
- callable.run();
+ callback.run();
}
+ }, identifier + ".timeout." + id, zdt.toInstant());
+ }
- }, identifier);
+ /**
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
+ * Sets a timer which executes a given function once the timer expires.
+ *
+ * @param callback function to run after the given delay
+ * @param delay time in milliseconds that the timer should wait before the callback is executed
+ * @return Positive integer value which identifies the timer created; this value can be passed to
+ * <code>clearTimeout()</code> to cancel the timeout.
+ */
+ public long setTimeout(Runnable callback, Long delay) {
+ return setTimeout(callback, delay, new Object());
}
- public Timer createTimerWithArgument(ZonedDateTime instant, Object arg1, Runnable callable) {
- return createTimerWithArgument(null, instant, arg1, callable);
+ /**
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
+ * Sets a timer which executes a given function once the timer expires.
+ *
+ * @param callback function to run after the given delay
+ * @param delay time in milliseconds that the timer should wait before the callback is executed
+ * @param args
+ * @return Positive integer value which identifies the timer created; this value can be passed to
+ * <code>clearTimeout()</code> to cancel the timeout.
+ */
+ public long setTimeout(Runnable callback, Long delay, Object... args) {
+ long id = lastId.incrementAndGet();
+ ScheduledCompletableFuture<Object> future = createFuture(id, ZonedDateTime.now().plusNanos(delay * 1000000),
+ callback);
+ idSchedulerMapping.put(id, future);
+ return id;
}
- public Timer createTimerWithArgument(@Nullable String identifier, ZonedDateTime instant, Object arg1,
- Runnable callable) {
- Scheduler scheduler = ScriptServiceUtil.getScheduler();
- return new TimerImpl(scheduler, instant, () -> {
+ /**
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout"><code>clearTimeout()</code></a> polyfill.
+ * Cancels a timeout previously created by <code>setTimeout()</code>.
+ *
+ * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
+ * to setTimeout().
+ */
+ public void clearTimeout(long timeoutId) {
+ ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
+ if (scheduled != null) {
+ scheduled.cancel(true);
+ }
+ }
+
+ /**
+ * 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) {
- callable.run();
+ callback.run();
}
-
- }, identifier);
+ }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
+ idSchedulerMapping.put(id, future);
}
/**
- * This is an implementation of the {@link Timer} interface.
- * Copy of {@link org.openhab.core.model.script.internal.actions.TimerImpl} as this is not accessible from outside
- * the
- * package.
+ * <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.
*
- * @author Kai Kreuzer - Initial contribution
+ * @param callback function to run
+ * @param delay time in milliseconds that the timer should delay in between executions of the callback
+ * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
+ * <code>clearInterval()</code> to cancel the interval.
*/
- @NonNullByDefault
- public static class TimerImpl implements Timer {
-
- private final Scheduler scheduler;
- private final ZonedDateTime startTime;
- private final SchedulerRunnable runnable;
- private final @Nullable String identifier;
- private ScheduledCompletableFuture<?> future;
-
- public TimerImpl(Scheduler scheduler, ZonedDateTime startTime, SchedulerRunnable runnable) {
- this(scheduler, startTime, runnable, null);
- }
-
- public TimerImpl(Scheduler scheduler, ZonedDateTime startTime, SchedulerRunnable runnable,
- @Nullable String identifier) {
- this.scheduler = scheduler;
- this.startTime = startTime;
- this.runnable = runnable;
- this.identifier = identifier;
+ public long setInterval(Runnable callback, Long delay) {
+ return setInterval(callback, delay, new Object());
+ }
- future = scheduler.schedule(runnable, identifier, startTime.toInstant());
- }
+ /**
+ * <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.
+ *
+ * @param callback function to run
+ * @param delay time in milliseconds that the timer should delay in between executions of the callback
+ * @param args
+ * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
+ * <code>clearInterval()</code> to cancel the interval.
+ */
+ public long setInterval(Runnable callback, Long delay, Object... args) {
+ long id = lastId.incrementAndGet();
+ createLoopingFuture(id, delay, callback);
+ return id;
+ }
- @Override
- public boolean cancel() {
- return future.cancel(true);
- }
+ /**
+ * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
+ * polyfill.
+ * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
+ *
+ * @param intervalID The identifier of the repeated action you want to cancel. This ID was returned by the
+ * corresponding call to <code>setInterval()</code>.
+ */
+ public void clearInterval(long intervalID) {
+ clearTimeout(intervalID);
+ }
- @Override
- public synchronized boolean reschedule(ZonedDateTime newTime) {
- future.cancel(false);
- future = scheduler.schedule(runnable, identifier, newTime.toInstant());
- return true;
- }
+ /**
+ * Cancels all timed actions (i.e. timeouts and intervals) that were created with this instance of
+ * {@link ThreadsafeTimers}.
+ * Should be called in a de-initialization/unload hook of the script engine to avoid having scheduled jobs that are
+ * running endless.
+ */
+ public void clearAll() {
+ idSchedulerMapping.forEach((id, future) -> future.cancel(true));
+ idSchedulerMapping.clear();
+ }
- @Override
- public @Nullable ZonedDateTime getExecutionTime() {
- return future.isCancelled() ? null : ZonedDateTime.now().plusNanos(future.getDelay(TimeUnit.NANOSECONDS));
- }
+ /**
+ * This is a temporal adjuster that takes a single delay.
+ * This adjuster makes the scheduler run as a fixed rate scheduler from the first time adjustInto was called.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+ private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
- @Override
- public boolean isActive() {
- return !future.isDone();
- }
+ private Duration delay;
+ private @Nullable Temporal timeDone;
- @Override
- public boolean isCancelled() {
- return future.isCancelled();
+ LoopingAdjuster(Duration delay) {
+ this.delay = delay;
}
@Override
- public boolean isRunning() {
- return isActive() && ZonedDateTime.now().isAfter(startTime);
+ public boolean isDone(Temporal temporal) {
+ // Always return false so that a new job will be scheduled
+ return false;
}
@Override
- public boolean hasTerminated() {
- return future.isDone();
+ public Temporal adjustInto(Temporal temporal) {
+ Temporal localTimeDone = timeDone;
+ Temporal nextTime;
+ if (localTimeDone != null) {
+ nextTime = localTimeDone.plus(delay);
+ } else {
+ nextTime = temporal.plus(delay);
+ }
+ timeDone = nextTime;
+ return nextTime;
}
}
}
+// ThreadsafeTimers is injected into the JS runtime
(function (global) {
'use strict';
// Append the script file name OR rule UID depending on which is available
const defaultIdentifier = "org.openhab.automation.script" + (globalThis["javax.script.filename"] ? ".file." + globalThis["javax.script.filename"].replace(/^.*[\\\/]/, '') : globalThis["ruleUID"] ? ".ui." + globalThis["ruleUID"] : "");
const System = Java.type('java.lang.System');
- const ZonedDateTime = Java.type('java.time.ZonedDateTime');
const formatRegExp = /%[sdj%]/g;
+ // Pass the defaultIdentifier to ThreadsafeTimers to enable naming of scheduled jobs
+ ThreadsafeTimers.setIdentifier(defaultIdentifier);
function createLogger(name = defaultIdentifier) {
return Java.type("org.slf4j.LoggerFactory").getLogger(name);
},
// Allow user customizable logging names
+ // Be aware that a log4j2 required a logger defined for the logger name, otherwise messages won't be logged!
set loggerName(name) {
log = createLogger(name);
this._loggerName = name;
+ ThreadsafeTimers.setIdentifier(name);
},
get loggerName() {
- return this._loggerName || defaultLoggerName;
+ return this._loggerName || defaultIdentifier;
}
};
- function setTimeout(cb, delay) {
- const args = Array.prototype.slice.call(arguments, 2);
- return ThreadsafeTimers.createTimerWithArgument(
- defaultIdentifier + '.setTimeout',
- ZonedDateTime.now().plusNanos(delay * 1000000),
- args,
- function (args) {
- cb.apply(global, args);
- }
- );
- }
-
- function clearTimeout(timer) {
- if (timer !== undefined && timer.isActive()) {
- timer.cancel();
- }
- }
-
- function setInterval(cb, delay) {
- const args = Array.prototype.slice.call(arguments, 2);
- const delayNanos = delay * 1000000
- let timer = ThreadsafeTimers.createTimerWithArgument(
- defaultIdentifier + '.setInterval',
- ZonedDateTime.now().plusNanos(delayNanos),
- args,
- function (args) {
- cb.apply(global, args);
- if (!timer.isCancelled()) {
- timer.reschedule(ZonedDateTime.now().plusNanos(delayNanos));
- }
- }
- );
- return timer;
- }
-
- function clearInterval(timer) {
- clearTimeout(timer);
- }
-
// Polyfill common NodeJS functions onto the global object
globalThis.console = console;
- globalThis.setTimeout = setTimeout;
- globalThis.clearTimeout = clearTimeout;
- globalThis.setInterval = setInterval;
- globalThis.clearInterval = clearInterval;
+ globalThis.setTimeout = ThreadsafeTimers.setTimeout;
+ globalThis.clearTimeout = ThreadsafeTimers.clearTimeout;
+ globalThis.setInterval = ThreadsafeTimers.setInterval;
+ globalThis.clearInterval = ThreadsafeTimers.clearInterval;
// Support legacy NodeJS libraries
globalThis.global = globalThis;