/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.engine.depgraph;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.opengamma.engine.value.ValueRequirement;
import com.opengamma.engine.value.ValueSpecification;
/* package */abstract class AbstractResolvedValueProducer implements ResolvedValueProducer, ResolvedValueProducer.Chain {
private static final Logger s_logger = LoggerFactory.getLogger(AbstractResolvedValueProducer.class);
private static final AtomicInteger s_nextObjectId = new AtomicInteger();
private final class Callback implements ResolutionPump, Cancelable {
private final int _objectId = s_nextObjectId.getAndIncrement();
private final ResolvedValueCallback _callback;
private ResolvedValue[] _results;
private int _resultsPushed;
public Callback(final ResolvedValueCallback callback) {
_callback = callback;
// Don't reference count the callback; the interface doesn't support it and if it is something like an
// AggregateResolvedValueProducer then counting references in this direction will introduce a loop and
// prevent dead tasks from being identified
}
@Override
public void pump(final GraphBuildingContext context) {
s_logger.debug("Pump called on {}", this);
ResolvedValue nextValue = null;
boolean finished = false;
ResolutionFailure failure = null;
boolean needsOuterPump = false;
synchronized (this) {
if (_resultsPushed < _results.length - 1) {
nextValue = _results[_resultsPushed++];
}
}
if (nextValue == null) {
synchronized (AbstractResolvedValueProducer.this) {
synchronized (this) {
_results = AbstractResolvedValueProducer.this._results;
if (_resultsPushed < _results.length) {
nextValue = _results[_resultsPushed++];
if (_finished && (_resultsPushed == _results.length)) {
finished = true;
}
} else {
if (_finished) {
finished = true;
failure = _failure;
} else {
if (_pumped != null) {
needsOuterPump = _pumped.isEmpty();
if (s_logger.isDebugEnabled()) {
if (needsOuterPump) {
s_logger.debug("Pumping outer object");
} else {
s_logger.debug("Adding to pump set");
}
}
if (needsOuterPump) {
_pumping = 1;
}
_pumped.add(this);
} else {
finished = true;
failure = _failure;
}
}
}
}
}
}
if (nextValue != null) {
if (finished) {
s_logger.debug("Publishing final value {}", nextValue);
release(context); // the reference added by addCallback
context.resolved(_callback, getValueRequirement(), nextValue, null);
} else {
s_logger.debug("Publishing value {}", nextValue);
context.resolved(_callback, getValueRequirement(), nextValue, this);
}
} else {
if (needsOuterPump) {
pumpImpl(context);
boolean lastResult = false;
Callback[] pumped = null;
int numPumped;
synchronized (AbstractResolvedValueProducer.this) {
switch (_pumping) {
case 1:
// No deferred results
break;
case 3:
// Next result ready; need to notify subscribers
numPumped = _pumped.size();
if (numPumped > 0) {
pumped = _pumped.toArray(new Callback[numPumped]);
_pumped.clear();
}
break;
case 7:
// Last result ready; need to notify subscribers
numPumped = _pumped.size();
if (numPumped > 0) {
pumped = _pumped.toArray(new Callback[numPumped]);
_pumped.clear();
}
_pumped = null;
lastResult = true;
break;
default:
throw new IllegalStateException("pumping = " + _pumping);
}
_pumping = 0;
}
pumpCallbacks(context, pumped, lastResult);
if (lastResult) {
finished(context);
}
} else {
if (finished) {
s_logger.debug("Finished {}", getValueRequirement());
release(context); // the reference added by addCallback
context.failed(_callback, getValueRequirement(), failure);
}
}
}
}
private boolean notPumpedState() {
synchronized (AbstractResolvedValueProducer.this) {
// Shouldn't be in the pumped state - a caller can't call close after calling pump
return _pumped == null || !_pumped.remove(this);
}
}
@Override
public void close(final GraphBuildingContext context) {
s_logger.debug("Closing callback {}", this);
assert notPumpedState();
release(context); // the reference added by addCallback
}
@Override
public boolean cancel(final GraphBuildingContext context) {
s_logger.debug("Cancelling callback {}", this);
synchronized (AbstractResolvedValueProducer.this) {
if ((_pumped != null) && _pumped.remove(this)) {
// Was in a pumped state; close and release the parent resolver
} else {
// Not in a pumped state - perhaps cancel was already called
return false;
}
}
release(context); // the reference added by addCallback
return true;
}
@Override
public String toString() {
return "Callback" + _objectId + "[" + _callback + ", " + AbstractResolvedValueProducer.this.toString() + "]";
}
@Override
public int hashCode() {
return _objectId;
}
}
private static final ResolvedValue[] NO_RESULTS = new ResolvedValue[0];
private final ValueRequirement _valueRequirement;
private final int _objectId = s_nextObjectId.getAndIncrement();
//private final InstanceCount _instanceCount = new InstanceCount(this);
private Set<ValueSpecification> _resolvedValues;
private Set<Callback> _pumped = new HashSet<Callback>();
private int _pumping; // 0 = not, 1 = pumpImpl about to be called, 3 = next result ready, 7 = last result ready
private int _refCount;
private ResolvedValue[] _results;
private volatile boolean _finished;
private ResolutionFailure _failure;
private boolean _failureCopied;
public AbstractResolvedValueProducer(final ValueRequirement valueRequirement) {
_valueRequirement = valueRequirement;
_results = NO_RESULTS;
_refCount = 1;
}
@Override
public Cancelable addCallback(final GraphBuildingContext context, final ResolvedValueCallback valueCallback) {
addRef(); // Caller already has open reference, this is for the reference held by the callback object
final Callback callback = new Callback(valueCallback);
ResolvedValue firstResult = null;
boolean finished = false;
ResolutionFailure failure = null;
synchronized (this) {
s_logger.debug("Added callback {} to {}", valueCallback, this);
callback._results = _results;
if (_results.length > 0) {
callback._resultsPushed = 1;
firstResult = _results[0];
if (_finished && _results.length == 1) {
finished = true;
}
} else {
if (_finished) {
finished = true;
failure = _failure;
} else {
_pumped.add(callback);
}
}
}
if (firstResult != null) {
if (finished) {
s_logger.debug("Pushing single callback result {}", firstResult);
release(context); // reference held by callback object
context.resolved(valueCallback, getValueRequirement(), firstResult, null);
return null;
} else {
s_logger.debug("Pushing first callback result {}", firstResult);
context.resolved(valueCallback, getValueRequirement(), firstResult, callback);
return callback;
}
} else if (finished) {
s_logger.debug("Pushing failure");
release(context); // reference held by callback object
context.failed(valueCallback, getValueRequirement(), failure);
return null;
} else {
return callback;
}
}
private void pumpCallbacks(final GraphBuildingContext context, final Callback[] pumped, final boolean lastResult) {
if (pumped != null) {
final boolean finished;
final ResolvedValue[] results;
final ResolutionFailure failure;
synchronized (this) {
finished = _finished;
results = _results;
failure = _failure;
}
for (Callback callback : pumped) {
ResolvedValue pumpValue = null;
boolean lastCallbackResult = false;
synchronized (callback) {
if (callback._resultsPushed < results.length) {
pumpValue = results[callback._resultsPushed++];
lastCallbackResult = lastResult && (callback._resultsPushed == results.length);
} else {
assert finished;
}
}
if (pumpValue != null) {
if (lastCallbackResult) {
s_logger.debug("Pushing last result to {}", callback._callback);
release(context); // reference held by the callback object
context.resolved(callback._callback, getValueRequirement(), pumpValue, null);
} else {
s_logger.debug("Pushing result to {}", callback._callback);
context.resolved(callback._callback, getValueRequirement(), pumpValue, callback);
}
} else {
s_logger.debug("Pushing failure to {}", callback._callback);
release(context); // the reference added by addCallback
context.failed(callback._callback, getValueRequirement(), failure);
}
}
}
}
private static boolean equals(final ValueSpecification a, final ValueSpecification b) {
// The ValueSpecifications put into ResolvedValue objects are normalized
assert (a == b) == a.equals(b);
return a == b;
}
/**
* Stores the result in the producer, publishing to any active subscribers that are currently waiting for a value.
*
* @param context the graph building context
* @param value the value to store
* @param lastResult last result indicator - if this is definitely the last result, the subscribers won't receive a "pump" handle allowing possible garbage collection of this producer
* @return true if a value was pushed to the subscribers, false if no value was generated and pushResult or finished must still be called
*/
protected boolean pushResult(final GraphBuildingContext context, final ResolvedValue value, final boolean lastResult) {
assert value != null;
assert !_finished;
if (!getValueRequirement().getConstraints().isSatisfiedBy(value.getValueSpecification().getProperties())) {
// Happens when a task was selected early on for satisfying the requirement as part of
// an aggregation feed. Late resolution then produces different specifications which
// would have caused in-line failures, but the delegating subscriber will receive them
// as-is. This would be bad.
s_logger.debug("Rejecting {} not satisfying {}", value, this);
return false;
}
Callback[] pumped = null;
synchronized (this) {
if (_results == null) {
// Already discarded -- accept the value silently
return true;
}
if (_resolvedValues != null) {
if (!_resolvedValues.add(value.getValueSpecification())) {
s_logger.debug("Rejecting {} already available from {}", value, this);
return false;
}
} else {
final ValueSpecification valueSpec;
switch (_results.length) {
case 0:
// No results yet
break;
case 1:
valueSpec = value.getValueSpecification();
if (equals(valueSpec, _results[0].getValueSpecification())) {
s_logger.debug("Rejecting {} already available from {}", value, this);
return false;
}
break;
case 2:
valueSpec = value.getValueSpecification();
if (equals(valueSpec, _results[0].getValueSpecification())
|| equals(valueSpec, _results[1].getValueSpecification())) {
s_logger.debug("Rejecting {} already available from {}", value, this);
return false;
}
break;
case 3:
valueSpec = value.getValueSpecification();
if (equals(valueSpec, _results[0].getValueSpecification())
|| equals(valueSpec, _results[1].getValueSpecification())
|| equals(valueSpec, _results[2].getValueSpecification())) {
s_logger.debug("Rejecting {} already available from {}", value, this);
return false;
}
_resolvedValues = new HashSet<ValueSpecification>(8);
_resolvedValues.add(_results[0].getValueSpecification());
_resolvedValues.add(_results[1].getValueSpecification());
_resolvedValues.add(_results[2].getValueSpecification());
_resolvedValues.add(valueSpec);
break;
default:
throw new IllegalStateException();
}
}
final int l = _results.length;
s_logger.debug("Result {} available from {}", value, this);
if (l > 0) {
final ResolvedValue[] newResults = new ResolvedValue[l + 1];
System.arraycopy(_results, 0, newResults, 0, l);
newResults[l] = value;
_results = newResults;
} else {
_results = new ResolvedValue[] {value };
}
if (_failure != null) {
// Don't hold onto any failure state if there is a result
_failure = null;
_failureCopied = false;
}
if (_pumping > 0) {
if (lastResult) {
_pumping |= 6;
} else {
_pumping |= 2;
}
return true;
}
final int s = _pumped.size();
if (s > 0) {
pumped = _pumped.toArray(new Callback[s]);
_pumped.clear();
}
if (lastResult) {
// Clear the _pumped reference so that if a subscriber calls pump we won't get an invocation of pumpImpl (which may call
// "finished") in addition to our call to "finished" below.
_pumped = null;
}
}
pumpCallbacks(context, pumped, lastResult);
if (lastResult) {
finished(context);
}
return true;
}
/**
* Triggers either a future (or immediate) call to either one or more {@link #pushResult} or a single {@link #finished}.
*
* @param context building context
*/
protected abstract void pumpImpl(final GraphBuildingContext context);
/**
* Call when there are no more values that can be pushed. Any callbacks that have had pump called on them will receive a failure notification. This must only be called once.
*/
protected void finished(final GraphBuildingContext context) {
assert !_finished;
s_logger.debug("Finished producing results at {}", this);
Callback[] pumped = null;
synchronized (this) {
_finished = true;
if (_pumping > 0) {
_pumping |= 2;
return;
}
if (_pumped != null) {
final int s = _pumped.size();
if (s > 0) {
pumped = _pumped.toArray(new Callback[s]);
}
_pumped = null;
}
}
pumpCallbacks(context, pumped, true);
}
protected boolean isFinished() {
return _finished;
}
protected void storeFailure(final ResolutionFailure failure) {
if (failure != null) {
synchronized (this) {
if ((_results != null) && (_results.length == 0)) {
// Only store failure info if there are no results to push
if (_failure == null) {
_failure = failure;
return;
}
if (!_failureCopied) {
_failure = (ResolutionFailure) _failure.clone();
_failureCopied = true;
}
_failure.merge(failure);
}
}
}
}
/**
* Returns the current results in the order they were produced. If the producer is not in a "finished" state, the results are the current intermediate values. The caller must not modify the content
* of the array.
*
* @return the current results
*/
protected synchronized ResolvedValue[] getResults() {
return _results;
}
@Override
public ValueRequirement getValueRequirement() {
return _valueRequirement;
}
protected int getObjectId() {
return _objectId;
}
@Override
public synchronized boolean addRef() {
if (_refCount > 0) {
_refCount++;
return true;
} else {
return false;
}
}
@Override
public synchronized int release(final GraphBuildingContext context) {
assert _refCount > 0;
final int result = --_refCount;
if (result == 0) {
// help out the garbage collector
_resolvedValues = null;
_pumped = null;
_results = null;
_failure = null;
}
return result;
}
/**
* Returns the current reference count for the object.
*
* @return the reference count
*/
@Override
public synchronized int getRefCount() {
return _refCount;
}
protected void setRecursionDetected() {
final List<Callback> pumped;
synchronized (this) {
if ((_pumped == null) || _pumped.isEmpty()) {
return;
}
pumped = new ArrayList<Callback>(_pumped);
}
for (Callback callback : pumped) {
callback._callback.recursionDetected();
}
}
@Override
public int cancelLoopMembers(final GraphBuildingContext context, final Map<Chain, LoopState> visited) {
final List<Callback> pumped;
synchronized (this) {
if ((_pumped == null) || _pumped.isEmpty()) {
s_logger.debug("Callback {} has no pumped callbacks", this);
// No callbacks pumped (or has already finished), so can't be in a loop
return 0;
}
pumped = new ArrayList<Callback>(_pumped);
}
int result = 0;
for (Callback callback : pumped) {
if (callback._callback instanceof Chain) {
final Chain chained = (Chain) callback._callback;
final LoopState state = visited.put(chained, LoopState.CHECKING);
if (state == null) {
// Not visited yet
final int looped = chained.cancelLoopMembers(context, visited);
if (looped > 0) {
result += looped;
// The callback is at least in a chain that leads to a loop, but might not be in a loop itself
if (visited.get(chained) == LoopState.CHECKING) {
visited.remove(chained);
}
} else {
// Known not to be in a loop
visited.put(chained, LoopState.NOT_IN_LOOP);
}
} else if (state == LoopState.NOT_IN_LOOP) {
// Callback is not in a loop - reset the flag
visited.put(chained, LoopState.NOT_IN_LOOP);
} else {
// The callback is in a loop - reset the flag and cancel it
visited.put(chained, LoopState.IN_LOOP);
s_logger.info("Detected loop at {}, callback {}", this, callback);
if (callback.cancel(context)) {
s_logger.debug("Canceled callback; signalling failure");
callback._callback.failed(context, getValueRequirement(), context.recursiveRequirement(getValueRequirement()));
result++;
} else {
s_logger.debug("Already canceled");
}
}
}
}
s_logger.info("Processed {} pumped callbacks, canceled {}", pumped.size(), result);
return result;
}
}