/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.livedata.client; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import org.fudgemsg.FudgeContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Gauge; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.livedata.LiveDataClient; import com.opengamma.livedata.LiveDataListener; import com.opengamma.livedata.LiveDataSpecification; import com.opengamma.livedata.LiveDataValueUpdate; import com.opengamma.livedata.LiveDataValueUpdateBean; import com.opengamma.livedata.UserPrincipal; import com.opengamma.livedata.msg.LiveDataSubscriptionResponse; import com.opengamma.livedata.msg.SubscriptionType; import com.opengamma.livedata.normalization.StandardRules; import com.opengamma.transport.ByteArrayMessageSender; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.PoolExecutor; import com.opengamma.util.PublicAPI; import com.opengamma.util.fudgemsg.OpenGammaFudgeContext; import com.opengamma.util.metric.MetricProducer; /** * A base class that handles all the in-memory requirements for a {@link LiveDataClient} implementation. */ @PublicAPI public abstract class AbstractLiveDataClient implements LiveDataClient, MetricProducer { private static final Logger s_logger = LoggerFactory.getLogger(AbstractLiveDataClient.class); // Injected Inputs: private long _heartbeatPeriod = Heartbeater.DEFAULT_PERIOD; private FudgeContext _fudgeContext = OpenGammaFudgeContext.getInstance(); // Running State: private final ValueDistributor _valueDistributor = new ValueDistributor(); private final Timer _timer = new Timer("LiveDataClient Timer"); private Heartbeater _heartbeater; private final Lock _subscriptionLock = new ReentrantLock(); private final ReentrantReadWriteLock _pendingSubscriptionLock = new ReentrantReadWriteLock(); private final ReadLock _pendingSubscriptionReadLock = _pendingSubscriptionLock.readLock(); private final WriteLock _pendingSubscriptionWriteLock = _pendingSubscriptionLock.writeLock(); private final Multimap<LiveDataSpecification, SubscriptionHandle> _fullyQualifiedSpec2PendingSubscriptions = HashMultimap.create(); /** * This is the reverse of _fullyQualifiedSpec2PendingSubscriptions */ // REVIEW simon 2012-02-20 -- I suspect that these could just be a BiMap, but it's not obvious from the current implementation private final Multimap<SubscriptionHandle, LiveDataSpecification> _specsByHandle = HashMultimap.create(); private Meter _inboundTickMeter; @Override public synchronized void registerMetrics(MetricRegistry summaryRegistry, MetricRegistry detailedRegistry, String namePrefix) { _inboundTickMeter = summaryRegistry.meter(namePrefix + ".ticks.count"); // REVIEW kirk 2013-04-22 -- This might be better as a Counter. summaryRegistry.register(namePrefix + ".subscriptions.count", new Gauge<Integer>() { @Override public Integer getValue() { return getValueDistributor().getActiveSpecificationCount(); } }); } public void setHeartbeatMessageSender(ByteArrayMessageSender messageSender) { ArgumentChecker.notNull(messageSender, "Message Sender"); _heartbeater = new Heartbeater(_valueDistributor, new HeartbeatSender(messageSender, getFudgeContext()), getTimer(), getHeartbeatPeriod()); } @Override public void close() { _timer.cancel(); } /** * @return the heartbeater */ public Heartbeater getHeartbeater() { return _heartbeater; } /** * @return the timer */ public Timer getTimer() { return _timer; } /** * @return the heartbeatPeriod */ public long getHeartbeatPeriod() { return _heartbeatPeriod; } /** * @param heartbeatPeriod the heartbeatPeriod to set */ public void setHeartbeatPeriod(long heartbeatPeriod) { _heartbeatPeriod = heartbeatPeriod; } /** * @return the valueDistributor */ public ValueDistributor getValueDistributor() { return _valueDistributor; } /** * @return the fudgeContext */ public FudgeContext getFudgeContext() { return _fudgeContext; } /** * @param fudgeContext the fudgeContext to set */ public void setFudgeContext(FudgeContext fudgeContext) { _fudgeContext = fudgeContext; } /** * Obtain <em>a copy of</em> the active subscription specifications. For concurrency reason this will return a new copy on each call. * * @return a copy of the Active Fully-Qualified Subscription Specifications */ public Set<LiveDataSpecification> getActiveSubscriptionSpecifications() { return getValueDistributor().getActiveSpecifications(); } @Override public void subscribe(UserPrincipal user, Collection<LiveDataSpecification> requestedSpecifications, LiveDataListener listener) { ArrayList<SubscriptionHandle> subscriptionHandles = new ArrayList<SubscriptionHandle>(); for (LiveDataSpecification requestedSpecification : requestedSpecifications) { SubscriptionHandle subHandle = new SubscriptionHandle(user, SubscriptionType.NON_PERSISTENT, requestedSpecification, listener); subscriptionHandles.add(subHandle); } if (!subscriptionHandles.isEmpty()) { handleSubscriptionRequest(subscriptionHandles); } } @Override public void subscribe(UserPrincipal user, LiveDataSpecification requestedSpecification, LiveDataListener listener) { subscribe(user, Collections.singleton(requestedSpecification), listener); } private abstract class SnapshotListener implements LiveDataListener { private final Collection<LiveDataSubscriptionResponse> _responses = new ArrayList<LiveDataSubscriptionResponse>(); private final AtomicInteger _responsesOutstanding; public SnapshotListener(int expectedNumberOfResponses) { _responsesOutstanding = new AtomicInteger(expectedNumberOfResponses); } @Override public void subscriptionResultReceived(LiveDataSubscriptionResponse subscriptionResult) { _responses.add(subscriptionResult); if (_responsesOutstanding.decrementAndGet() <= 0) { notifyResponses(); } } @Override public void subscriptionResultsReceived(Collection<LiveDataSubscriptionResponse> subscriptionResults) { _responses.addAll(subscriptionResults); if (_responsesOutstanding.addAndGet(-subscriptionResults.size()) <= 0) { notifyResponses(); } } @Override public void subscriptionStopped(LiveDataSpecification fullyQualifiedSpecification) { // should never go here throw new UnsupportedOperationException(); } @Override public void valueUpdate(LiveDataValueUpdate valueUpdate) { // should never go here throw new UnsupportedOperationException(); } protected boolean responsesOutstanding() { return _responsesOutstanding.get() > 0; } protected abstract void notifyResponses(); public Collection<LiveDataSubscriptionResponse> getResponses() { return _responses; } } private final class SynchronousSnapshotListener extends SnapshotListener { public SynchronousSnapshotListener(final int expectedNumberOfResponses) { super(expectedNumberOfResponses); } @Override protected synchronized void notifyResponses() { notifyAll(); } public synchronized boolean waitForResponses(final long timeout) throws InterruptedException { final long delayUntil = System.nanoTime() + timeout * 1_000_000L; while (responsesOutstanding()) { final long delay = delayUntil - System.nanoTime(); if (delay < 1_000_000L) { return !responsesOutstanding(); } wait(delay / 1_000_000L); } return true; } } private final class AsynchronousSnapshotListener extends SnapshotListener { private PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>> _callback; public AsynchronousSnapshotListener(final int expectedNumberOfResponses, final PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>> callback) { super(expectedNumberOfResponses); _callback = callback; } @Override protected synchronized void notifyResponses() { final PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>> callback; synchronized (this) { callback = _callback; _callback = null; } if (callback != null) { callback.success(getResponses()); } } public void waitForResponses(final long timeout) { _timer.schedule(new TimerTask() { @Override public void run() { final PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>> callback; synchronized (this) { callback = _callback; _callback = null; } if (callback != null) { callback.success(null); } } }, timeout); } } @Override public Collection<LiveDataSubscriptionResponse> snapshot(UserPrincipal user, Collection<LiveDataSpecification> requestedSpecifications, long timeout) { ArgumentChecker.notNull(user, "User"); ArgumentChecker.notNull(requestedSpecifications, "Live Data specifications"); if (requestedSpecifications.isEmpty()) { return Collections.emptySet(); } final SynchronousSnapshotListener listener = new SynchronousSnapshotListener(requestedSpecifications.size()); ArrayList<SubscriptionHandle> subscriptionHandles = new ArrayList<SubscriptionHandle>(); for (LiveDataSpecification requestedSpecification : requestedSpecifications) { SubscriptionHandle subHandle = new SubscriptionHandle(user, SubscriptionType.SNAPSHOT, requestedSpecification, listener); subscriptionHandles.add(subHandle); } handleSubscriptionRequest(subscriptionHandles); boolean success; try { success = listener.waitForResponses(timeout); } catch (InterruptedException e) { Thread.interrupted(); throw new OpenGammaRuntimeException("Thread interrupted when obtaining snapshot"); } if (success) { return listener.getResponses(); } else { throw new OpenGammaRuntimeException("Timeout " + timeout + "ms reached when obtaining snapshot of " + requestedSpecifications.size() + " handles"); } } /** * Asynchronous form of {@link #snapshot(UserPrincipal, Collection<LiveDataSpecification>, long)}. * * @param user see {@link #snapshot(UserPrincipal, Collection<LiveDataSpecification>, long)} * @param requestedSpecifications see {@link #snapshot(UserPrincipal, Collection<LiveDataSpecification>, long)} * @param timeout see {@link #snapshot(UserPrincipal, Collection<LiveDataSpecification>, long)} * @param callback receives the result of execution */ protected void snapshot(final UserPrincipal user, final Collection<LiveDataSpecification> requestedSpecifications, final long timeout, final PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>> callback) { ArgumentChecker.notNull(user, "User"); ArgumentChecker.notNull(requestedSpecifications, "Live Data specifications"); if (requestedSpecifications.isEmpty()) { callback.success(Collections.<LiveDataSubscriptionResponse>emptySet()); return; } final AsynchronousSnapshotListener listener = new AsynchronousSnapshotListener(requestedSpecifications.size(), new PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>>() { @Override public void success(final Collection<LiveDataSubscriptionResponse> result) { if (result != null) { callback.success(result); } else { callback.failure(new OpenGammaRuntimeException("Timeout " + timeout + "ms reached when obtaining snapshot of " + requestedSpecifications.size() + " handles")); } } @Override public void failure(final Throwable error) { callback.failure(error); } }); ArrayList<SubscriptionHandle> subscriptionHandles = new ArrayList<SubscriptionHandle>(); for (LiveDataSpecification requestedSpecification : requestedSpecifications) { SubscriptionHandle subHandle = new SubscriptionHandle(user, SubscriptionType.SNAPSHOT, requestedSpecification, listener); subscriptionHandles.add(subHandle); } handleSubscriptionRequest(subscriptionHandles); listener.waitForResponses(timeout); } @Override public LiveDataSubscriptionResponse snapshot(UserPrincipal user, LiveDataSpecification requestedSpecification, long timeout) { Collection<LiveDataSubscriptionResponse> snapshots = snapshot(user, Collections.singleton(requestedSpecification), timeout); if (snapshots.size() != 1) { throw new OpenGammaRuntimeException("One snapshot request should return 1 snapshot, was " + snapshots.size()); } return snapshots.iterator().next(); } /** * @param subHandle Not null, not empty */ protected abstract void handleSubscriptionRequest(Collection<SubscriptionHandle> subHandle); protected void subscriptionStartingToReceiveTicks(SubscriptionHandle subHandle, LiveDataSubscriptionResponse response) { _pendingSubscriptionWriteLock.lock(); try { _fullyQualifiedSpec2PendingSubscriptions.put(response.getFullyQualifiedSpecification(), subHandle); _specsByHandle.put(subHandle, response.getFullyQualifiedSpecification()); } finally { _pendingSubscriptionWriteLock.unlock(); } } protected void subscriptionRequestSatisfied(SubscriptionHandle subHandle, LiveDataSubscriptionResponse response) { _pendingSubscriptionWriteLock.lock(); try { // Atomically (to valueUpdate callers) turn the pending subscription into a full subscription. // REVIEW jonathan 2010-12-01 -- rearranged this so that the internal _subscriptionLock is not being held while // releasing ticks to listeners, which is a recipe for deadlock. removePendingSubscription(subHandle); subHandle.releaseTicksOnHold(); _subscriptionLock.lock(); try { getValueDistributor().addListener(response.getFullyQualifiedSpecification(), subHandle.getListener()); } finally { _subscriptionLock.unlock(); } } finally { _pendingSubscriptionWriteLock.unlock(); } } protected void subscriptionRequestFailed(SubscriptionHandle subHandle, LiveDataSubscriptionResponse response) { removePendingSubscription(subHandle); } protected void removePendingSubscription(SubscriptionHandle subHandle) { _pendingSubscriptionWriteLock.lock(); try { Collection<LiveDataSpecification> specs = _specsByHandle.removeAll(subHandle); for (LiveDataSpecification liveDataSpecification : specs) { _fullyQualifiedSpec2PendingSubscriptions.remove(liveDataSpecification, subHandle); } } finally { _pendingSubscriptionWriteLock.unlock(); } } @Override public void unsubscribe(UserPrincipal user, Collection<LiveDataSpecification> fullyQualifiedSpecifications, LiveDataListener listener) { for (LiveDataSpecification fullyQualifiedSpecification : fullyQualifiedSpecifications) { s_logger.info("Unsubscribing by {} to {} delivered to {}", new Object[] {user, fullyQualifiedSpecification, listener }); boolean unsubscribeToSpec = false; _subscriptionLock.lock(); try { boolean stillActiveSubs = getValueDistributor().removeListener(fullyQualifiedSpecification, listener); if (!stillActiveSubs) { unsubscribeToSpec = true; } } finally { _subscriptionLock.unlock(); } // REVIEW kirk 2009-09-29 -- Potential race condition with multiple // subscribers and unsubscribers here.... do something about it? if (unsubscribeToSpec) { cancelPublication(fullyQualifiedSpecification); } listener.subscriptionStopped(fullyQualifiedSpecification); } } @Override public void unsubscribe(UserPrincipal user, LiveDataSpecification fullyQualifiedSpecification, LiveDataListener listener) { unsubscribe(user, Collections.singleton(fullyQualifiedSpecification), listener); } protected abstract void cancelPublication(LiveDataSpecification fullyQualifiedSpecification); @Override public String getDefaultNormalizationRuleSetId() { return StandardRules.getOpenGammaRuleSetId(); } protected void valueUpdate(LiveDataValueUpdateBean update) { if (_inboundTickMeter != null) { _inboundTickMeter.mark(); } s_logger.debug("{}", update); _pendingSubscriptionReadLock.lock(); try { Collection<SubscriptionHandle> pendingSubscriptions = _fullyQualifiedSpec2PendingSubscriptions.get(update.getSpecification()); for (SubscriptionHandle pendingSubscription : pendingSubscriptions) { pendingSubscription.addTickOnHold(update); } } finally { _pendingSubscriptionReadLock.unlock(); } getValueDistributor().notifyListeners(update); } }