/* * org.openmicroscopy.shoola.util.concur.ProducerLoop * *------------------------------------------------------------------------------ * Copyright (C) 2006 University of Dundee. All rights reserved. * * * 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, Fifth Floor, Boston, MA 02110-1301 USA. * *------------------------------------------------------------------------------ */ package org.openmicroscopy.shoola.util.concur; //Java imports //Third-party libraries //Application-internal dependencies import org.openmicroscopy.shoola.util.concur.tasks.ExecMonitor; import org.openmicroscopy.shoola.util.concur.tasks.MultiStepTask; /** * The asynchronous producer loop of an {@link AsyncByteBuffer}. * Implements a {@link MultiStepTask} service to have the producer (an instance * of {@link ByteBufferFiller}, handed off by the {@link AsyncByteBuffer} at * creation time) fill up the {@link AsyncByteBuffer}'s internal buffer. Also * keeps track of the loop state so that the {@link AsyncByteBuffer} can query * it in order to find out whether a given data segment is available (that is, * it has been written to the buffer) and possibly wait until it becomes * available or the data is discarded — because of an exception thrown by * the producer or because the loop is cancelled by the {@link AsyncByteBuffer} * itself. * * @see org.openmicroscopy.shoola.util.concur.AsyncByteBuffer * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author <br>Andrea Falconi      * <a href="mailto:a.falconi@dundee.ac.uk"> * a.falconi@dundee.ac.uk</a> * @version 2.2 * <small> * (<b>Internal version:</b> $Revision$ $Date$) * </small> * @since OME2.2 */ class ProducerLoop implements MultiStepTask, ExecMonitor { /** * State flag to denote the <i>Filling</i> state. * In this state the producer thread runs the <code>doStep</code> loop to * fill up the {@link AsyncByteBuffer}'s internal buffer. This state is * characterized by the following constraints: * <code>bytesWritten < {@link #PAYLOAD}</code> and * <code>exc = null</code>. */ static final int FILLING = 0; /** * State flag to denote the <i>Done</i> state. * This state is reached when the producer thread has finished running the * <code>doStep</code> loop and {@link #PAYLOAD} bytes have been written to * the {@link AsyncByteBuffer}'s internal buffer. This state is * characterized by the following constraints: * <code>bytesWritten = {@link #PAYLOAD}</code> and * <code>exc = null</code>. */ static final int DONE = 1; /** * State flag to denote the <i>Data Discarded</i> state. * This state is reached if the producer thread exits abnormally the * <code>doStep</code> loop. This can happen because of an exception * thrown by the {@link #producer} object, the loop is cancelled, or * an amount of bytes different from {@link #PAYLOAD} have been written * to the {@link AsyncByteBuffer}'s internal buffer. This state is * characterized by the following constraint: <code>exc != null</code>. */ static final int DATA_DISCARDED = 2; /** * The object that actually writes bytes into the {@link AsyncByteBuffer}'s * internal buffer. */ private final ByteBufferFiller producer; /** * Link back to the {@link AsyncByteBuffer} this instance is working with. */ private final AsyncByteBuffer buffer; /** * Total amount of bytes that can ever be produced by the {@link #producer}. * This is the value returned by {@link ByteBufferFiller#getTotalLength()}, * which we grab only once at creation time so to avoid problems if a * buggy producer returns different values in different calls to the * <code>getTotalLength</code> method. */ private final int PAYLOAD; //State written by producer thread and queried by consumer threads. /** * The amount of bytes that have currently been written to the * {@link AsyncByteBuffer}'s internal buffer. This field is written * by the producer thread and indirectly queried by consumer threads * via the {@link AsyncByteBuffer}. */ private int bytesWritten; /** * Details why data was discarded if the loop transitions to the * {@link #DATA_DISCARDED} state. This field is written by the * producer thread and indirectly queried by consumer threads via * the {@link AsyncByteBuffer}. */ private BufferWriteException discardCause; /** * Keeps track of the loop state. This field is written by the * producer thread and indirectly queried by consumer threads via * the {@link AsyncByteBuffer}. */ private int state; /** * Tells whether {@link #doStep()} should be invoked. * Only used by the producer thread, it is initialized to <code>false</code> * and then latches to <code>true</code> when {@link #doStep()} has * written the last chunk of bytes. */ private boolean done; /** * Creates a new instance. * * @param buffer The {@link AsyncByteBuffer} object that this new * <code>ProducerLoop</code> has to work for. * Mustn't be <code>null</code>. * @param producer The object that will actually write to the buffer. * Mustn't be <code>null</code>. */ ProducerLoop(AsyncByteBuffer buffer, ByteBufferFiller producer) { //Links. if (buffer == null) throw new NullPointerException("No buffer."); if (producer == null) throw new NullPointerException("No producer."); this.buffer = buffer; this.producer = producer; //Grab the total lenght of the byte stream now to avoid problems //later if the producer is buggy. Don't bother to check that is //greater than 0 -- if not, doStep will fail t the first call. PAYLOAD = producer.getTotalLength(); if (PAYLOAD < 1) throw new IllegalArgumentException( "producer.getTotalLength() didn't return a positive value: "+ PAYLOAD+"."); //Initialize state (just for the sake of clarity). bytesWritten = 0; discardCause = null; done = false; //Set logical state. state = FILLING; } /** * Increases the {@link #bytesWritten} field by the amount of bytes written * by the last call to {@link #doStep()}. * Note that this method acquires this object's lock so to force a flush * of the working memory and make the new field value available to * consumer threads. It also notifies any consumer thread waiting for * a data segment. * * @param writeLength The amount of bytes lastly written by * {@link #doStep()}. */ private synchronized void updateBytesWritten(int writeLength) { if (flowObs != null) flowObs.update(LOCK_ACQUIRED); bytesWritten += writeLength; notifyAll(); //B/c there might be more than 1 consumer waiting. } /** * Verifies that <code>[offset, length]</code> is a valid interval and * falls within <code>[0, {@link #PAYLOAD}]</code>. Failing that, an * {@link IllegalArgumentException} is thrown. * * @param offset The start of the data segment. * @param length The length of the data segment. */ private void checkInterval(int offset, int length) { if (offset < 0 || length < 0 || PAYLOAD < offset+length) throw new IllegalArgumentException( "Illegal data segment: [offset="+offset+", offset+length="+ (offset+length)+"] not in [0, PAYLOAD="+PAYLOAD+"]."); } /** * Checks if the specified data segment is available and optionally waits * <code>timeout</code> milliseconds to evaluate said condition again if * the state is {@link #FILLING}. * If the state is {@link #DATA_DISCARDED}, then there's no point in * checking and {@link #discardCause} is thrown instead. If the state * is {@link #DONE}, then there's no point in waiting as no more bytes * will ever be written, and we just verify the above mentioned condition * — which should always evaluate to <code>true</code> in this case. * This method assumes the caller already owns the object's monitor — * that is, call this method only within a <code>synchronized</code> * block. Another precondition which is assumed to hold <code>true</code> * is that <code>[offset, length]</code> is a valid interval within * <code>[0, {@link #PAYLOAD}]</code>. * * @param offset The start of the data segment. * @param length The length of the data segment. * @param timeout The amount of milliseconds the calling thread should be * suspended if the data segment hasn't yet been retrieved * — possible if the state is {@link #FILLING}. * Pass <code>0</code> for an unbounded wait or <code>-1</code> * to not wait at all. * @return <code>true</code> if <code>[offset, length]</code> falls within * <code>[0, {@link #PAYLOAD}]</code>, <code>false</code> * otherwise. * @throws BufferWriteException If the data has been discarded. * @throws InterruptedException If the current thread has been interrupted * while waiting for the condition to become * <code>true</code>. */ private boolean isAvailable(int offset, int length, long timeout) throws BufferWriteException, InterruptedException { //We own the object's monitor and [off,len] in [0,PAYLOAD]. boolean available = (offset+length <= bytesWritten); switch (state) { case DATA_DISCARDED: throw discardCause; case DONE: //[off,len] in [0,PAYLOAD] and bytesWritten==PAYLOAD. return available; //So this must be true. case FILLING: if (available) return true; //Don't wait if already true. if (0 <= timeout) wait(timeout); //Wait for unbounded time if 0. //Else don't wait at all if timeout < 0. } //State is FILLING (see above switch). Re-evaluate condition. return (offset+length <= bytesWritten); } /** * Atomically checks if the specified data segment is available and * optionally waits <code>timeout</code> milliseconds for said condition * to be satisfied. * * @param offset The start of the data segment. * @param length The length of the data segment. * @param timeout The amount of milliseconds the calling thread should be * suspended if the data segment hasn't yet been retrieved. * If the passed value is not positive, then the caller is not * suspended at all. * @return <code>true</code> if <code>[offset, length]</code> falls within * <code>[0, {@link #PAYLOAD}]</code>, <code>false</code> * otherwise. * @throws BufferWriteException If the data has been discarded. * @throws InterruptedException If the current thread has been interrupted * while waiting for the condition to become * <code>true</code>. * @throws IllegalArgumentException If <code>[offset, length]</code> is not * a valid interval or doesn't fall within * <code>[0, {@link #PAYLOAD}]</code>. */ boolean waitForData(int offset, int length, long timeout) throws BufferWriteException, InterruptedException { //Don't acquire the lock if the current thread has been interrupted. //Pointless to have this thread compete to acquire the lock and then //possibly suspend when it should stop its activity instead. if (Thread.interrupted()) throw new InterruptedException(); checkInterval(offset, length); //Make sure [off,len] in [0,PAYLOAD]. //Get the lock. synchronized (this) { if (flowObs != null) flowObs.update(LOCK_ACQUIRED); if (isAvailable(offset, length, -1)) //Check without waiting. return true; if (timeout <= 0) //No wait. Data not available so return false. return false; //Release lock and suspend until allowed to proceed or timed-out. long start = System.currentTimeMillis(), delta = timeout; while (true) { if (isAvailable(offset, length, delta)) return true; delta = (start+timeout) - System.currentTimeMillis(); if (delta <= 0) return false; } } } /** * Atomically checks if the specified data segment is available and, if * not, waits for said condition to be satisfied. * * @param offset The start of the data segment. * @param length The length of the data segment. * @return <code>true</code>. * @throws BufferWriteException If the data has been discarded. * @throws InterruptedException If the current thread has been interrupted * while waiting for the condition to become * <code>true</code>. * @throws IllegalArgumentException If <code>[offset, length]</code> is not * a valid interval or doesn't fall within * <code>[0, {@link #PAYLOAD}]</code>. */ boolean waitForData(int offset, int length) throws BufferWriteException, InterruptedException { //Don't acquire the lock if the current thread has been interrupted. //Pointless to have this thread compete to acquire the lock and then //possibly suspend when it should stop its activity instead. if (Thread.interrupted()) throw new InterruptedException(); checkInterval(offset, length); //Make sure [off,len] in [0,PAYLOAD]. //Get the lock, but release it and suspend if not allowed to proceed. synchronized (this) { if (flowObs != null) flowObs.update(LOCK_ACQUIRED); //Wait until data is available or aborted/cancelled. while (!isAvailable(offset, length, 0)) ; } return true; } /** * Run in the producer thread to fill up the {@link AsyncByteBuffer}'s * internal buffer. Each call writes a chunk of bytes up to a value * specified by {@link AsyncByteBuffer} and then updates the * {@link #bytesWritten} field. If more than {@link #PAYLOAD} bytes * have been written, then a {@link BufferWriteException} is thrown. * * @see MultiStepTask#doStep() */ public Object doStep() throws Exception { if (PAYLOAD < bytesWritten) throw new BufferWriteException("Overflow: PAYLOAD="+PAYLOAD+ " shouldn't be exceeded ("+bytesWritten+ " bytes written so far)."); int writeLength = buffer.writeToBuffer(producer, bytesWritten); if (writeLength != -1) updateBytesWritten(writeLength); else done = true; return null; //Result written directly into buffer. } /** * Implemented as specified by the {@link MultiStepTask} interface. * @see MultiStepTask#isDone() */ public boolean isDone() { return done; } /** * No-op implementation. * Required by the {@link ExecMonitor} interface but not actually needed. * @see ExecMonitor#onStart() */ public void onStart() {} /** * No-op implementation. * Required by the {@link ExecMonitor} interface but not actually needed. * Replaced by {@link #updateBytesWritten(int)}. * @see ExecMonitor#update(int) */ public void update(int step) {} /** * Just calls {@link #onAbort(Throwable)} with a * {@link BufferWriteException} argument to specifiy that the * producer loop was cancelled. * @see ExecMonitor#onCancel() */ public void onCancel() { onAbort(new BufferWriteException("Data retrieval cancelled.")); /* NOTE 1: onAbort and onCancel are mutually exclusive, either one * is invoked by the service execution workflow. * NOTE 2: PAYLOAD bytes could have been written to the buffer if * cancellation is detected after the last call to doStep(). */ } /** * Transitions this object to the {@link #DATA_DISCARDED} state. * Note that this method acquires this object's lock so to force a flush * of the working memory and make the exception available to consumer * threads. It also notifies any consumer thread waiting for a data * segment. * * @param cause Details why the data retrieval was aborted. If not an * instance of {@link BufferWriteException}, then it is * wrapped with an instance of said exception. * @see ExecMonitor#onAbort(Throwable) */ public synchronized void onAbort(Throwable cause) { if (flowObs != null) flowObs.update(LOCK_ACQUIRED); if (cause instanceof BufferWriteException) discardCause = (BufferWriteException) cause; else //Can only be RuntimeException. discardCause = new BufferWriteException( "Unexpected runtime exception.", cause); state = DATA_DISCARDED; notifyAll(); //B/c there might be more than 1 consumer waiting. } /** * Transitions this object to the {@link #DONE} or {@link #DATA_DISCARDED} * state. If {@link #PAYLOAD} equals {@link #bytesWritten} then we go to * the {@link #DONE} state, otherwise to the {@link #DATA_DISCARDED} state. * Note that this method acquires this object's lock so to force a flush * of the working memory and make the new state available to consumer * threads. It also notifies any consumer thread waiting for a data * segment. * @see ExecMonitor#onEnd(Object) */ public synchronized void onEnd(Object result) { if (flowObs != null) flowObs.update(LOCK_ACQUIRED); if (bytesWritten == PAYLOAD) state = DONE; else { //bytesWritten < PAYLOAD. In fact, if we're here then doStep must have //set done to true. This impies PAYLOAD <= bytesWritten (otherwise an //exception would have been thrown and onAbort called instead). String msg = "Underflow: PAYLOAD="+PAYLOAD+" hasn't been reached "+ "("+bytesWritten+" bytes written in total)."; discardCause = new BufferWriteException(msg); state = DATA_DISCARDED; } notifyAll(); //B/c there might be more than 1 consumer waiting. } /* * ============================================================== * Follows code to enable testing. * ============================================================== */ static final int LOCK_ACQUIRED = 100; private ControlFlowObserver flowObs; void register(ControlFlowObserver obs) { flowObs = obs; } int getPayload() { return PAYLOAD; } synchronized int getState() { return state; } synchronized int getBytesWritten() { return bytesWritten; } synchronized BufferWriteException getDiscardCause() { return discardCause; } }