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.Instant;
17 import java.time.ZonedDateTime;
18 import java.time.temporal.Temporal;
20 import java.util.concurrent.ConcurrentHashMap;
21 import java.util.concurrent.atomic.AtomicLong;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.core.automation.module.script.action.ScriptExecution;
25 import org.openhab.core.automation.module.script.action.Timer;
26 import org.openhab.core.scheduler.ScheduledCompletableFuture;
27 import org.openhab.core.scheduler.Scheduler;
28 import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
31 * A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
32 * the cancel methods) which controls multithreaded execution access to the single-threaded GraalJS contexts.
34 * @author Florian Hotze - Initial contribution; Reimplementation to conform standard JS setTimeout and setInterval;
35 * Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
37 public class ThreadsafeTimers {
38 private final Object lock;
39 private final Scheduler scheduler;
40 private final ScriptExecution scriptExecution;
41 // Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
42 private final Map<Long, ScheduledCompletableFuture<Object>> idSchedulerMapping = new ConcurrentHashMap<>();
43 private AtomicLong lastId = new AtomicLong();
44 private String identifier = "noIdentifier";
46 public ThreadsafeTimers(Object lock, ScriptExecution scriptExecution, Scheduler scheduler) {
48 this.scheduler = scheduler;
49 this.scriptExecution = scriptExecution;
53 * Set the identifier base string used for naming scheduled jobs.
55 * @param identifier identifier to use
57 public void setIdentifier(String identifier) {
58 this.identifier = identifier;
62 * Schedules a block of code for later execution.
64 * @param instant the point in time when the code should be executed
65 * @param closure the code block to execute
66 * @return a handle to the created timer, so that it can be canceled or rescheduled
68 public Timer createTimer(ZonedDateTime instant, Runnable closure) {
69 return createTimer(identifier, instant, closure);
73 * Schedules a block of code for later execution.
75 * @param identifier an optional identifier
76 * @param instant the point in time when the code should be executed
77 * @param closure the code block to execute
78 * @return a handle to the created timer, so that it can be canceled or rescheduled
80 public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
81 return scriptExecution.createTimer(identifier, instant, () -> {
89 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
90 * Sets a timer which executes a given function once the timer expires.
92 * @param callback function to run after the given delay
93 * @param delay time in milliseconds that the timer should wait before the callback is executed
94 * @return Positive integer value which identifies the timer created; this value can be passed to
95 * <code>clearTimeout()</code> to cancel the timeout.
97 public long setTimeout(Runnable callback, Long delay) {
98 return setTimeout(callback, delay, new Object());
102 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
103 * Sets a timer which executes a given function once the timer expires.
105 * @param callback function to run after the given delay
106 * @param delay time in milliseconds that the timer should wait before the callback is executed
108 * @return Positive integer value which identifies the timer created; this value can be passed to
109 * <code>clearTimeout()</code> to cancel the timeout.
111 public long setTimeout(Runnable callback, Long delay, Object... args) {
112 long id = lastId.incrementAndGet();
113 ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
114 synchronized (lock) {
116 idSchedulerMapping.remove(id);
118 }, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
119 idSchedulerMapping.put(id, future);
124 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout"><code>clearTimeout()</code></a> polyfill.
125 * Cancels a timeout previously created by <code>setTimeout()</code>.
127 * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
130 public void clearTimeout(long timeoutId) {
131 ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
132 if (scheduled != null) {
133 scheduled.cancel(true);
138 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
139 * Repeatedly calls a function with a fixed time delay between each call.
141 * @param callback function to run
142 * @param delay time in milliseconds that the timer should delay in between executions of the callback
143 * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
144 * <code>clearInterval()</code> to cancel the interval.
146 public long setInterval(Runnable callback, Long delay) {
147 return setInterval(callback, delay, new Object());
151 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
152 * Repeatedly calls a function with a fixed time delay between each call.
154 * @param callback function to run
155 * @param delay time in milliseconds that the timer should delay in between executions of the callback
157 * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
158 * <code>clearInterval()</code> to cancel the interval.
160 public long setInterval(Runnable callback, Long delay, Object... args) {
161 long id = lastId.incrementAndGet();
162 ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
163 synchronized (lock) {
166 }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
167 idSchedulerMapping.put(id, future);
172 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
174 * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
176 * @param intervalID The identifier of the repeated action you want to cancel. This ID was returned by the
177 * corresponding call to <code>setInterval()</code>.
179 public void clearInterval(long intervalID) {
180 clearTimeout(intervalID);
184 * Cancels all timed actions (i.e. timeouts and intervals) that were created with this instance of
185 * {@link ThreadsafeTimers}.
186 * Should be called in a de-initialization/unload hook of the script engine to avoid having scheduled jobs that are
189 public void clearAll() {
190 idSchedulerMapping.forEach((id, future) -> future.cancel(true));
191 idSchedulerMapping.clear();
195 * This is a temporal adjuster that takes a single delay.
196 * This adjuster makes the scheduler run as a fixed rate scheduler from the first time adjustInto was called.
198 * @author Florian Hotze - Initial contribution
200 private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
202 private Duration delay;
203 private @Nullable Temporal timeDone;
205 LoopingAdjuster(Duration delay) {
210 public boolean isDone(Temporal temporal) {
211 // Always return false so that a new job will be scheduled
216 public Temporal adjustInto(Temporal temporal) {
217 Temporal localTimeDone = timeDone;
219 if (localTimeDone != null) {
220 nextTime = localTimeDone.plus(delay);
222 nextTime = temporal.plus(delay);