/* * Copyright 2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.scheduler; import java.util.PriorityQueue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import ccre.channel.EventOutput; import ccre.concurrency.ReporterThread; import ccre.log.Logger; import ccre.time.Time; import ccre.util.ThreadedAllocationPool; import ccre.verifier.FlowPhase; import ccre.verifier.SuppressPhaseWarnings; class RunLoop extends ReporterThread implements IRunLoop { public RunLoop() { super("RunLoop"); this.setPriority(Thread.MAX_PRIORITY - 1); } private static class Entry implements Comparable<Entry> { public EventOutput target; public long time; public String tag; @Override public int compareTo(Entry o) { return Long.compare(time, o.time); } @FlowPhase public Entry populate(String tag, EventOutput target, long time) { this.tag = tag; this.time = time; this.target = target; return this; } } private final ReentrantLock queueLock = new ReentrantLock(); private final Condition update = queueLock.newCondition(); private final PriorityQueue<Entry> queue = new PriorityQueue<>(1024); private final ThreadedAllocationPool<Entry> pool = new ThreadedAllocationPool<>(1024, Entry::new); private volatile boolean terminated; @Override @SuppressPhaseWarnings // deadlock issues demonstrated to be absent public void add(String tag, EventOutput event, long time) { Entry ent = pool.allocate().populate(tag, event, time); queueLock.lock(); try { queue.add(ent); update.signalAll(); } finally { queueLock.unlock(); } } @Override protected void threadBody() { try { queueLock.lockInterruptibly(); try { while (!terminated) { // loop until we have something ready to run Entry ent = queue.peek(); long now = Time.currentTimeNanos(); if (ent == null) { // just wait until we actually HAVE something reportAwaiting(true); // TODO: what are the guarantees if this throws an // exception? update.await(); reportAwaiting(false); } else if (ent.time > now) { // not yet time to run reportAwaiting(true); update.awaitNanos(ent.time - now); reportAwaiting(false); } else { // ready to run an event! ent = queue.remove(); // unlock while we process an event, so that we don't // block // any queue insertions. queueLock.unlock(); reportActive(ent.tag); // extract target, then free EventOutput target = ent.target; ent.target = null; // avoid garbage linger pool.free(ent); // actually run the event try { target.event(); } catch (Throwable thr) { Logger.severe("Top-level failure in scheduled event", thr); } // back into the monitor loop reportActive(null); queueLock.lockInterruptibly(); } } } finally { if (queueLock.isHeldByCurrentThread()) { queueLock.unlock(); } } } catch (InterruptedException e) { // interrupted! } } // TODO: These MUST never block! RecordedRunLoop might do it for a small // amount of time while waiting for a queue to be unlocked, but that's it. // Really, even that should be avoided, but as long as it's just a delay, // it'll be okay from a correctness perspective. @FlowPhase protected void reportAwaiting(boolean isAwaiting) { // to be overridden as necessary } @FlowPhase protected void reportActive(String tag) { // to be overridden as necessary } @Override public void terminate() { terminated = true; this.interrupt(); } }