]> git.basschouten.com Git - openhab-addons.git/blob
2f45a650112c90ab3184edef5fbeaa8fd8bf4019
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 import java.util.concurrent.locks.Lock;
23
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.automation.module.script.action.ScriptExecution;
26 import org.openhab.core.automation.module.script.action.Timer;
27 import org.openhab.core.scheduler.ScheduledCompletableFuture;
28 import org.openhab.core.scheduler.Scheduler;
29 import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
30
31 /**
32  * A polyfill implementation of NodeJS timer functionality (<code>setTimeout()</code>, <code>setInterval()</code> and
33  * the cancel methods) which controls multithreaded execution access to the single-threaded GraalJS contexts.
34  *
35  * @author Florian Hotze - Initial contribution; Reimplementation to conform standard JS setTimeout and setInterval;
36  *         Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
37  */
38 public class ThreadsafeTimers {
39     private final Lock lock;
40     private final Scheduler scheduler;
41     private final ScriptExecution scriptExecution;
42     // Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
43     private final Map<Long, ScheduledCompletableFuture<Object>> idSchedulerMapping = new ConcurrentHashMap<>();
44     private AtomicLong lastId = new AtomicLong();
45     private String identifier = "noIdentifier";
46
47     public ThreadsafeTimers(Lock lock, ScriptExecution scriptExecution, Scheduler scheduler) {
48         this.lock = lock;
49         this.scheduler = scheduler;
50         this.scriptExecution = scriptExecution;
51     }
52
53     /**
54      * Set the identifier base string used for naming scheduled jobs.
55      *
56      * @param identifier identifier to use
57      */
58     public void setIdentifier(String identifier) {
59         this.identifier = identifier;
60     }
61
62     /**
63      * Schedules a block of code for later execution.
64      *
65      * @param instant the point in time when the code should be executed
66      * @param closure the code block to execute
67      * @return a handle to the created timer, so that it can be canceled or rescheduled
68      */
69     public Timer createTimer(ZonedDateTime instant, Runnable closure) {
70         return createTimer(identifier, instant, closure);
71     }
72
73     /**
74      * Schedules a block of code for later execution.
75      *
76      * @param identifier an optional identifier
77      * @param instant the point in time when the code should be executed
78      * @param closure the code block to execute
79      * @return a handle to the created timer, so that it can be canceled or rescheduled
80      */
81     public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
82         return scriptExecution.createTimer(identifier, instant, () -> {
83             lock.lock();
84             try {
85                 closure.run();
86             } finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
87                         // deadlocks
88                 lock.unlock();
89             }
90         });
91     }
92
93     /**
94      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
95      * Sets a timer which executes a given function once the timer expires.
96      *
97      * @param callback function to run after the given delay
98      * @param delay time in milliseconds that the timer should wait before the callback is executed
99      * @return Positive integer value which identifies the timer created; this value can be passed to
100      *         <code>clearTimeout()</code> to cancel the timeout.
101      */
102     public long setTimeout(Runnable callback, Long delay) {
103         return setTimeout(callback, delay, new Object());
104     }
105
106     /**
107      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setTimeout"><code>setTimeout()</code></a> polyfill.
108      * Sets a timer which executes a given function once the timer expires.
109      *
110      * @param callback function to run after the given delay
111      * @param delay time in milliseconds that the timer should wait before the callback is executed
112      * @param args
113      * @return Positive integer value which identifies the timer created; this value can be passed to
114      *         <code>clearTimeout()</code> to cancel the timeout.
115      */
116     public long setTimeout(Runnable callback, Long delay, @Nullable Object... args) {
117         long id = lastId.incrementAndGet();
118         ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
119             lock.lock();
120             try {
121                 callback.run();
122                 idSchedulerMapping.remove(id);
123             } finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
124                         // deadlocks
125                 lock.unlock();
126             }
127         }, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
128         idSchedulerMapping.put(id, future);
129         return id;
130     }
131
132     /**
133      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout"><code>clearTimeout()</code></a> polyfill.
134      * Cancels a timeout previously created by <code>setTimeout()</code>.
135      *
136      * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call
137      *            to setTimeout().
138      */
139     public void clearTimeout(long timeoutId) {
140         ScheduledCompletableFuture<Object> scheduled = idSchedulerMapping.remove(timeoutId);
141         if (scheduled != null) {
142             scheduled.cancel(true);
143         }
144     }
145
146     /**
147      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
148      * Repeatedly calls a function with a fixed time delay between each call.
149      *
150      * @param callback function to run
151      * @param delay time in milliseconds that the timer should delay in between executions of the callback
152      * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
153      *         <code>clearInterval()</code> to cancel the interval.
154      */
155     public long setInterval(Runnable callback, Long delay) {
156         return setInterval(callback, delay, new Object());
157     }
158
159     /**
160      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/setInterval"><code>setInterval()</code></a> polyfill.
161      * Repeatedly calls a function with a fixed time delay between each call.
162      *
163      * @param callback function to run
164      * @param delay time in milliseconds that the timer should delay in between executions of the callback
165      * @param args
166      * @return Numeric, non-zero value which identifies the timer created; this value can be passed to
167      *         <code>clearInterval()</code> to cancel the interval.
168      */
169     public long setInterval(Runnable callback, Long delay, @Nullable Object... args) {
170         long id = lastId.incrementAndGet();
171         ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
172             lock.lock();
173             try {
174                 callback.run();
175             } finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
176                         // deadlocks
177                 lock.unlock();
178             }
179         }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
180         idSchedulerMapping.put(id, future);
181         return id;
182     }
183
184     /**
185      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearInterval"><code>clearInterval()</code></a>
186      * polyfill.
187      * Cancels a timed, repeating action which was previously established by a call to <code>setInterval()</code>.
188      *
189      * @param intervalID The identifier of the repeated action you want to cancel. This ID was returned by the
190      *            corresponding call to <code>setInterval()</code>.
191      */
192     public void clearInterval(long intervalID) {
193         clearTimeout(intervalID);
194     }
195
196     /**
197      * Cancels all timed actions (i.e. timeouts and intervals) that were created with this instance of
198      * {@link ThreadsafeTimers}.
199      * Should be called in a de-initialization/unload hook of the script engine to avoid having scheduled jobs that are
200      * running endless.
201      */
202     public void clearAll() {
203         idSchedulerMapping.forEach((id, future) -> future.cancel(true));
204         idSchedulerMapping.clear();
205     }
206
207     /**
208      * This is a temporal adjuster that takes a single delay.
209      * This adjuster makes the scheduler run as a fixed rate scheduler from the first time adjustInto was called.
210      *
211      * @author Florian Hotze - Initial contribution
212      */
213     private static class LoopingAdjuster implements SchedulerTemporalAdjuster {
214
215         private Duration delay;
216         private @Nullable Temporal timeDone;
217
218         LoopingAdjuster(Duration delay) {
219             this.delay = delay;
220         }
221
222         @Override
223         public boolean isDone(Temporal temporal) {
224             // Always return false so that a new job will be scheduled
225             return false;
226         }
227
228         @Override
229         public Temporal adjustInto(Temporal temporal) {
230             Temporal localTimeDone = timeDone;
231             Temporal nextTime;
232             if (localTimeDone != null) {
233                 nextTime = localTimeDone.plus(delay);
234             } else {
235                 nextTime = temporal.plus(delay);
236             }
237             timeDone = nextTime;
238             return nextTime;
239         }
240     }
241 }