001package jmri.jmrit.logixng.util;
002
003import java.awt.event.ActionEvent;
004import java.util.*;
005import java.util.concurrent.BlockingQueue;
006import java.util.concurrent.LinkedBlockingQueue;
007
008import javax.swing.Timer;
009import javax.annotation.Nonnull;
010import javax.annotation.concurrent.ThreadSafe;
011
012import jmri.util.*;
013
014import jmri.util.ThreadingUtil.ThreadAction;
015
016/**
017 * Utilities for handling JMRI's LogixNG threading conventions.
018 * <p>
019 * For background, see
020 * <a href="http://jmri.org/help/en/html/doc/Technical/Threads.shtml">http://jmri.org/help/en/html/doc/Technical/Threads.shtml</a>
021 * <p>
022 * This is the ThreadingUtil class for LogixNG.
023 *
024 * @author Bob Jacobsen      Copyright 2015
025 * @author Daniel Bergqvist  Copyright 2020
026 */
027@ThreadSafe
028public class LogixNG_Thread {
029
030    public static final int ERROR_HANDLING_LOGIXNG_THREAD = Integer.MIN_VALUE;
031    public static final int DEFAULT_LOGIXNG_THREAD = 0;
032    public static final int DEFAULT_LOGIXNG_DEBUG_THREAD = 1;
033
034    private static final Map<Integer, LogixNG_Thread> _threads = new HashMap<>();
035    private static final Map<String, LogixNG_Thread> _threadNames = new HashMap<>();
036    private static int _highestThreadID = -1;
037
038    private final int _threadID;
039    private String _name;
040    private volatile boolean _stopThread = false;
041    private volatile boolean _threadIsStopped = false;
042
043    private final Thread _logixNGThread;
044    private boolean _threadInUse = false;
045    private final BlockingQueue<ThreadEvent> _eventQueue;
046    private volatile boolean _queueSizeWarned = false;
047    private static final int MAX_QUEUE_SIZE_BEFORE_WARNING = 1000;
048
049
050    public static LogixNG_Thread createNewThread(@Nonnull String name) {
051        return createNewThread(-1, name);
052    }
053
054    public static LogixNG_Thread createNewThread(int threadID, @Nonnull String name) {
055        synchronized (LogixNG_Thread.class) {
056            if (threadID == -1) {
057                threadID = ++_highestThreadID;
058            } else {
059                if (threadID > _highestThreadID) _highestThreadID = threadID;
060            }
061
062            if (_threads.containsKey(threadID) && _threads.get(threadID)._name.equals(name)) {
063                log.warn("Thread ID {} with name {} already exists", threadID, name);
064                return _threads.get(threadID);
065            }
066
067            if (_threads.containsKey(threadID)) {
068                throw new IllegalArgumentException(String.format("Thread ID %d already exists", threadID));
069            }
070
071            if (_threadNames.containsKey(name)) {
072                throw new IllegalArgumentException(String.format("Thread name %s already exists", name));
073            }
074            LogixNG_Thread thread = new LogixNG_Thread(threadID, name);
075            _threads.put(threadID, thread);
076            _threadNames.put(name, thread);
077            thread._logixNGThread.start();
078
079            return thread;
080        }
081    }
082
083    public static boolean validateNewThreadName(@Nonnull String name) {
084        synchronized (LogixNG_Thread.class) {
085            return !_threadNames.containsKey(name);
086        }
087    }
088
089    public static LogixNG_Thread getThread(int threadID) {
090        synchronized (LogixNG_Thread.class) {
091            LogixNG_Thread thread = _threads.get(threadID);
092            if (thread == null) {
093                switch (threadID) {
094                    case ERROR_HANDLING_LOGIXNG_THREAD:
095                        thread = createNewThread(ERROR_HANDLING_LOGIXNG_THREAD, "Error handling thread");
096                        break;
097                    case DEFAULT_LOGIXNG_THREAD:
098                        thread = createNewThread(DEFAULT_LOGIXNG_THREAD, Bundle.getMessage("LogixNG_Thread"));
099                        break;
100                    case DEFAULT_LOGIXNG_DEBUG_THREAD:
101                        thread = createNewThread(DEFAULT_LOGIXNG_DEBUG_THREAD, Bundle.getMessage("LogixNG_DebugThread"));
102                        break;
103                    default:
104                        throw new IllegalArgumentException(String.format("Thread ID %d does not exists", threadID));
105                }
106            }
107            return thread;
108        }
109    }
110
111    public static int getThreadID(String name) {
112        synchronized (LogixNG_Thread.class) {
113            for (LogixNG_Thread t : _threads.values()) {
114                if (name.equals(t._name)) return t._threadID;
115            }
116            throw new IllegalArgumentException(String.format("Thread name \"%s\" does not exists", name));
117        }
118    }
119
120    public static void deleteThread(LogixNG_Thread thread) {
121        synchronized (LogixNG_Thread.class) {
122            LogixNG_Thread aThread = _threads.get(thread._threadID);
123
124            if (aThread == null) throw new IllegalArgumentException("Thread does not exists");
125            if (aThread != thread) throw new IllegalArgumentException("Thread ID does not match");
126            if (aThread._threadInUse) throw new IllegalArgumentException("Thread is in use");
127
128            _threads.remove(thread._threadID);
129            _threadNames.remove(thread._name);
130        }
131    }
132
133    public static Collection<LogixNG_Thread> getThreads() {
134        var threadsCopy = new HashMap<>(_threads);
135        threadsCopy.remove(ERROR_HANDLING_LOGIXNG_THREAD);
136        return Collections.unmodifiableCollection(threadsCopy.values());
137    }
138
139    private LogixNG_Thread(int threadID, String name) {
140        _threadID = threadID;
141        _name = name;
142
143        synchronized(LogixNG_Thread.class) {
144
145            // The queue has no fixed capacity. If it were full, adding an
146            // event would throw an exception into the code that changed the
147            // sensor, turnout or other bean that LogixNG listens to.
148            _eventQueue = new LinkedBlockingQueue<>();
149            _logixNGThread = new Thread(() -> {
150                while (!_stopThread) {
151                    try {
152                        ThreadEvent event = _eventQueue.take();
153                        if (_eventQueue.isEmpty()) {
154                            // The backlog has cleared, so a later backlog is
155                            // reported again.
156                            _queueSizeWarned = false;
157                        }
158                        if (event._lock != null) {
159                            synchronized(event._lock) {
160                                try {
161                                    if (!_stopThread) event._threadAction.run();
162                                } catch (RuntimeException ex) {
163                                    // Log before the waiting caller continues
164                                    log.error("Exception on LogixNG thread \"{}\"", _name, ex);
165                                } finally {
166                                    event._done = true;
167                                    event._lock.notify();
168                                }
169                            }
170                        } else {
171                            event._threadAction.run();
172                        }
173                    } catch (InterruptedException ex) {
174                        Thread.currentThread().interrupt();
175                    } catch (RuntimeException ex) {
176                        // Keep the thread running, otherwise all LogixNGs
177                        // on this thread would stop.
178                        log.error("Exception on LogixNG thread \"{}\"", _name, ex);
179                    }
180                }
181                _threadIsStopped = true;
182            }, "JMRI LogixNGThread");
183
184            _logixNGThread.setDaemon(true);
185        }
186    }
187
188    public Thread getThread() {
189        return _logixNGThread;
190    }
191
192    public int getThreadId() {
193        return _threadID;
194    }
195
196    public String getThreadName() {
197        return _name;
198    }
199
200    public void setThreadName(@Nonnull String name) {
201        if (_name.equals(name)) return;
202
203        synchronized (LogixNG_Thread.class) {
204            if (_threadNames.containsKey(name)) {
205                throw new IllegalArgumentException(String.format("Thread name %s already exists", name));
206            }
207            _threadNames.remove(_name);
208            _threadNames.put(name, this);
209            _name = name;
210        }
211    }
212
213    public boolean getThreadInUse() {
214        return _threadInUse;
215    }
216
217    /**
218     * Set the thread to "in use".
219     * If a thread is in use, it cannot be unset as not in use.
220     */
221    public void setThreadInUse() {
222        _threadInUse = true;
223    }
224
225    /**
226     * Run some LogixNG-specific code before returning.
227     * <p>
228     * Typical uses:
229     * <p> {@code
230     * ThreadingUtil.runOnLogixNG(() -> {
231     *     logixNG.doSomething(value);
232     * });
233     * }
234     *
235     * @param ta What to run, usually as a lambda expression
236     */
237    public void runOnLogixNG(@Nonnull ThreadAction ta) {
238        if (_logixNGThread != null) {
239            Object lock = new Object();
240            synchronized(lock) {
241                ThreadEvent event = new ThreadEvent(ta, lock);
242                addEvent(event);
243                try {
244                    // Wait in a loop since wait() may return before notify()
245                    // has been called (spurious wakeup).
246                    while (!event._done) {
247                        lock.wait();
248                    }
249                } catch (InterruptedException e) {
250                    log.debug("Interrupted while running on LogixNG thread");
251                    Thread.currentThread().interrupt();
252                }
253            }
254        } else {
255            throw new RuntimeException("LogixNG thread not started. ThreadID: "+Integer.toString(_threadID));
256        }
257    }
258
259    /**
260     * Run some LogixNG-specific code at some later point.
261     * <p>
262     * Please note the operation may have happened before this returns. Or
263     * later. No long-term guarantees.
264     * <p>
265     * Typical uses:
266     * <p> {@code
267     * ThreadingUtil.runOnLogixNGEventually(() -> {
268     *     sensor.setState(value);
269     * });
270     * }
271     *
272     * @param ta What to run, usually as a lambda expression
273     */
274    public void runOnLogixNGEventually(@Nonnull ThreadAction ta) {
275        if (_logixNGThread != null) {
276            addEvent(new ThreadEvent(ta));
277        } else {
278            throw new RuntimeException("LogixNG thread not started");
279        }
280    }
281
282    /**
283     * Run some LogixNG-specific code at some later point, at least a known time
284     * in the future.
285     * <p>
286     * There is no long-term guarantee about the accuracy of the interval.
287     * <p>
288     * Typical uses:
289     * <p> {@code
290     * ThreadingUtil.runOnLogixNGDelayed(() -> {
291     *     sensor.setState(value);
292     * }, 1000);
293     * }
294     *
295     * @param ta    What to run, usually as a lambda expression
296     * @param delay interval in milliseconds
297     * @return reference to timer object handling delay so you can cancel if desired; note that operation may have already taken place.
298     */
299    @Nonnull
300    public Timer runOnLogixNGDelayed(@Nonnull ThreadAction ta, int delay) {
301        if (_logixNGThread != null) {
302            // dispatch to logixng thread via timer. We are forced to use a
303            // Swing Timer since the method returns a Timer object and we don't
304            // want to change the method interface.
305            Timer timer = new Timer(delay, (ActionEvent e) -> {
306                // Dispatch the event to the LogixNG event handler once the time
307                // has passed.
308                addEvent(new ThreadEvent(ta));
309            });
310            timer.setRepeats(false);
311            timer.start();
312            return timer;
313        } else {
314            throw new RuntimeException("LogixNG thread not started");
315        }
316    }
317
318    /**
319     * Add an event to the queue. A warning is logged once each time the queue
320     * grows past {@value #MAX_QUEUE_SIZE_BEFORE_WARNING} events. The warning
321     * is re-armed when the LogixNG thread has emptied the queue, so each
322     * separate backlog is reported, but a long backlog is reported only once.
323     *
324     * @param event the event to add
325     */
326    private void addEvent(ThreadEvent event) {
327        _eventQueue.add(event);
328        if (!_queueSizeWarned && _eventQueue.size() > MAX_QUEUE_SIZE_BEFORE_WARNING) {
329            _queueSizeWarned = true;
330            log.warn("The LogixNG thread \"{}\" has more than {} events waiting. LogixNG may be too busy or blocked.",
331                    _name, MAX_QUEUE_SIZE_BEFORE_WARNING);
332        }
333    }
334
335    public boolean isQueueEmpty() {
336        return _eventQueue.isEmpty();
337    }
338
339    /**
340     * Check if on the LogixNG-operation thread.
341     *
342     * @return true if on the LogixNG-operation thread
343     */
344    public boolean isLogixNGThread() {
345        if (_logixNGThread != null) {
346            return _logixNGThread == Thread.currentThread();
347        } else {
348            throw new RuntimeException("LogixNG thread not started");
349        }
350    }
351
352    /**
353     * Checks if the the current thread is the LogixNG thread.
354     * The check is only done if debug is enabled.
355     */
356    public void checkIsLogixNGThread() {
357        if (log.isDebugEnabled()) {
358            if (!isLogixNGThread()) {
359                LoggingUtil.warnOnce(log, "checkIsLogixNGThread() called on wrong thread", new Exception());
360            }
361        }
362    }
363
364    private static class ThreadEvent {
365        private final ThreadAction _threadAction;
366        private final Object _lock;
367        private boolean _done = false;     // Guarded by _lock
368
369        public ThreadEvent(ThreadAction threadAction) {
370            _threadAction = threadAction;
371            _lock = null;
372        }
373
374        public ThreadEvent(ThreadAction threadAction,
375                Object lock) {
376            _threadAction = threadAction;
377            _lock = lock;
378        }
379    }
380
381    private void stopLogixNGThread() {
382        synchronized(LogixNG_Thread.class) {
383            if (_logixNGThread != null) {
384                _stopThread = true;
385                _logixNGThread.interrupt();
386                try {
387                    _logixNGThread.join(0);
388                } catch (InterruptedException e) {
389                    throw new RuntimeException("stopLogixNGThread() was interrupted");
390                }
391                if (_logixNGThread.getState() != Thread.State.TERMINATED) {
392                    throw new RuntimeException("Could not stop logixNGThread. Current state: "+_logixNGThread.getState().name());
393                }
394                _threads.remove(_threadID);
395                _threadNames.remove(_name);
396                _stopThread = false;
397            }
398        }
399    }
400
401    public static void stopAllLogixNGThreads() {
402        synchronized(LogixNG_Thread.class) {
403            List<LogixNG_Thread> list = new ArrayList<>(_threads.values());
404            for (LogixNG_Thread thread : list) {
405                thread.stopLogixNGThread();
406            }
407        }
408    }
409
410    public static void assertLogixNGThreadNotRunning() {
411        synchronized(LogixNG_Thread.class) {
412            boolean aThreadIsRunning = false;
413            for (LogixNG_Thread thread : _threads.values()) {
414                if (!thread._threadIsStopped) {
415                    aThreadIsRunning = true;
416                    thread.stopLogixNGThread();
417                }
418            }
419            if (aThreadIsRunning) {
420                throw new RuntimeException("logixNGThread is running");
421            }
422        }
423    }
424
425    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogixNG_Thread.class);
426
427}
428