// This file is part of PleoCommand: // Interactively control Pleo with psychobiological parameters // // Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Boston, USA. package pleocmd.pipe.data; import java.io.IOException; import java.util.ArrayList; import java.util.List; import pleocmd.Log; import pleocmd.StandardInput; import pleocmd.exc.FormatException; import pleocmd.exc.InternalException; import pleocmd.pipe.PipePart; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; /** * Provides a FiFo implemented as a ring-buffer which passes {@link Data} from * {@link Input} / {@link Converter} thread to the {@link Output} thread in a * thread-safe manner with priority support. * * @author oliver * @see StandardInput */ public final class DataQueue { /** * Will be used if the queue is empty and there is a {@link #get()} waiting * to indicate that the queue is currently accepting everything without any * side effects. */ private static final int PRIO_UNDEFINED = Byte.MAX_VALUE; /** * Initial ringbuffer size after {@link #resetCache()}. */ private static final int RB_DEFAULT = 16; /** * This array represents a ring buffer. */ private Data[] buffer; /** * The position of the next byte to read from {@link #buffer}. */ private int readPos; /** * The position of the next byte to write to {@link #buffer}. */ private int writePos; /** * The priority of all the {@link Data}s in the queue.<br> * This is {@link #PRIO_UNDEFINED} if the queue is empty and there is a * {@link #get()} waiting or there never was any {@link #get()} (since the * last {@link #resetCache()}).<br> * So it's defined for an empty queue only if the last {@link #get()} is * still being processed by the Output-Thread. */ private byte priority; /** * Only true if the cache has been closed, i.e. the remaining data in * {@link #buffer} can still be read, but no new data can be put into the * {@link #buffer} and if {@link #readPos} catches up {@link #writePos} * {@link #get()} throws an {@link IOException}. */ private boolean closed; private int sizeBeforeClear; /** * Creates a new, empty and opened {@link DataQueue}. */ public DataQueue() { resetCache(); } /** * Appends a "close" to the ring buffer.<br> * The remaining Data in the ring buffer can still be {@link #get()} but no * new data can be {@link #put(Data)} into it. After no more data is * available {@link #get()} throws an {@link IOException}.<br> * Has no effect if the {@link DataQueue} is already closed. */ public synchronized void close() { Log.detail("Sending close to ring-buffer '%s'", this); closed = true; notify(); } /** * Clears and (if currently closed) reopens the queue.<br> * All data in the ring buffer not yet read will be lost. */ public synchronized void resetCache() { Log.detail("Resetting ring-buffer '%s'", this); buffer = new Data[RB_DEFAULT]; readPos = 0; writePos = 0; closed = false; priority = PRIO_UNDEFINED; sizeBeforeClear = 0; Log.detail("Reset ring-buffer '%s'", this); notify(); } /** * Reads one {@link Data} from the ring buffer.<br> * Blocks until the {@link Data} is available.<br> * Should only be called from the Output thread. * * @return the next {@link Data} or <b>null</b> if no more {@link Data} is * available and the {@link DataQueue} has been {@link #close()}d * @throws InterruptedException * if waiting for the next data block has been interrupted */ public Data get() throws InterruptedException { Log.detail("Trying to read in '%s'", this); boolean first = true; synchronized (this) { while (true) { // check if read catches up write? if (readPos != writePos) break; if (first) { // queue empty and waiting in get(), so: priority = PRIO_UNDEFINED; Log.detail("Queue empty and waiting => " + "undefined priority in '%s'", this); } // if queue closed, we return null to signal end of pipe if (closed) return null; first = false; // block until data available wait(); } final Data res = buffer[readPos]; Log.detail("Read from %03d '%s' in '%s'", readPos, res, this); readPos = (readPos + 1) % buffer.length; return res; } } /** * The return value of {@link DataQueue#put(Data)}. * * @author oliver */ public enum PutResult { /** * The {@link Data} has been put into the queue. */ Put, /** * The queue has been cleared because the new {@link Data} has a higher * priority as the current {@link Data}s in the queue. */ ClearedAndPut, /** * The new {@link Data} has silently been dropped. */ Dropped } /** * Puts one {@link Data} into the ringbuffer, so it can be read by * {@link #get()}.<br> * If {@link Data}'s priority is lower than the one of the current elements * in the queue, the new {@link Data} will silently be dropped. <br> * If {@link Data}'s priority is higher than the one of the current elements * in the queue, the queue is cleared before inserting the new {@link Data}. <br> * Should only be called from the Input/Converter thread. * * @param data * data to put into the ring buffer * @return a {@link PutResult} * @throws IOException * if the {@link DataQueue} has been {@link #close()}d. */ public synchronized PutResult put(final Data data) throws IOException { if (closed) throw new IOException("DataQueue is closed"); boolean hasBeenCleared = false; if (priority != PRIO_UNDEFINED && data.getPriority() < priority) { // silently drop the new Data Log.detail("Dropped '%s' in '%s'", data, this); return PutResult.Dropped; } if (priority != PRIO_UNDEFINED && data.getPriority() > priority) { // fast-clearing of the queue sizeBeforeClear = (writePos - readPos) % buffer.length; if (sizeBeforeClear < 0) sizeBeforeClear += buffer.length; int i = readPos; while (i != writePos) { final PipePart org = buffer[i].getOrigin(); if (org != null) org.getFeedback().incDropCount(); i = (i + 1) % buffer.length; } readPos = writePos; Log.detail("Cleared '%s' because of '%s'", this, data); hasBeenCleared = true; } buffer[writePos] = data; priority = data.getPriority(); Log.detail("Put at %03d '%s' in '%s'", writePos, data, this); writePos = (writePos + 1) % buffer.length; if (writePos == readPos) { // we need to increase our ring buffer: // we "insert space" between the current write and // read position so writePos stays the same while // readPos moves. final Data[] newbuf = new Data[buffer.length * 2]; readPos += newbuf.length - buffer.length; System.arraycopy(buffer, 0, newbuf, 0, writePos); System.arraycopy(buffer, writePos, newbuf, readPos, buffer.length - writePos); buffer = newbuf; Log.detail("Increased buffer in '%s'", this); } notify(); return hasBeenCleared ? PutResult.ClearedAndPut : PutResult.Put; } /** * @return the number of {@link Data}s which were in the queue immediately * before the queue has been cleared due to a high-priority * {@link Data} in {@link #put(Data)}.<br> * Is <b>0></b> if the queue has never been cleared since the last * {@link #resetCache()}. */ public int getSizeBeforeClear() { return sizeBeforeClear; } @Override public String toString() { return String.format("cap: %d, read: %d, write: %d, lastPrio: %d", buffer == null ? -1 : buffer.length, readPos, writePos, priority); } public synchronized List<Data> getAll() { final List<Data> res = new ArrayList<Data>(); int i = readPos; while (i != writePos) i = (i + 1) % buffer.length; if (closed) try { res.add(Data.createFromAscii("QUEUE CLOSED")); } catch (final IOException e) { throw new InternalException(e); } catch (final FormatException e) { throw new InternalException(e); } return res; } }