/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.livedata.server.distribution; import java.util.Collection; import java.util.concurrent.atomic.AtomicLong; import org.fudgemsg.FudgeMsg; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.livedata.LiveDataSpecification; import com.opengamma.livedata.LiveDataValueUpdateBean; import com.opengamma.livedata.server.DistributionSpecification; import com.opengamma.livedata.server.FieldHistoryStore; import com.opengamma.livedata.server.LastKnownValueStore; import com.opengamma.livedata.server.LastKnownValueStoreProvider; import com.opengamma.livedata.server.Subscription; import com.opengamma.util.ArgumentChecker; /** * Distributes market data to clients and keeps a history of what has been distributed. */ public class MarketDataDistributor { /** Logger. */ private static final Logger s_logger = LoggerFactory.getLogger(MarketDataDistributor.class); /** * What data should be distributed, how and where. */ private final DistributionSpecification _distributionSpec; /** * Which subscription this distributor belongs to. */ private final Subscription _subscription; /** These listener(s) actually publish the data */ private final Collection<MarketDataSender> _marketDataSenders; /** * Last known values of ALL fully normalized fields that were * sent to clients. This is not the last message as such * because the last message might not have included all the fields. * Instead, because the last value of ALL fields is stored, * this store provides a current snapshot of the entire state of the * market data line. */ private final LastKnownValueStore _lastKnownValues; /** * A history store to be used by the FieldHistoryUpdater normalization rule. * Fields stored in this history could either be completely unnormalized, * partially normalized, or fully normalized. */ private final FieldHistoryStore _history = new FieldHistoryStore(); /** * Stores how many normalized messages have been sent to clients. */ private final AtomicLong _numMessagesSent = new AtomicLong(0); /** * Whether this distributor is persistent. * <p> * True = a persistent distributor, should survive a server restart. * False = a non-persistent distributor. Will die if the server is * restarted. */ private boolean _persistent; /** * When this distributor should stop distributing * data if no heartbeats are received from clients. * <p> * Stored as milliseconds from UTC epoch. * <p> * Null means the distributor should not expire. */ private Long _expiry; /** * Creates an instance. * * @param distributionSpec What data should be distributed, how and where. * @param subscription Which subscription this distributor belongs to. * @param marketDataSenderFactory Used to create listener(s) that actually publish the data * @param persistent Whether this distributor is persistent. * @param lkvStoreProvider The factory for LastKnownValue stores. */ public MarketDataDistributor(DistributionSpecification distributionSpec, Subscription subscription, MarketDataSenderFactory marketDataSenderFactory, boolean persistent, LastKnownValueStoreProvider lkvStoreProvider) { ArgumentChecker.notNull(distributionSpec, "Distribution spec"); ArgumentChecker.notNull(subscription, "Subscription"); ArgumentChecker.notNull(marketDataSenderFactory, "Market data sender factory"); ArgumentChecker.notNull(lkvStoreProvider, "LKV Store Provider"); _distributionSpec = distributionSpec; _subscription = subscription; _marketDataSenders = marketDataSenderFactory.create(this); if (_marketDataSenders == null) { throw new IllegalStateException("Null returned by " + marketDataSenderFactory); } setPersistent(persistent); _lastKnownValues = lkvStoreProvider.newInstance(distributionSpec.getMarketDataId(), distributionSpec.getNormalizationRuleSet().getId()); // Initialize history with last known values. // This does nothing in the default state (where the LKV is empty) but // in case where the LKV is backed by a persistent store will prep the // current state based on the persistent version. _history.liveDataReceived(_lastKnownValues.getFields()); } //------------------------------------------------------------------------- /** * Gets the distribution specification. * * @return the distribution specification, not null */ public DistributionSpecification getDistributionSpec() { return _distributionSpec; } /** * Gets the live data specification. * * @return the specification of the data */ public LiveDataSpecification getFullyQualifiedLiveDataSpecification() { return getDistributionSpec().getFullyQualifiedLiveDataSpecification(); } /** * Gets the subscription details. * * @return the subscription details */ public Subscription getSubscription() { return _subscription; } /** * Gets the number of messages sent. * * @return the message count */ public long getNumMessagesSent() { return _numMessagesSent.get(); } //------------------------------------------------------------------------- /** * Gets a snapshot of data, returning the latest value. * * @return the latest value, not null */ public LiveDataValueUpdateBean getSnapshot() { FudgeMsg lastKnownValues = getLastKnownValues(); if (lastKnownValues == null) { return null; } return new LiveDataValueUpdateBean( getNumMessagesSent(), // 0-based as it should be getDistributionSpec().getFullyQualifiedLiveDataSpecification(), lastKnownValues); } private synchronized FudgeMsg getLastKnownValues() { // NOTE kirk 2012-07-12 -- I have to assume the sentinel is important // so I'm preserving it even though _lastKnownValues will never be null now. if (_lastKnownValues.isEmpty()) { return null; } return _lastKnownValues.getFields(); } private synchronized void updateLastKnownValues(FudgeMsg lastKnownValue) { _lastKnownValues.updateFields(lastKnownValue); } //------------------------------------------------------------------------- /** * Normalizes the data. * * @param msg the message received from underlying market data API in its native format * @return the normalized message. Null if in the process of normalization, * the message became empty and therefore should not be sent. */ private FudgeMsg normalize(FudgeMsg msg) { FudgeMsg normalizedMsg = _distributionSpec.getNormalizedMessage(msg, _subscription.getSecurityUniqueId(), _history); return normalizedMsg; } /** * Updates field history without sending any market data to field receivers. * * @param msg Unnormalized market data from underlying market data API. */ public synchronized void updateFieldHistory(FudgeMsg msg) { FudgeMsg normalizedMsg = normalize(msg); if (normalizedMsg != null) { updateLastKnownValues(normalizedMsg); } } /** * Sends normalized market data to field receivers. * <p> * Serialized to ensure a well-defined distribution order for this topic. * * @param liveDataFields Unnormalized market data from underlying market data API. */ public synchronized void distributeLiveData(FudgeMsg liveDataFields) { FudgeMsg normalizedMsg; try { normalizedMsg = normalize(liveDataFields); } catch (RuntimeException e) { s_logger.error("Normalizing " + liveDataFields + " to " + this + " failed.", e); return; } if (normalizedMsg != null) { updateLastKnownValues(normalizedMsg); LiveDataValueUpdateBean data = new LiveDataValueUpdateBean( getNumMessagesSent(), // 0-based as it should be getDistributionSpec().getFullyQualifiedLiveDataSpecification(), normalizedMsg); s_logger.debug("{}: Sending Live Data update {}", this, data); for (MarketDataSender sender : _marketDataSenders) { try { sender.sendMarketData(data); } catch (RuntimeException e) { s_logger.error(sender + " failed", e); } } _numMessagesSent.incrementAndGet(); } else { s_logger.debug("{}: Not sending Live Data update (message extinguished).", this); } } //------------------------------------------------------------------------- /** * Gets the expiry instant. * * @return the millisecond instant from UTC epoch, or null if the distributor never expires */ public synchronized Long getExpiry() { return _expiry; } /** * Sets the expiry instant. * * @param expiry the millisecond instant from UTC epoch, or null if the distributor never expires. */ public synchronized void setExpiry(Long expiry) { _expiry = expiry; } /** * Extends the expiry instant to a number of milliseconds in the future. * * @param timeoutExtensionMillis the extension duration */ public synchronized void extendExpiry(long timeoutExtensionMillis) { setExpiry(System.currentTimeMillis() + timeoutExtensionMillis); } /** * Checks if this has expired. * * @return true if expired */ public synchronized boolean hasExpired() { if (isPersistent()) { return false; } if (getExpiry() == null) { return false; } return getExpiry() < System.currentTimeMillis(); } //------------------------------------------------------------------------- /** * Checks if this is persistent. * <p> * True = a persistent distributor, should survive a server restart. * False = a non-persistent distributor. Will die if the server is * restarted. * * @return whether this distributor is persistent */ public synchronized boolean isPersistent() { return _persistent; } /** * Sets the persistent flag. * * @param persistent whether this is persistent */ public synchronized void setPersistent(boolean persistent) { _persistent = persistent; } //------------------------------------------------------------------------- @Override public String toString() { return "MarketDataDistributor[" + getDistributionSpec().toString() + "]"; } }