2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.automation.jsscripting.internal.threading;
15 import java.time.Duration;
16 import java.time.ZonedDateTime;
17 import java.time.temporal.Temporal;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.atomic.AtomicLong;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.core.model.script.ScriptServiceUtil;
24 import org.openhab.core.scheduler.ScheduledCompletableFuture;
25 import org.openhab.core.scheduler.Scheduler;
26 import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
29 * A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
30 * the cancel methods) which controls multithreaded execution access to the single-threaded GraalJS contexts.
32 * @author Florian Hotze - Initial contribution
33 * @author Florian Hotze - Reimplementation to conform standard JS setTimeout and setInterval
35 public class ThreadsafeTimers {
36 private final Object lock;
37 private final Scheduler scheduler;
38 // Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
39 private final Map<Long, ScheduledCompletableFuture<Object>> idSchedulerMapping = new ConcurrentHashMap<>();
40 private AtomicLong lastId = new AtomicLong();
41 private String identifier = "noIdentifier";
43 public ThreadsafeTimers(Object lock) {
45 this.scheduler = ScriptServiceUtil.getScheduler();
49 * Set the identifier base string used for naming scheduled jobs.
51 * @param identifier identifier to use
53 public void setIdentifier(String identifier) {
54 this.identifier = identifier;
58 * Schedules a callback to run at a given time.
60 * @param id timerId to append to the identifier base for naming the scheduled job
61 * @param zdt time to schedule the job
62 * @param callback function to run at the given time
63 * @return a {@link ScheduledCompletableFuture}
65 private ScheduledCompletableFuture<Object> createFuture(long id, ZonedDateTime zdt, Runnable callback) {
66 return scheduler.schedule(() -> {
70 }, identifier + ".timeout." + id, zdt.toInstant());
74 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
75 * Sets a timer which executes a given function once the timer expires.
77 * @param callback function to run after the given delay
78 * @param delay time in milliseconds that the timer should wait before the callback is executed
79 * @return Positive integer value which identifies the timer created; this value can be passed to
80 * <code>clearTimeout()</code> to cancel the timeout.
82 public long setTimeout(Runnable callback, Long delay) {
83 return setTimeout(callback, delay, new Object());
87 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
88 * Sets a timer which executes a given function once the timer expires.
90 * @param callback function to run after the given delay
91 * @param delay time in milliseconds that the timer should wait before the callback is executed
93 * @return Positive integer value which identifies the timer created; this value can be passed to
94 * <code>clearTimeout()</code> to cancel the timeout.
96 public long setTimeout(Runnable callback, Long delay, Object... args) {
97 long id = lastId.incrementAndGet();
98 ScheduledCompletableFuture<Object> future = createFuture(id, ZonedDateTime.now().plusNanos(delay * 1000000),
100 idSchedulerMapping.put(id, future);
105 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout"><code>clearTimeout()</code></a> polyfill.
106 * Cancels a timeout previously created by <code>setTimeout()</code>.
108 * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
111 public void clearTimeout(long timeoutId) {
112 ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
113 if (scheduled != null) {
114 scheduled.cancel(true);
119 * Schedules a callback to run in a loop with a given delay between the executions.
121 * @param id timerId to append to the identifier base for naming the scheduled job
122 * @param delay time in milliseconds that the timer should delay in between executions of the callback
123 * @param callback function to run
125 private void createLoopingFuture(long id, Long delay, Runnable callback) {
126 ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
127 synchronized (lock) {
130 }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
131 idSchedulerMapping.put(id, future);
135 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
136 * Repeatedly calls a function with a fixed time delay between each call.
138 * @param callback function to run
139 * @param delay time in milliseconds that the timer should delay in between executions of the callback
140 * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
141 * <code>clearInterval()</code> to cancel the interval.
143 public long setInterval(Runnable callback, Long delay) {
144 return setInterval(callback, delay, new Object());
148 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
149 * Repeatedly calls a function with a fixed time delay between each call.
151 * @param callback function to run
152 * @param delay time in milliseconds that the timer should delay in between executions of the callback
154 * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
155 * <code>clearInterval()</code> to cancel the interval.
157 public long setInterval(Runnable callback, Long delay, Object... args) {
158 long id = lastId.incrementAndGet();
159 createLoopingFuture(id, delay, callback);
164 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
166 * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
168 * @param intervalID The identifier of the repeated action you want to cancel. This ID was returned by the
169 * corresponding call to <code>setInterval()</code>.
171 public void clearInterval(long intervalID) {
172 clearTimeout(intervalID);
176 * Cancels all timed actions (i.e. timeouts and intervals) that were created with this instance of
177 * {@link ThreadsafeTimers}.
178 * Should be called in a de-initialization/unload hook of the script engine to avoid having scheduled jobs that are
181 public void clearAll() {
182 idSchedulerMapping.forEach((id, future) -> future.cancel(true));
183 idSchedulerMapping.clear();
187 * This is a temporal adjuster that takes a single delay.
188 * This adjuster makes the scheduler run as a fixed rate scheduler from the first time adjustInto was called.
190 * @author Florian Hotze - Initial contribution
192 private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
194 private Duration delay;
195 private @Nullable Temporal timeDone;
197 LoopingAdjuster(Duration delay) {
202 public boolean isDone(Temporal temporal) {
203 // Always return false so that a new job will be scheduled
208 public Temporal adjustInto(Temporal temporal) {
209 Temporal localTimeDone = timeDone;
211 if (localTimeDone != null) {
212 nextTime = localTimeDone.plus(delay);
214 nextTime = temporal.plus(delay);