// 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();
}
}
}