]> git.basschouten.com Git - openhab-addons.git/blob
8b88ac3aad4d9db0eff034ed2123dd9cbf1fe193
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.automation.jsscripting.internal.threading;
14
15 import java.time.Duration;
16 import java.time.ZonedDateTime;
17 import java.time.temporal.Temporal;
18 import java.util.Map;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.atomic.AtomicLong;
21
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;
27
28 /**
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.
31  *
32  * @author Florian Hotze - Initial contribution
33  * @author Florian Hotze - Reimplementation to conform standard JS setTimeout and setInterval
34  */
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";
42
43     public ThreadsafeTimers(Object lock) {
44         this.lock = lock;
45         this.scheduler = ScriptServiceUtil.getScheduler();
46     }
47
48     /**
49      * Set the identifier base string used for naming scheduled jobs.
50      *
51      * @param identifier identifier to use
52      */
53     public void setIdentifier(String identifier) {
54         this.identifier = identifier;
55     }
56
57     /**
58      * Schedules a callback to run at a given time.
59      *
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}
64      */
65     private ScheduledCompletableFuture<Object> createFuture(long id, ZonedDateTime zdt, Runnable callback) {
66         return scheduler.schedule(() -> {
67             synchronized (lock) {
68                 callback.run();
69             }
70         }, identifier + ".timeout." + id, zdt.toInstant());
71     }
72
73     /**
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.
76      *
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.
81      */
82     public long setTimeout(Runnable callback, Long delay) {
83         return setTimeout(callback, delay, new Object());
84     }
85
86     /**
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.
89      *
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
92      * @param args
93      * @return Positive integer value which identifies the timer created; this value can be passed to
94      *         <code>clearTimeout()</code> to cancel the timeout.
95      */
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),
99                 callback);
100         idSchedulerMapping.put(id, future);
101         return id;
102     }
103
104     /**
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>.
107      *
108      * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
109      *            to setTimeout().
110      */
111     public void clearTimeout(long timeoutId) {
112         ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
113         if (scheduled != null) {
114             scheduled.cancel(true);
115         }
116     }
117
118     /**
119      * Schedules a callback to run in a loop with a given delay between the executions.
120      *
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
124      */
125     private void createLoopingFuture(long id, Long delay, Runnable callback) {
126         ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
127             synchronized (lock) {
128                 callback.run();
129             }
130         }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
131         idSchedulerMapping.put(id, future);
132     }
133
134     /**
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.
137      *
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.
142      */
143     public long setInterval(Runnable callback, Long delay) {
144         return setInterval(callback, delay, new Object());
145     }
146
147     /**
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.
150      *
151      * @param callback function to run
152      * @param delay time in milliseconds that the timer should delay in between executions of the callback
153      * @param args
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.
156      */
157     public long setInterval(Runnable callback, Long delay, Object... args) {
158         long id = lastId.incrementAndGet();
159         createLoopingFuture(id, delay, callback);
160         return id;
161     }
162
163     /**
164      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
165      * polyfill.
166      * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
167      *
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>.
170      */
171     public void clearInterval(long intervalID) {
172         clearTimeout(intervalID);
173     }
174
175     /**
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
179      * running endless.
180      */
181     public void clearAll() {
182         idSchedulerMapping.forEach((id, future) -> future.cancel(true));
183         idSchedulerMapping.clear();
184     }
185
186     /**
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.
189      *
190      * @author Florian Hotze - Initial contribution
191      */
192     private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
193
194         private Duration delay;
195         private @Nullable Temporal timeDone;
196
197         LoopingAdjuster(Duration delay) {
198             this.delay = delay;
199         }
200
201         @Override
202         public boolean isDone(Temporal temporal) {
203             // Always return false so that a new job will be scheduled
204             return false;
205         }
206
207         @Override
208         public Temporal adjustInto(Temporal temporal) {
209             Temporal localTimeDone = timeDone;
210             Temporal nextTime;
211             if (localTimeDone != null) {
212                 nextTime = localTimeDone.plus(delay);
213             } else {
214                 nextTime = temporal.plus(delay);
215             }
216             timeDone = nextTime;
217             return nextTime;
218         }
219     }
220 }