]> git.basschouten.com Git - openhab-addons.git/blob
513dceee40cab212350eecefb77afba2a3f6a3ed
[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.Instant;
17 import java.time.ZonedDateTime;
18 import java.time.temporal.Temporal;
19 import java.util.Map;
20 import java.util.concurrent.ConcurrentHashMap;
21 import java.util.concurrent.atomic.AtomicLong;
22
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;
29
30 /**
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.
33  *
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}
36  */
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";
45
46     public ThreadsafeTimers(Object lock, ScriptExecution scriptExecution, Scheduler scheduler) {
47         this.lock = lock;
48         this.scheduler = scheduler;
49         this.scriptExecution = scriptExecution;
50     }
51
52     /**
53      * Set the identifier base string used for naming scheduled jobs.
54      *
55      * @param identifier identifier to use
56      */
57     public void setIdentifier(String identifier) {
58         this.identifier = identifier;
59     }
60
61     /**
62      * Schedules a block of code for later execution.
63      *
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
67      */
68     public Timer createTimer(ZonedDateTime instant, Runnable closure) {
69         return createTimer(identifier, instant, closure);
70     }
71
72     /**
73      * Schedules a block of code for later execution.
74      *
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
79      */
80     public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
81         return scriptExecution.createTimer(identifier, instant, () -> {
82             synchronized (lock) {
83                 closure.run();
84             }
85         });
86     }
87
88     /**
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.
91      *
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.
96      */
97     public long setTimeout(Runnable callback, Long delay) {
98         return setTimeout(callback, delay, new Object());
99     }
100
101     /**
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.
104      *
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
107      * @param args
108      * @return Positive integer value which identifies the timer created; this value can be passed to
109      *         <code>clearTimeout()</code> to cancel the timeout.
110      */
111     public long setTimeout(Runnable callback, Long delay, Object... args) {
112         long id = lastId.incrementAndGet();
113         ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
114             synchronized (lock) {
115                 callback.run();
116                 idSchedulerMapping.remove(id);
117             }
118         }, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
119         idSchedulerMapping.put(id, future);
120         return id;
121     }
122
123     /**
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>.
126      *
127      * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
128      *            to setTimeout().
129      */
130     public void clearTimeout(long timeoutId) {
131         ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
132         if (scheduled != null) {
133             scheduled.cancel(true);
134         }
135     }
136
137     /**
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.
140      *
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.
145      */
146     public long setInterval(Runnable callback, Long delay) {
147         return setInterval(callback, delay, new Object());
148     }
149
150     /**
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.
153      *
154      * @param callback function to run
155      * @param delay time in milliseconds that the timer should delay in between executions of the callback
156      * @param args
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.
159      */
160     public long setInterval(Runnable callback, Long delay, Object... args) {
161         long id = lastId.incrementAndGet();
162         ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
163             synchronized (lock) {
164                 callback.run();
165             }
166         }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
167         idSchedulerMapping.put(id, future);
168         return id;
169     }
170
171     /**
172      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
173      * polyfill.
174      * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
175      *
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>.
178      */
179     public void clearInterval(long intervalID) {
180         clearTimeout(intervalID);
181     }
182
183     /**
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
187      * running endless.
188      */
189     public void clearAll() {
190         idSchedulerMapping.forEach((id, future) -> future.cancel(true));
191         idSchedulerMapping.clear();
192     }
193
194     /**
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.
197      *
198      * @author Florian Hotze - Initial contribution
199      */
200     private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
201
202         private Duration delay;
203         private @Nullable Temporal timeDone;
204
205         LoopingAdjuster(Duration delay) {
206             this.delay = delay;
207         }
208
209         @Override
210         public boolean isDone(Temporal temporal) {
211             // Always return false so that a new job will be scheduled
212             return false;
213         }
214
215         @Override
216         public Temporal adjustInto(Temporal temporal) {
217             Temporal localTimeDone = timeDone;
218             Temporal nextTime;
219             if (localTimeDone != null) {
220                 nextTime = localTimeDone.plus(delay);
221             } else {
222                 nextTime = temporal.plus(delay);
223             }
224             timeDone = nextTime;
225             return nextTime;
226         }
227     }
228 }