/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.calc.runner;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.opengamma.strata.basics.CalculationTarget;
import com.opengamma.strata.calc.Column;
import com.opengamma.strata.collect.ArgChecker;
/**
* Wrapper around a listener for thread-safety.
* <p>
* This is a wrapper around a {@link CalculationListener} that ensures the listener
* is only invoked by a single thread at a time. When the calculations are complete,
* it calls {@link CalculationListener#calculationsComplete() calculationsComplete}.
* <p>
* Calculations may be performed in bulk for a given target.
* The logic in this class unwraps the {@link CalculationResults}, calling the
* listener with each individual {@link CalculationResult}.
*/
final class ListenerWrapper implements Consumer<CalculationResults> {
private static final Logger log = LoggerFactory.getLogger(ListenerWrapper.class);
/** The wrapped listener. */
private final CalculationListener listener;
/** Queue of actions to perform on the delegate. */
private final Queue<CalculationResults> queue = new LinkedList<>();
/** Protects the queue and the executing flag. */
private final Lock lock = new ReentrantLock();
/** This lock is never contended; it is used to guarantee the listener state is visible to all threads. */
private final Lock listenerLock = new ReentrantLock();
/** The total number of tasks to be executed. */
private final int tasksExpected;
// Mutable state -----------------------------------------------------
/**
* Flags whether a call to the underlying listener is executing.
* If this flag is set when {@link #accept} is called, the result is added to
* the queue and the calling thread returns. The executing thread will ensure
* all queued results are delivered.
*/
private boolean executing;
/** The number of task results that have been received. */
private int tasksReceived;
//-------------------------------------------------------------------------
/**
* Creates an instance wrapping the specified listener.
* @param listener the underlying listener wrapped by this object
* @param tasksExpected the number of tasks to be executed
* @param columns the columns for which values are being calculated
*/
ListenerWrapper(CalculationListener listener, int tasksExpected, List<CalculationTarget> targets, List<Column> columns) {
this.listener = ArgChecker.notNull(listener, "listener");
this.tasksExpected = ArgChecker.notNegative(tasksExpected, "tasksExpected");
listenerLock.lock();
try {
listener.calculationsStarted(targets, columns);
if (tasksExpected == 0) {
listener.calculationsComplete();
}
} finally {
listenerLock.unlock();
}
}
//-------------------------------------------------------------------------
/**
* Accepts a calculation result and delivers it to the listener
* <p>
* This method can be invoked concurrently by multiple threads.
* Only one of them will invoke the listener directly to ensure that
* it is not accessed concurrently by multiple threads.
* <p>
* The other threads do not block while the listener is invoked. They
* add their results to a queue and return quickly. Their results are
* delivered by the thread invoking the listener.
*
* @param result the result of a calculation
*/
@Override
public void accept(CalculationResults result) {
CalculationResults nextResult;
// Multiple calculation threads can try to acquire this lock at the same time.
// The thread which acquires the lock will set the executing flag and proceed into
// the body of the method.
// If another thread acquires the lock while the first thread is executing it will
// add an item to the queue and return.
// The lock also ensures the state of the executing flag and the queue are visible
// to any thread acquiring the lock.
lock.lock();
try {
if (executing) {
// Another thread is already invoking the listener. Add the result to
// the queue and return. The other thread will ensure the queued results
// are delivered.
queue.add(result);
return;
} else {
// There is no thread invoking the listener. Set the executing flag to
// ensure no other thread passes this point and invoke the listener.
executing = true;
nextResult = result;
}
} finally {
lock.unlock();
}
// The logic in the block above guarantees that there will never be more than one thread in the
// rest of the method below this point.
// Loop until the nextResult and all the results from the queue have been delivered
for (;;) {
// The logic above means this lock is never contended; the executing flag means
// only one thread will ever be in this loop at any given time.
// This lock is required to ensure any state changes in the listener are visible to all threads
listenerLock.lock();
try {
// Invoke the listener while not protected by lock. This allows other threads
// to queue results while this thread is delivering them to the listener.
for (CalculationResult cell : nextResult.getCells()) {
listener.resultReceived(nextResult.getTarget(), cell);
}
} catch (RuntimeException e) {
log.warn("Exception invoking listener.resultReceived", e);
} finally {
listenerLock.unlock();
}
// The following code must be executed whilst holding the lock to guarantee any changes
// to the executing flag and to the state of the queue are visible to all threads
lock.lock();
try {
if (++tasksReceived == tasksExpected) {
// The expected number of results have been received, inform the listener.
// The listener lock must be acquired to ensure any state changes in the listener are
// visible to all threads
listenerLock.lock();
try {
listener.calculationsComplete();
} catch (RuntimeException e) {
log.warn("Exception invoking listener.calculationsComplete", e);
} finally {
listenerLock.unlock();
}
return;
} else if (queue.isEmpty()) {
// There are no more results to deliver. Unset the executing flag and return.
// This allows the next calling thread to deliver results.
executing = false;
return;
} else {
// There are results on the queue. This means another thread called accept(),
// added a result to the queue and returned while this thread was invoking the listener.
// This thread must deliver the results from the queue.
nextResult = queue.remove();
}
} finally {
lock.unlock();
}
}
}
}