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