/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.engine.view.client.merging; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Instant; import com.google.common.base.Function; import com.opengamma.engine.resource.EngineResourceManagerInternal; import com.opengamma.engine.resource.EngineResourceRetainer; import com.opengamma.engine.view.ViewComputationResultModel; import com.opengamma.engine.view.ViewDeltaResultModel; import com.opengamma.engine.view.compilation.CompiledViewDefinition; import com.opengamma.engine.view.cycle.ViewCycleMetadata; import com.opengamma.engine.view.execution.ViewCycleExecutionOptions; import com.opengamma.engine.view.listener.ClientShutdownCall; import com.opengamma.engine.view.listener.CycleCompletedCall; import com.opengamma.engine.view.listener.CycleExecutionFailedCall; import com.opengamma.engine.view.listener.CycleFragmentCompletedCall; import com.opengamma.engine.view.listener.CycleStartedCall; import com.opengamma.engine.view.listener.ProcessCompletedCall; import com.opengamma.engine.view.listener.ProcessTerminatedCall; import com.opengamma.engine.view.listener.ViewDefinitionCompilationFailedCall; import com.opengamma.engine.view.listener.ViewDefinitionCompiledCall; import com.opengamma.engine.view.listener.ViewResultListener; import com.opengamma.livedata.UserPrincipal; import com.opengamma.util.ArgumentChecker; /** * Collects and merges view process updates, releasing them only when {@code drain()} is called. Also ensures that different update types are passed to the underlying listener in the correct order * when drained. * <p> * Fragments and delta results are merged so that those corresponding to the latest cycle will be available. Individual notifications from earlier cycles will be discarded. For example, if there is a * view compilation and a number of cycles run, the events released will be the compilation notification, merged events corresponding to the last full cycle, and anything available for any incomplete * cycle. */ public class MergingViewProcessListener implements ViewResultListener { private static final Logger s_logger = LoggerFactory.getLogger(MergingViewProcessListener.class); /** * Node in a doubly linked list of calls. This structure allows merged calls to be plucked easily from the list and moved to the end. */ protected static final class Call<T extends Function<ViewResultListener, ?>> { private final T _function; private Call<?> _prev; private Call<?> _next; private Call(final T function, final Call<?> last) { _function = function; _prev = last; if (last != null) { assert last._next == null; last._next = this; } } private T getFunction() { return _function; } private void remove() { if (_prev != null) { _prev._next = _next; } if (_next != null) { _next._prev = _prev; } } private void moveToEnd(final Call<?> last) { assert (last != null) && (last != this) && (last._next == null); final Call<?> prev = _prev; final Call<?> next = _next; _prev = last; _next = null; last._next = this; if (prev != null) { prev._next = next; } if (next != null) { next._prev = prev; } } } private final ReentrantLock _mergerLock = new ReentrantLock(); private final ViewResultListener _underlying; private boolean _isPassThrough = true; private boolean _isLatestResultCycleRetained; private EngineResourceRetainer _cycleRetainer; /** * The time at which an update was last received. */ private final AtomicLong _lastUpdateMillis = new AtomicLong(0); private Call<?> _firstCall; private Call<?> _lastCall; /** * The view compilation notification that corresponds to the last full result, or execution failure, if {@link #_latestCompilation} is set */ private Call<?> _previousCompilation; /** * The last view compilation notification seen. */ private Call<?> _latestCompilation; /** * The start notification that corresponds to the last full result, or execution failure, if {@link #_latestCycleStarted} is set */ private Call<CycleStartedCall> _previousCycleStarted; /** * The last start notification seen. */ private Call<CycleStartedCall> _latestCycleStarted; /** * The fragment notification that corresponds to the last full result, or execution failure, if {@link #_latestCycleFragmentCompleted} is set */ private Call<CycleFragmentCompletedCall> _previousCycleFragmentCompleted; /** * The last fragment notification seen. */ private Call<CycleFragmentCompletedCall> _latestCycleFragmentCompleted; /** * The last cycle failure notification seen. There will be no earlier cycle completion or failure notifications in the queue */ private Call<CycleExecutionFailedCall> _cycleFailed; /** * The last cycle completed notification seen. There will be no earlier cycle completion or failure notifications in the queue. */ private Call<CycleCompletedCall> _cycleCompleted; public MergingViewProcessListener(ViewResultListener underlying, EngineResourceManagerInternal<?> cycleManager) { ArgumentChecker.notNull(underlying, "underlying"); _underlying = underlying; _cycleRetainer = new EngineResourceRetainer(cycleManager); } //------------------------------------------------------------------------- /** * Gets whether incoming updates should be allowed to pass straight through without merging. If this is false then updates will not be released unless an update is triggered. * * @return true if updates should be passed straight to listeners without merging */ protected boolean isPassThrough() { _mergerLock.lock(); try { return _isPassThrough; } finally { _mergerLock.unlock(); } } /** * Sets whether incoming updates should be allowed to pass straight through without merging. If this is changed to <code>true</code> then an update is first triggered to clear any existing merged * updates. Subsequent updates will pass straight through until this is set to <code>false</code>. * * @param passThrough true if incoming updates should be allowed to pass straight through without merging, or false to merge updates until an update is triggered * @return previously batched invocations on the underlying listener as a result of enabling pass-through. It is up to the caller to invoke them. */ protected Call<?> setPassThrough(boolean passThrough) { _mergerLock.lock(); try { _isPassThrough = passThrough; if (passThrough) { // Release anything that's been merged while it hasn't been passing updates straight through return takeCallQueue(); } else { return null; } } finally { _mergerLock.unlock(); } } /** * Gets the time at which the last update was received. * * @return the time at which the last udpate was received, in milliseconds */ protected long getLastUpdateTimeMillis() { return _lastUpdateMillis.get(); } //------------------------------------------------------------------------- public boolean isLatestResultCycleRetained() { return _isLatestResultCycleRetained; } public void setLatestResultCycleRetained(boolean isLatestResultCycleRetained) { _mergerLock.lock(); try { _isLatestResultCycleRetained = isLatestResultCycleRetained; if (!isLatestResultCycleRetained) { getCycleRetainer().replaceRetainedCycle(null); } } finally { _mergerLock.unlock(); } } //------------------------------------------------------------------------- @Override public UserPrincipal getUser() { return getUnderlying().getUser(); } @Override public void viewDefinitionCompiled(CompiledViewDefinition compiledViewDefinition, boolean hasMarketDataPermissions) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { if (_previousCompilation != null) { removeCall(_previousCompilation); } _previousCompilation = _latestCompilation; _latestCompilation = addCall(new ViewDefinitionCompiledCall(compiledViewDefinition, hasMarketDataPermissions)); return; } } finally { _mergerLock.unlock(); } getUnderlying().viewDefinitionCompiled(compiledViewDefinition, hasMarketDataPermissions); } @Override public void viewDefinitionCompilationFailed(Instant valuationTime, Exception exception) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { clearCallQueue(); _previousCompilation = addCall(new ViewDefinitionCompilationFailedCall(valuationTime, exception)); return; } } finally { _mergerLock.unlock(); } getUnderlying().viewDefinitionCompilationFailed(valuationTime, exception); } @Override public void cycleStarted(ViewCycleMetadata cycleMetadata) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { if (_previousCycleStarted != null) { // This shouldn't happen if notifications appear as expected removeCall(_previousCycleStarted); } _previousCycleStarted = _latestCycleStarted; _latestCycleStarted = addCall(new CycleStartedCall(cycleMetadata)); return; } } finally { _mergerLock.unlock(); } getUnderlying().cycleStarted(cycleMetadata); } @Override public void cycleCompleted(ViewComputationResultModel fullResult, ViewDeltaResultModel deltaResult) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (isLatestResultCycleRetained() && fullResult != null) { getCycleRetainer().replaceRetainedCycle(fullResult.getViewCycleId()); } if (!isPassThrough()) { if (_cycleFailed != null) { // There's a previous failure in the queue; remove it removeCall(_cycleFailed); _cycleFailed = null; } if (_cycleCompleted != null) { // There's a previous cycle completed call in the queue - move to end putCallToEnd(_cycleCompleted); // Merge new cycle completed call into old one _cycleCompleted.getFunction().update(fullResult, deltaResult); } else { // No existing cycle completed call - add new one _cycleCompleted = addCall(new CycleCompletedCall(fullResult, deltaResult)); } // Only keep any fragment corresponding to this latest result if (_previousCycleFragmentCompleted != null) { removeCall(_previousCycleFragmentCompleted); } _previousCycleFragmentCompleted = _latestCycleFragmentCompleted; _latestCycleFragmentCompleted = null; // Only keep the cycle started call for the latest complete result if (_previousCycleStarted != null) { removeCall(_previousCycleStarted); _previousCycleStarted = null; } // Only keep the compilation call for the latest complete result if (_previousCompilation != null) { removeCall(_previousCompilation); _previousCompilation = null; } return; } } finally { _mergerLock.unlock(); } getUnderlying().cycleCompleted(fullResult, deltaResult); } @Override public void cycleFragmentCompleted(ViewComputationResultModel fullFragment, ViewDeltaResultModel deltaFragment) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { if (_latestCycleFragmentCompleted != null) { // There's a current fragment completed call in the queue - move to end putCallToEnd(_latestCycleFragmentCompleted); // Merge new fragment completed call into old one _latestCycleFragmentCompleted.getFunction().update(fullFragment, deltaFragment); } else { // No existing fragment completed call - add new one _latestCycleFragmentCompleted = addCall(new CycleFragmentCompletedCall(fullFragment, deltaFragment)); } return; } } finally { _mergerLock.unlock(); } getUnderlying().cycleFragmentCompleted(fullFragment, deltaFragment); } @Override public void cycleExecutionFailed(ViewCycleExecutionOptions executionOptions, Exception exception) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { if (_cycleCompleted != null) { // Remove any previous success removeCall(_cycleCompleted); _cycleCompleted = null; } if (_cycleFailed != null) { // Remove any previous failure removeCall(_cycleFailed); } _cycleFailed = addCall(new CycleExecutionFailedCall(executionOptions, exception)); // Only keep any fragment corresponding to this failure if (_previousCycleFragmentCompleted != null) { removeCall(_previousCycleFragmentCompleted); } _previousCycleFragmentCompleted = _latestCycleFragmentCompleted; _latestCycleFragmentCompleted = null; // Only keep the cycle started call for this failure if (_previousCycleStarted != null) { removeCall(_previousCycleStarted); _previousCycleStarted = null; } // Only keep the compilation call for this failure if (_previousCompilation != null) { removeCall(_previousCompilation); _previousCompilation = null; } return; } } finally { _mergerLock.unlock(); } getUnderlying().cycleExecutionFailed(executionOptions, exception); } @Override public void processCompleted() { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { addCall(new ProcessCompletedCall()); return; } } finally { _mergerLock.unlock(); } getUnderlying().processCompleted(); } @Override public void processTerminated(boolean executionInterrupted) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); getCycleRetainer().replaceRetainedCycle(null); if (!isPassThrough()) { addCall(new ProcessTerminatedCall(executionInterrupted)); return; } } finally { _mergerLock.unlock(); } getUnderlying().processTerminated(executionInterrupted); } @Override public void clientShutdown(Exception e) { _mergerLock.lock(); try { _lastUpdateMillis.set(System.currentTimeMillis()); if (!isPassThrough()) { addCall(new ClientShutdownCall(e)); return; } } finally { _mergerLock.unlock(); } getUnderlying().clientShutdown(e); } /** * Clears the internal call queue state. The caller must hold the {@link #_mergerLock}. */ private void clearCallQueue() { _firstCall = null; _lastCall = null; _previousCompilation = null; _latestCompilation = null; _previousCycleStarted = null; _latestCycleStarted = null; _cycleFailed = null; _cycleCompleted = null; _previousCycleFragmentCompleted = null; _latestCycleFragmentCompleted = null; } /** * Returns the head of the queue, clearing it. The caller must hold the {@link #_mergerLock} lock. * * @return the first element in the taken queue, or null if there was none */ private Call<?> takeCallQueue() { final Call<?> result; result = _firstCall; clearCallQueue(); return result; } /** * Makes queued calls to the underlying. The caller must not hold the {@link #_mergerLock} lock. * * @param call the node head of the list, null for an empty list */ protected void invoke(Call<?> call) { while (call != null) { try { call.getFunction().apply(getUnderlying()); } catch (RuntimeException e) { s_logger.error("Error notifying underlying of {}: {}", call.getFunction(), e.getMessage()); s_logger.warn("Caught exception", e); } call = call._next; } } /** * Invokes all of the calls from the queue, clearing it. The caller must not hold the {@link #_mergerLock} lock. */ protected void drain() { final Call<?> calls; _mergerLock.lock(); try { calls = takeCallQueue(); } finally { _mergerLock.unlock(); } invoke(calls); } /** * Consumes any updates waiting in the merger without notifying the underlying listener. */ public void reset() { _mergerLock.lock(); try { clearCallQueue(); getCycleRetainer().replaceRetainedCycle(null); } finally { _mergerLock.unlock(); } } /** * Moves the given call to the end of the linked list. The caller must hold the {@link #_mergerLock} lock. */ private void putCallToEnd(final Call<?> call) { if (call == _lastCall) { // Already at the end return; } if (call == _firstCall) { // At the head of the queue, and there is more than one element _firstCall = call._next; } call.moveToEnd(_lastCall); _lastCall = call; } /** * Removes the given call from the linked list. The caller must hold the {@link #_mergerLock} lock. */ private void removeCall(final Call<?> call) { if (call == _firstCall) { // At the head of the queue _firstCall = call._next; } if (call == _lastCall) { // At the tail of the queue _lastCall = call._prev; } call.remove(); } /** * Adds a new call to the end of the linked list. The caller must hold the {@link #_mergerLock} lock. */ private <T extends Function<ViewResultListener, ?>> Call<T> addCall(final T function) { final Call<T> call = new Call<T>(function, _lastCall); if (_firstCall == null) { // First element into the queue _firstCall = call; } _lastCall = call; return call; } private ViewResultListener getUnderlying() { return _underlying; } private EngineResourceRetainer getCycleRetainer() { return _cycleRetainer; } }