// This software is released into the Public Domain. See copying.txt for details. package org.openstreetmap.osmosis.core.store; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.openstreetmap.osmosis.core.OsmosisRuntimeException; import org.openstreetmap.osmosis.core.task.v0_6.Initializable; /** * <p> * This class provides a mechanism for a thread to pass data to another thread. * Both threads will block until the other is ready. It supports a single * writing thread, and a single reading thread. Multiple reading or writing * threads are NOT supported. * <p> * The input thread must call methods in the following sequence: * </p> * <ul> * <li>initialize - Called during successful startup, can be skipped in failure * condition</li> * <li>put - Called from zero to N times until input data is exhausted</li> * <li>complete - Only called if input processing completed successfully</li> * <li>release - Called once at the end of processing regardless of success or * failure</li> * </ul> * <p> * The output thread must call methods in the following sequence: * </p> * <ul> * <li>outputInitialize - Called during successful startup, can be skipped in * failure condition</li> * <li>hasNext/getNext - Both called until hasNext returns false in which case * no more data is available</li> * <li>outputComplete - Only called if output processing completed successfully</li> * <li>outputRelease - Called once at the end of processing regardless of * success or failure</li> * </ul> * <p> * The input thread will block in the following situations: * </p> * <ul> * <li>initialize has been called, but outputInitialize has not yet been called</li> * <li>put has been called, and the buffer is full</li> * <li>The complete method has been called, and outputComplete has not yet been * called</li> * <li>The release method has been called, and outputRelease has not yet been * called. This wait must occur to support the scenario where both threads * subsequently wish to initialize again.</li> * </ul> * <p> * The output thread will block in the following situations: * </p> * <ul> * <li>The outputInitialize method has been called, and initialize has not yet * been called</li> * <li>hasNext has been called, the buffer is empty, and complete has not yet * been called</li> * <li>The outputComplete method has been called, but complete has not yet been * called (Should never happen because hasNext won't return false until complete * has been called).</li> * <li>The outputRelease method has been called, but release has not yet been * called.</li> * </ul> * <p> * This class may be re-used multiple times. For this to work, both input and * output methods must be called an equal number of times or deadlock will * occur. Re-use may occur after input or output threads fail, however in all * cases calls to release and outputRelease must be matched. * * @param <T> * The type of data held in the postbox. */ public class DataPostbox<T> implements Initializable { private int bufferCapacity; private int chunkSize; private Lock lock; private Condition dataWaitCondition; private Map<String, Object> processingMetaData; private Collection<T> centralQueue; private Collection<T> inboundQueue; private Queue<T> outboundQueue; private boolean inputInitialized; private boolean outputInitialized; private boolean inputComplete; private boolean outputComplete; private boolean inputReleased; private boolean outputReleased; private boolean inputExit; private boolean outputExit; private boolean inputOkay; private boolean outputOkay; /** * Creates a new instance. * * @param capacity * The maximum number of objects to hold in the postbox before * blocking. */ public DataPostbox(int capacity) { if (capacity <= 0) { throw new OsmosisRuntimeException("A capacity of " + capacity + " is invalid, must be greater than 0."); } this.bufferCapacity = capacity; // Use a chunk size one quarter of total buffer size. This is a magic // number but performance isn't highly sensitive to this parameter. chunkSize = bufferCapacity / 4; if (chunkSize <= 0) { chunkSize = 1; } // Create the thread synchronisation primitives. lock = new ReentrantLock(); dataWaitCondition = lock.newCondition(); // Thread synchronisation flags. Each thread moves in lockstep through // each of these phases. Only initialize and or complete flags may be // skipped which trigger error conditions and the setting of the okay // flags. inputInitialized = false; outputInitialized = false; inputComplete = false; outputComplete = false; inputReleased = false; outputReleased = false; inputExit = true; outputExit = true; inputOkay = true; outputOkay = true; // Create the inter-thread data transfer queues. initializeQueues(); } private void initializeQueues() { // Create buffer objects. centralQueue = new ArrayList<T>(); inboundQueue = new ArrayList<T>(); outboundQueue = new ArrayDeque<T>(); } /** * This is called by the input thread to validate that no errors have * occurred on the output thread. */ private void checkForOutputErrors() { // Check for reading thread error. if (!outputOkay) { throw new OsmosisRuntimeException("An output error has occurred, aborting."); } } /** * This is called by the output thread to validate that no errors have * occurred on the input thread. */ private void checkForInputErrors() { // Check for writing thread error. if (!inputOkay) { throw new OsmosisRuntimeException("An input error has occurred, aborting."); } } /** * Either thread can call this method when they wish to wait until an update * has been performed by the other thread. */ private void waitForUpdate() { try { dataWaitCondition.await(); } catch (InterruptedException e) { throw new OsmosisRuntimeException("Thread was interrupted.", e); } } /** * Either thread can call this method when they wish to signal the other * thread that an update has occurred. */ private void signalUpdate() { dataWaitCondition.signal(); } /** * Adds a group of objects to the central queue ready for consumption by the * receiver. * * @param o * The objects to be added. */ private void populateCentralQueue() { lock.lock(); try { checkForOutputErrors(); // Wait until the currently posted data is cleared. while (centralQueue.size() >= bufferCapacity) { waitForUpdate(); checkForOutputErrors(); } // Post the new data. centralQueue.addAll(inboundQueue); inboundQueue.clear(); signalUpdate(); } finally { lock.unlock(); } } /** * Empties the contents of the central queue into the outbound queue. */ private void consumeCentralQueue() { lock.lock(); try { checkForInputErrors(); // Wait until data is available. while (!((centralQueue.size() > 0) || inputComplete)) { waitForUpdate(); checkForInputErrors(); } outboundQueue.addAll(centralQueue); centralQueue.clear(); signalUpdate(); } finally { lock.unlock(); } } /** * {@inheritDoc} */ @Override public void initialize(Map<String, Object> metaData) { if (inputInitialized) { throw new OsmosisRuntimeException("initialize has already been called"); } lock.lock(); try { checkForOutputErrors(); // Set the processing metadata, and flag that we have initialized. processingMetaData = metaData; inputInitialized = true; signalUpdate(); // Now we must wait until the output thread initializes or // encounters an error. while (!outputInitialized) { waitForUpdate(); checkForOutputErrors(); } } finally { lock.unlock(); } } /** * Adds a new object to the postbox. * * @param o * The object to be added. */ public void put(T o) { if (!inputInitialized) { throw new OsmosisRuntimeException("initialize has not been called"); } inboundQueue.add(o); if (inboundQueue.size() >= chunkSize) { populateCentralQueue(); } } /** * {@inheritDoc} */ @Override public void complete() { if (!inputInitialized) { throw new OsmosisRuntimeException("initialize has not been called"); } lock.lock(); try { populateCentralQueue(); inputComplete = true; signalUpdate(); // Now we must wait until the output thread completes or // encounters an error. while (!outputComplete) { waitForUpdate(); checkForOutputErrors(); } } finally { lock.unlock(); } } /** * This method conforms to the * {@link org.openstreetmap.osmosis.core.lifecycle.Closeable} contract, * however there are limitations around calling it multiple times. Each call * to this method must be matched by a call to the outputRelease method in a * separate thread or deadlock will occur. */ @Override public void close() { lock.lock(); try { // If release is being called without having completed successfully, // it is an error condition. if (!inputComplete) { inputOkay = false; } inputReleased = true; inputExit = false; signalUpdate(); // Wait until the output thread releases. while (!outputReleased) { waitForUpdate(); } // At this point both threads have reached a release state so we can // reset our state. initializeQueues(); inputInitialized = false; inputComplete = false; inputReleased = false; inputExit = true; inputOkay = true; signalUpdate(); // Wait for the output thread to exit. while (!outputExit) { waitForUpdate(); } } finally { lock.unlock(); } } /** * Notifies that the output thread has begun processing, and gets the * initialization data set by the input thread. This will block until either * the input thread has called initialize, or an input error occurs. * * @return The initialization data. */ public Map<String, Object> outputInitialize() { if (outputInitialized) { throw new OsmosisRuntimeException("outputInitialize has already been called"); } lock.lock(); try { checkForInputErrors(); // We must wait until the input thread initializes or // encounters an error. while (!inputInitialized) { waitForUpdate(); checkForInputErrors(); } outputInitialized = true; signalUpdate(); return processingMetaData; } finally { lock.unlock(); } } /** * Indicates if data is available for output. This will block until either * data is available, input processing has completed, or an input error * occurs. * * @return True if data is available. */ public boolean hasNext() { int queueSize; if (!outputInitialized) { throw new OsmosisRuntimeException("outputInitialize has not been called"); } queueSize = outboundQueue.size(); if (queueSize <= 0) { consumeCentralQueue(); queueSize = outboundQueue.size(); } return queueSize > 0; } /** * Returns the next available object from the postbox. This should be * preceeded by a call to hasNext. * * @return The next available object. */ public T getNext() { if (hasNext()) { T result; result = outboundQueue.remove(); return result; } else { throw new OsmosisRuntimeException("No data is available, should call hasNext first."); } } /** * Notifies that the output thread has completed processing. This will block * until either the input thread has called complete, or an input error * occurs. */ public void outputComplete() { if (!outputInitialized) { throw new OsmosisRuntimeException("outputInitialize has not been called"); } lock.lock(); try { checkForInputErrors(); // We must wait until the input thread completes or encounters an // error. while (!inputComplete) { waitForUpdate(); checkForInputErrors(); } outputComplete = true; signalUpdate(); } finally { lock.unlock(); } } /** * Notifies that the output thread has released. This will block until the * input thread has also released and the object has been reset. */ public void outputRelease() { lock.lock(); try { // If release is being called without having completed successfully, // it is an error condition. if (!outputComplete) { outputOkay = false; signalUpdate(); } // Wait until the input thread is released. while (!inputReleased) { waitForUpdate(); } // At this point both threads have reached a release state so we can // set out state as released but waiting for exit. outputInitialized = false; outputComplete = false; outputReleased = true; outputExit = false; outputOkay = true; signalUpdate(); // Wait until the input thread has reached the exit point. while (!inputExit) { waitForUpdate(); } // The input thread has reached exit, so now we can clear the // release flag (required so that subsequent iterations if they // exist must go through the same handshake sequence) and flag that // we've reached exit. outputReleased = false; outputExit = true; signalUpdate(); } finally { lock.unlock(); } } }