/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.livedata.server; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import net.sf.ehcache.CacheManager; import org.fudgemsg.FudgeMsg; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.Lifecycle; import org.threeten.bp.Instant; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.id.ExternalId; import com.opengamma.id.ExternalIdBundle; import com.opengamma.id.ExternalScheme; import com.opengamma.livedata.LiveDataSpecification; import com.opengamma.livedata.LiveDataValueUpdateBean; import com.opengamma.livedata.entitlement.LiveDataEntitlementChecker; import com.opengamma.livedata.entitlement.PermissiveLiveDataEntitlementChecker; import com.opengamma.livedata.msg.LiveDataSubscriptionRequest; import com.opengamma.livedata.msg.LiveDataSubscriptionResponse; import com.opengamma.livedata.msg.LiveDataSubscriptionResponseMsg; import com.opengamma.livedata.msg.LiveDataSubscriptionResult; import com.opengamma.livedata.msg.SubscriptionType; import com.opengamma.livedata.normalization.StandardRules; import com.opengamma.livedata.permission.PermissionUtils; import com.opengamma.livedata.resolver.DistributionSpecificationResolver; import com.opengamma.livedata.resolver.NaiveDistributionSpecificationResolver; import com.opengamma.livedata.server.distribution.EmptyMarketDataSenderFactory; import com.opengamma.livedata.server.distribution.MarketDataDistributor; import com.opengamma.livedata.server.distribution.MarketDataSenderFactory; import com.opengamma.livedata.server.mxbean.DistributorTrace; import com.opengamma.livedata.server.mxbean.SubscriptionTrace; import com.opengamma.livedata.server.mxbean.SubscriptionTracer; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.PerformanceCounter; import com.opengamma.util.PublicAPI; /** * The base class from which most OpenGamma Live Data feed servers should extend. Handles most common cases for distributed contract management. */ @PublicAPI public abstract class StandardLiveDataServer implements LiveDataServer, Lifecycle, SubscriptionTracer { /** Logger. */ private static final Logger s_logger = LoggerFactory.getLogger(StandardLiveDataServer.class); private volatile MarketDataSenderFactory _marketDataSenderFactory = new EmptyMarketDataSenderFactory(); private final Collection<SubscriptionListener> _subscriptionListeners = new CopyOnWriteArrayList<>(); /** Access controlled via _subscriptionLock */ private final Set<Subscription> _currentlyActiveSubscriptions = new HashSet<>(); /** _Write_ access controlled via _subscriptionLock */ private final Map<String, Subscription> _securityUniqueId2Subscription = new ConcurrentHashMap<>(); /** Access controlled via _subscriptionLock */ private final Map<LiveDataSpecification, MarketDataDistributor> _fullyQualifiedSpec2Distributor = new HashMap<>(); private final AtomicLong _numMarketDataUpdatesReceived = new AtomicLong(0); private final PerformanceCounter _performanceCounter; private final CacheManager _cacheManager; private final Lock _subscriptionLock = new ReentrantLock(); private DistributionSpecificationResolver _distributionSpecificationResolver = new NaiveDistributionSpecificationResolver(); private LiveDataEntitlementChecker _entitlementChecker = new PermissiveLiveDataEntitlementChecker(); /** * The entitlement checker to be used for subscription requests. If null, * then {@link #_entitlementChecker} will be used. */ private LiveDataEntitlementChecker _subscriptionEntitlementChecker; private LastKnownValueStoreProvider _lkvStoreProvider = new MapLastKnownValueStoreProvider(); private volatile ConnectionStatus _connectionStatus = ConnectionStatus.NOT_CONNECTED; /** * The subscription expiry manager */ private final ExpirationManager _expirationManager = new ExpirationManager(this); /** * Creates an instance. * * @param cacheManager the cache manager, not null */ protected StandardLiveDataServer(CacheManager cacheManager) { this(cacheManager, true); } /** * Creates an instance controlling performance counting. * <p> * You may wish to disable performance counting if you expect a high rate of messages, or to process messages on several threads. * * @param cacheManager the cache manager, not null * @param isPerformanceCountingEnabled whether to track the message rate here, see {@link #getNumLiveDataUpdatesSentPerSecondOverLastMinute()} */ protected StandardLiveDataServer(CacheManager cacheManager, boolean isPerformanceCountingEnabled) { ArgumentChecker.notNull(cacheManager, "cacheManager"); _cacheManager = cacheManager; _performanceCounter = isPerformanceCountingEnabled ? new PerformanceCounter(60) : null; } //------------------------------------------------------------------------- /** * Gets the cache manager. * * @return the cache manager */ public CacheManager getCacheManager() { return _cacheManager; } /** * Gets the distribution resolver. * * @return the resolver, not null */ public DistributionSpecificationResolver getDistributionSpecificationResolver() { return _distributionSpecificationResolver; } /** * Sets the distribution resolver. * * @param distributionSpecificationResolver the distribution resolver, not null */ public void setDistributionSpecificationResolver(DistributionSpecificationResolver distributionSpecificationResolver) { ArgumentChecker.notNull(distributionSpecificationResolver, "distributionSpecificationResolver"); _distributionSpecificationResolver = distributionSpecificationResolver; } /** * Returns the expiration manager used to housekeep the subscriptions. * * @return the expiration manager, not null */ public ExpirationManager getExpirationManager() { return _expirationManager; } /** * Gets the market data sender factory. * * @return the factory, not null */ public MarketDataSenderFactory getMarketDataSenderFactory() { return _marketDataSenderFactory; } /** * Sets the market data sender factory. * * @param marketDataSenderFactory the factory, not null */ public void setMarketDataSenderFactory(MarketDataSenderFactory marketDataSenderFactory) { ArgumentChecker.notNull(marketDataSenderFactory, "marketDataSenderFactory"); _marketDataSenderFactory = marketDataSenderFactory; } /** * Adds a subscription listener. * * @param subscriptionListener the listener, not null */ public void addSubscriptionListener(SubscriptionListener subscriptionListener) { ArgumentChecker.notNull(subscriptionListener, "subscriptionListener"); _subscriptionListeners.add(subscriptionListener); } /** * Sets the subscription listeners, replacing all existing ones. * * @param subscriptionListeners the listeners, not null */ public void setSubscriptionListeners(Collection<SubscriptionListener> subscriptionListeners) { ArgumentChecker.noNulls(subscriptionListeners, "subscriptionListeners"); _subscriptionListeners.clear(); for (SubscriptionListener subscriptionListener : subscriptionListeners) { addSubscriptionListener(subscriptionListener); } } /** * Gets the entitlement checker. * * @return the entitlement checker, not null */ public LiveDataEntitlementChecker getEntitlementChecker() { return _entitlementChecker; } /** * Sets the entitlement checker. * * @param entitlementChecker the entitlement checker, not null */ public void setEntitlementChecker(LiveDataEntitlementChecker entitlementChecker) { ArgumentChecker.notNull(entitlementChecker, "entitlementChecker"); _entitlementChecker = entitlementChecker; } /** * Sets the entitlement checker to be used when a subscription is being made. If a checker * has not been specified then the standard entitlement checker will be used. If null is * passed then the standard one is used. * * @param subscriptionEntitlementChecker the entitlement checker to be used for subscriptions */ public void setSubscriptionEntitlementChecker(LiveDataEntitlementChecker subscriptionEntitlementChecker) { _subscriptionEntitlementChecker = subscriptionEntitlementChecker; } /** * Gets the default normalization rule set identifier. * * @return the identifier, not null */ public String getDefaultNormalizationRuleSetId() { return StandardRules.getOpenGammaRuleSetId(); } /** * Gets the provider for the last known value store. * * @return the provider, not null */ public LastKnownValueStoreProvider getLkvStoreProvider() { return _lkvStoreProvider; } /** * Sets the provider for the last known value store. * * @param lkvStoreProvider the provider, not null */ public void setLkvStoreProvider(LastKnownValueStoreProvider lkvStoreProvider) { ArgumentChecker.notNull(lkvStoreProvider, "lkvStoreProvider"); _lkvStoreProvider = lkvStoreProvider; } //------------------------------------------------------------------------- /** * Subscribes to the specified tickers using the underlying market data provider. * <p> * This returns a map from identifier to subscription handle. The map must contain an entry for each input <code>uniqueId</code>. Failure to subscribe to any <code>uniqueId</code> should result in * an exception being thrown. * * @param uniqueIds the collection of identifiers to subscribe to, may be empty, not null * @return the subscription handles corresponding to the identifiers, not null * @throws RuntimeException if subscribing to any unique IDs failed */ protected abstract Map<String, Object> doSubscribe(Collection<String> uniqueIds); /** * Unsubscribes to the given tickers using the underlying market data provider. * <p> * The handles returned by {@link #doSubscribe} are used to unsubscribe. * * @param subscriptionHandles the subscription handles to unsubscribe, may be empty, not null */ protected abstract void doUnsubscribe(Collection<Object> subscriptionHandles); /** * Returns an image (i.e., all fields) from the underlying market data provider. * <p> * This returns a map from identifier to a message describing the fields. The map must contain an entry for each <code>uniqueId</code>. Failure to snapshot any <code>uniqueId</code> should result in * an exception being thrown. * * @param uniqueIds the collection of identifiers to query, may be empty, not null * @return the snapshot result, not null * @throws RuntimeException if the snapshot could not be obtained */ protected abstract Map<String, FudgeMsg> doSnapshot(Collection<String> uniqueIds); /** * Gets the external scheme that defines securities for the underlying market data provider. * * @return the scheme, not null */ protected abstract ExternalScheme getUniqueIdDomain(); /** * Connects to the underlying market data provider. You can rely on the fact that this method * is only called when getConnectionStatus() == ConnectionStatus.NOT_CONNECTED. */ protected abstract void doConnect(); /** * Disconnects from the underlying market data provider. You can rely on the fact that this * method is only called when getConnectionStatus() == ConnectionStatus.CONNECTED. */ protected abstract void doDisconnect(); /** * In some cases, the underlying market data API may not, when a subscription is created, return a full image of all fields. If so, we need to get the full image explicitly. * * @param subscription the subscription currently being created, not null * @return true if a snapshot should be made when a new subscription is created */ protected abstract boolean snapshotOnSubscriptionStartRequired(Subscription subscription); /** * In some cases a subscription with no data may indicate that a snapshot will have no data * * @param distributior the currently active distributor for the security being snapshotted, not null * @return true if an empty subscription indicates that the snapshot result would be empty */ protected boolean canSatisfySnapshotFromEmptySubscription(MarketDataDistributor distributior) { //NOTE simon 28/11/2011: Only in the case of requiring a snapshot is it safe to use an empty snapshot from a subscription, since in the other case we may still be waiting for values return snapshotOnSubscriptionStartRequired(distributior.getSubscription()); } public LiveDataEntitlementChecker getSubscriptionEntitlementChecker() { return _subscriptionEntitlementChecker != null ? _subscriptionEntitlementChecker : _entitlementChecker; } //------------------------------------------------------------------------- /** * Is the server connected to underlying market data API? */ public enum ConnectionStatus { /** Connection active */ CONNECTED, /** Connection not active */ NOT_CONNECTED; } /** * Gets the current connection status. * * @return the status, not null */ public ConnectionStatus getConnectionStatus() { return _connectionStatus; } /** * Sets the connection status. * * @param connectionStatus the status, not null */ public void setConnectionStatus(ConnectionStatus connectionStatus) { _connectionStatus = connectionStatus; s_logger.info("Connection status changed to " + connectionStatus); if (connectionStatus == ConnectionStatus.NOT_CONNECTED) { for (Subscription subscription : getSubscriptions()) { subscription.setHandle(null); } } } //------------------------------------------------------------------------- public void reestablishSubscriptions() { _subscriptionLock.lock(); try { s_logger.warn("Attempting to re-establish subscriptions for {} securities", _securityUniqueId2Subscription.size()); Set<String> securities = _securityUniqueId2Subscription.keySet(); try { Map<String, Object> subscriptions = doSubscribe(securities); if (securities.size() != subscriptions.size()) { s_logger.warn("Attempting to re-establish security subscriptions - have {} securities " + "but only managed to establish subscriptions to {}", securities.size(), subscriptions.size()); } for (Iterator<Map.Entry<String, Subscription>> it = _securityUniqueId2Subscription.entrySet().iterator(); it.hasNext(); ) { final Map.Entry<String, Subscription> entry = it.next(); final Object handle = subscriptions.get(entry.getKey()); if (handle != null) { s_logger.debug("Reconnected to {}", entry.getKey()); entry.getValue().setHandle(handle); } else { s_logger.warn("Couldn't reconnect to {} - removing from list of active subscriptions", entry.getKey()); it.remove(); } } } catch (RuntimeException e) { s_logger.error("Could not reestablish subscription to {}", new Object[] {securities }, e); } } finally { _subscriptionLock.unlock(); } } protected void verifyConnectionOk() { if (getConnectionStatus() == ConnectionStatus.NOT_CONNECTED) { throw new IllegalStateException("Connection to market data API down"); } } @Override public synchronized boolean isRunning() { return getConnectionStatus() == ConnectionStatus.CONNECTED; } protected void startExpirationManager() { getExpirationManager().start(); } @Override public synchronized void start() { if (getConnectionStatus() == ConnectionStatus.NOT_CONNECTED) { connect(); startExpirationManager(); } } protected void stopExpirationManager() { getExpirationManager().stop(); } @Override public synchronized void stop() { if (getConnectionStatus() == ConnectionStatus.CONNECTED) { disconnect(); stopExpirationManager(); } } public synchronized void connect() { if (getConnectionStatus() != ConnectionStatus.NOT_CONNECTED) { throw new IllegalStateException("Can only connect if not connected"); } doConnect(); setConnectionStatus(ConnectionStatus.CONNECTED); } public synchronized void disconnect() { if (getConnectionStatus() != ConnectionStatus.CONNECTED) { throw new IllegalStateException("Can only disconnect if connected"); } doDisconnect(); setConnectionStatus(ConnectionStatus.NOT_CONNECTED); } /** * @param securityUniqueId Security unique ID * @return A {@code LiveDataSpecification} with default normalization rule used. */ public LiveDataSpecification getLiveDataSpecification(String securityUniqueId) { return new LiveDataSpecification( getDefaultNormalizationRuleSetId(), ExternalId.of(getUniqueIdDomain(), securityUniqueId)); } /** * Subscribes to the market data and creates a default distributor. * * @param securityUniqueId Security unique ID * @return Whether the subscription succeeded or failed * @see #getDefaultNormalizationRuleSetId() */ public LiveDataSubscriptionResponse subscribe(String securityUniqueId) { return subscribe(securityUniqueId, false); } /** * Subscribes to the market data and creates a default distributor. * * @param securityUniqueId Security unique ID * @param persistent See {@link MarketDataDistributor#isPersistent()} * @return Whether the subscription succeeded or failed * @see #getDefaultNormalizationRuleSetId() */ public LiveDataSubscriptionResponse subscribe(String securityUniqueId, boolean persistent) { LiveDataSpecification liveDataSpecification = getLiveDataSpecification(securityUniqueId); return subscribe(liveDataSpecification, persistent); } public LiveDataSubscriptionResponse subscribe(LiveDataSpecification liveDataSpecificationFromClient, boolean persistent) { Collection<LiveDataSubscriptionResponse> results = subscribe( Collections.singleton(liveDataSpecificationFromClient), persistent); if (results == null || results.size() != 1) { String errorMsg = "subscribe() did not fulfill its contract to populate map for each live data spec"; return buildErrorMessageResponse(liveDataSpecificationFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg); } LiveDataSubscriptionResponse result = results.iterator().next(); if (!liveDataSpecificationFromClient.equals(result.getRequestedSpecification())) { String errorMsg = "Expected a subscription result for " + liveDataSpecificationFromClient + " but received one for " + result.getRequestedSpecification(); return buildErrorMessageResponse(liveDataSpecificationFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg); } return result; } public Collection<LiveDataSubscriptionResponse> subscribe( Collection<LiveDataSpecification> liveDataSpecificationsFromClient, boolean persistent) { ArgumentChecker.notNull(liveDataSpecificationsFromClient, "Subscriptions to be created"); s_logger.info("Subscribe requested for {}, persistent = {}", liveDataSpecificationsFromClient, persistent); verifyConnectionOk(); Map<ExternalIdBundle, LiveDataSubscriptionResponse> responses = new HashMap<>(); Map<String, Subscription> securityUniqueId2NewSubscription = new HashMap<>(); Map<String, LiveDataSpecification> securityUniqueId2SpecFromClient = new HashMap<>(); _subscriptionLock.lock(); try { final long distributionExpiryTime = System.currentTimeMillis() + getExpirationManager().getTimeoutExtension(); Map<LiveDataSpecification, DistributionSpecification> distrSpecs = getDistributionSpecificationResolver().resolve(liveDataSpecificationsFromClient); for (LiveDataSpecification specFromClient : liveDataSpecificationsFromClient) { // this is the only place where subscribe() can 'partially' fail final DistributionSpecification distributionSpec = distrSpecs.get(specFromClient); if (distributionSpec == null) { s_logger.info("Unable to work out distribution spec for specification " + specFromClient); responses.put(specFromClient.getIdentifiers(), buildErrorMessageResponse(specFromClient, LiveDataSubscriptionResult.NOT_PRESENT, "Unable to work out distribution spec")); continue; } final LiveDataSpecification fullyQualifiedSpec = distributionSpec.getFullyQualifiedLiveDataSpecification(); Subscription subscription = getSubscription(fullyQualifiedSpec); if (subscription != null) { s_logger.info("Already subscribed to {}", fullyQualifiedSpec); subscription.createDistributor(distributionSpec, persistent).setExpiry(distributionExpiryTime); } else { String securityUniqueId = fullyQualifiedSpec.getIdentifier(getUniqueIdDomain()); if (securityUniqueId == null) { String errorMsg = "Qualified spec " + fullyQualifiedSpec + " does not contain ID of domain " + getUniqueIdDomain(); responses.put(specFromClient.getIdentifiers(), buildErrorMessageResponse(specFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg)); continue; } subscription = new Subscription(securityUniqueId, getMarketDataSenderFactory(), getLkvStoreProvider()); securityUniqueId2NewSubscription.put(subscription.getSecurityUniqueId(), subscription); securityUniqueId2SpecFromClient.put(subscription.getSecurityUniqueId(), specFromClient); MarketDataDistributor distributor = subscription.createDistributor(distributionSpec, persistent); distributor.setExpiry(distributionExpiryTime); // PLAT-5958 - make a note of this here so that if another requested live data spec aliases to the same // subscription then we reuse this new subscription rather than creating another _fullyQualifiedSpec2Distributor.put(distributor.getFullyQualifiedLiveDataSpecification(), distributor); s_logger.info("Created subscription for {}: {}", fullyQualifiedSpec, subscription); } responses.put(specFromClient.getIdentifiers(), buildSubscriptionResponse(specFromClient, distributionSpec)); } //Allow checks here, before we do the snapshot or the subscribe checkSubscribe(securityUniqueId2NewSubscription.keySet()); // In some cases, the underlying market data API may not, when the subscription is started, // return a full image of all fields. If so, we need to get the full image explicitly. Collection<String> newSubscriptionsForWhichSnapshotIsRequired = new ArrayList<>(); for (Subscription subscription : securityUniqueId2NewSubscription.values()) { if (snapshotOnSubscriptionStartRequired(subscription)) { newSubscriptionsForWhichSnapshotIsRequired.add(subscription.getSecurityUniqueId()); } } s_logger.info("Subscription snapshot required for {}", newSubscriptionsForWhichSnapshotIsRequired); Map<String, FudgeMsg> snapshots = doSnapshot(newSubscriptionsForWhichSnapshotIsRequired); for (Map.Entry<String, FudgeMsg> snapshot : snapshots.entrySet()) { Subscription subscription = securityUniqueId2NewSubscription.get(snapshot.getKey()); if (snapshot.getValue() != null) { if (snapshot.getValue().hasField(PermissionUtils.LIVE_DATA_PERMISSION_DENIED_FIELD)) { LiveDataSpecification originalSpec = securityUniqueId2SpecFromClient.get(snapshot.getKey()); LiveDataSubscriptionResponse errorRsp = buildErrorMessageResponse( originalSpec, LiveDataSubscriptionResult.NOT_AUTHORIZED, snapshot.getValue().getString(PermissionUtils.LIVE_DATA_PERMISSION_DENIED_FIELD)); responses.put(originalSpec.getIdentifiers(), errorRsp); } else { subscription.initialSnapshotReceived(snapshot.getValue()); } } } // Setup the subscriptions in the underlying data provider. for (Subscription subscription : securityUniqueId2NewSubscription.values()) { // this is necessary so we don't lose any updates immediately after doSubscribe(). See AbstractLiveDataServer#liveDataReceived() // and how it calls AbstractLiveDataServer#getSubscription() _securityUniqueId2Subscription.put(subscription.getSecurityUniqueId(), subscription); } s_logger.info("Creating underlying market data API subscription to {}", securityUniqueId2NewSubscription.keySet()); Map<String, Object> subscriptionHandles = doSubscribe(securityUniqueId2NewSubscription.keySet()); // Set up data structures for (Map.Entry<String, Object> subscriptionHandle : subscriptionHandles.entrySet()) { String securityUniqueId = subscriptionHandle.getKey(); Object handle = subscriptionHandle.getValue(); Subscription subscription = securityUniqueId2NewSubscription.get(securityUniqueId); subscription.setHandle(handle); _currentlyActiveSubscriptions.add(subscription); notifySubscriptionListeners(subscription); } } catch (RuntimeException e) { s_logger.info("Unexpected exception thrown when subscribing. Cleaning up.", e); for (Subscription subscription : securityUniqueId2NewSubscription.values()) { _securityUniqueId2Subscription.remove(subscription.getSecurityUniqueId()); for (MarketDataDistributor distributor : subscription.getDistributors()) { _fullyQualifiedSpec2Distributor.remove(distributor.getFullyQualifiedLiveDataSpecification()); } } _currentlyActiveSubscriptions.removeAll(securityUniqueId2NewSubscription.values()); throw e; } finally { _subscriptionLock.unlock(); } //notify that subscription data structure is completely built subscriptionDone(securityUniqueId2NewSubscription.keySet()); return responses.values(); } private void notifySubscriptionListeners(Subscription subscription) { for (SubscriptionListener listener : _subscriptionListeners) { try { listener.subscribed(subscription); } catch (RuntimeException e) { s_logger.error("Listener " + listener + " subscribe failed", e); } } } /** * Implement necessary data flow logic after subscription is completed * * @param subscriptions the subscriptions, not null */ protected void subscriptionDone(Set<String> subscriptions) { //Do nothing by default } /** * Check that a subscription request is valid. Will be called before any snapshot or subscribe requests for the keys * * @param uniqueIds The unique ids for which a subscribe is being requested */ protected void checkSubscribe(Set<String> uniqueIds) { //Do nothing by default } /** * Returns a snapshot of the requested market data. If the server already subscribes to the market data, the last known value from that subscription is used. Otherwise a snapshot is requested from * the underlying market data API. * * @param liveDataSpecificationsFromClient What snapshot(s) are being requested. Not empty * @return Responses to snapshot requests. Some, or even all, of them might be failures. * @throws RuntimeException If no snapshot could be obtained due to unexpected error. */ public Collection<LiveDataSubscriptionResponse> snapshot(Collection<LiveDataSpecification> liveDataSpecificationsFromClient) { ArgumentChecker.notNull(liveDataSpecificationsFromClient, "Snapshots to be obtained"); s_logger.info("Snapshot requested for {}", liveDataSpecificationsFromClient); verifyConnectionOk(); Collection<LiveDataSubscriptionResponse> responses = new ArrayList<>(); Collection<String> snapshotsToActuallyDo = new ArrayList<>(); Map<String, LiveDataSpecification> securityUniqueId2LiveDataSpecificationFromClient = new HashMap<>(); Map<LiveDataSpecification, DistributionSpecification> resolved = getDistributionSpecificationResolver().resolve(liveDataSpecificationsFromClient); for (LiveDataSpecification liveDataSpecificationFromClient : liveDataSpecificationsFromClient) { DistributionSpecification distributionSpec = resolved.get(liveDataSpecificationFromClient); LiveDataSpecification fullyQualifiedSpec = distributionSpec.getFullyQualifiedLiveDataSpecification(); MarketDataDistributor currentlyActiveDistributor = getMarketDataDistributor(distributionSpec); if (currentlyActiveDistributor != null) { if (currentlyActiveDistributor.getSnapshot() != null) { //NOTE simon 28/11/2011: We presume that all the fields were provided in one go, all or nothing. s_logger.debug("Able to satisfy {} from existing LKV", liveDataSpecificationFromClient); LiveDataValueUpdateBean snapshot = currentlyActiveDistributor.getSnapshot(); responses.add(buildSnapshotResponse(liveDataSpecificationFromClient, snapshot)); continue; } else if (canSatisfySnapshotFromEmptySubscription(currentlyActiveDistributor)) { //BBG-91 - don't requery when an existing subscription indicates that the snapshot will fail s_logger.debug("Able to satisfy failed snapshot {} from existing LKV", liveDataSpecificationFromClient); String errorMsg = "Existing subscription for " + currentlyActiveDistributor.getDistributionSpec().getMarketDataId() + " failed to retrieve a snapshot. Perhaps required fields are unavailable."; responses.add(buildErrorMessageResponse(liveDataSpecificationFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg)); continue; } else { s_logger.debug("Can't use existing subscription to satisfy {} from existing LKV", liveDataSpecificationFromClient); } } String securityUniqueId = fullyQualifiedSpec.getIdentifier(getUniqueIdDomain()); if (securityUniqueId == null) { String errorMsg = "Qualified spec " + fullyQualifiedSpec + " does not contain ID of domain " + getUniqueIdDomain(); responses.add(buildErrorMessageResponse(liveDataSpecificationFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg)); continue; } snapshotsToActuallyDo.add(securityUniqueId); securityUniqueId2LiveDataSpecificationFromClient.put(securityUniqueId, liveDataSpecificationFromClient); } s_logger.debug("Need to actually snapshot {}", snapshotsToActuallyDo); Map<String, FudgeMsg> snapshots = doSnapshot(snapshotsToActuallyDo); for (Map.Entry<String, FudgeMsg> snapshotEntry : snapshots.entrySet()) { String securityUniqueId = snapshotEntry.getKey(); FudgeMsg msg = snapshotEntry.getValue(); LiveDataSpecification liveDataSpecFromClient = securityUniqueId2LiveDataSpecificationFromClient.get(securityUniqueId); DistributionSpecification distributionSpec = resolved.get(liveDataSpecFromClient); FudgeMsg normalizedMsg = distributionSpec.getNormalizedMessage(msg, securityUniqueId); if (normalizedMsg == null) { String errorMsg = "When snapshot for " + securityUniqueId + " was run through normalization, the message disappeared. " + " This indicates there are buggy normalization rules in place, or that buggy (or unexpected) data was" + " received from the underlying market data API. Check your normalization rules. Raw, unnormalized msg = " + msg; responses.add(buildErrorMessageResponse(liveDataSpecFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg)); continue; } LiveDataValueUpdateBean snapshot = new LiveDataValueUpdateBean(0, distributionSpec.getFullyQualifiedLiveDataSpecification(), normalizedMsg); responses.add(buildSnapshotResponse(liveDataSpecFromClient, snapshot)); } return responses; } /** * If you want to force a snapshot - i.e., always a request a snapshot from the underlying API - you can use this method. * * @param securityUniqueId Security unique ID * @return The snapshot */ public FudgeMsg doSnapshot(String securityUniqueId) { Map<String, FudgeMsg> snapshots = doSnapshot(Collections.singleton(securityUniqueId)); FudgeMsg snapshot = snapshots.get(securityUniqueId); if (snapshot == null) { throw new OpenGammaRuntimeException("doSnapshot() did not fulfill its contract to populate map for each unique ID"); } return snapshot; } //------------------------------------------------------------------------- /** * Processes a market data subscription request by going through the steps of resolution, entitlement check, and subscription. * * @param subscriptionRequest the request from the client telling what to subscribe to, not null * @return the response sent back to the client of this server, not null */ public LiveDataSubscriptionResponseMsg subscriptionRequestMade(LiveDataSubscriptionRequest subscriptionRequest) { try { return subscriptionRequestMadeImpl(subscriptionRequest); } catch (Exception ex) { s_logger.error("Failed to subscribe to " + subscriptionRequest, ex); ArrayList<LiveDataSubscriptionResponse> responses = new ArrayList<>(); for (LiveDataSpecification requestedSpecification : subscriptionRequest.getSpecifications()) { responses.add(buildErrorResponse(requestedSpecification, ex)); } return new LiveDataSubscriptionResponseMsg(subscriptionRequest.getUser(), responses); } } /** * Handles a subscription request. * * @param subscriptionRequest the request, not null * @return the response, not null */ protected LiveDataSubscriptionResponseMsg subscriptionRequestMadeImpl(LiveDataSubscriptionRequest subscriptionRequest) { final boolean persistent = subscriptionRequest.getType().equals(SubscriptionType.PERSISTENT); final ArrayList<LiveDataSubscriptionResponse> responses = new ArrayList<>(); // build and check the distribution specifications Map<LiveDataSpecification, DistributionSpecification> distributionSpecifications = getDistributionSpecificationResolver().resolve( subscriptionRequest.getSpecifications()); ArrayList<LiveDataSpecification> distributable = new ArrayList<>(); for (LiveDataSpecification requestedSpecification : subscriptionRequest.getSpecifications()) { try { // Check that this spec can be found DistributionSpecification spec = distributionSpecifications.get(requestedSpecification); if (spec == null) { String errorMsg = "Could not build distribution specification for " + requestedSpecification; s_logger.debug(errorMsg); responses.add(buildErrorMessageResponse(requestedSpecification, LiveDataSubscriptionResult.NOT_PRESENT, errorMsg)); } else { distributable.add(requestedSpecification); } } catch (Exception ex) { s_logger.error("Failed to subscribe to " + requestedSpecification, ex); responses.add(buildErrorResponse(requestedSpecification, ex)); } } // check entitlement and sort into snapshots/subscriptions ArrayList<LiveDataSpecification> snapshots = new ArrayList<>(); ArrayList<LiveDataSpecification> subscriptions = new ArrayList<>(); Map<LiveDataSpecification, Boolean> entitled = getSubscriptionEntitlementChecker().isEntitled(subscriptionRequest.getUser(), distributable); for (Entry<LiveDataSpecification, Boolean> entry : entitled.entrySet()) { LiveDataSpecification requestedSpecification = entry.getKey(); try { Boolean entitlement = entry.getValue(); if (!entitlement) { String errorMsg = subscriptionRequest.getUser() + " is not entitled to " + requestedSpecification; s_logger.info(errorMsg); responses.add(buildErrorMessageResponse(requestedSpecification, LiveDataSubscriptionResult.NOT_AUTHORIZED, errorMsg)); continue; } // Pass to the right bucket by type if (subscriptionRequest.getType() == SubscriptionType.SNAPSHOT) { snapshots.add(requestedSpecification); } else { subscriptions.add(requestedSpecification); } } catch (Exception ex) { s_logger.error("Failed to subscribe to " + requestedSpecification, ex); responses.add(buildErrorResponse(requestedSpecification, ex)); } } // handle snapshots if (!snapshots.isEmpty()) { try { responses.addAll(snapshot(snapshots)); } catch (Exception ex) { s_logger.error("Error obtaining snapshots for {}: {}", snapshots, ex.getMessage()); if (s_logger.isDebugEnabled()) { s_logger.debug("Underlying exception in snapshot error " + snapshots, ex); } // REVIEW kirk 2012-07-20 -- This doesn't really look like an InternalError, // but we have no way to discriminate in the response from doSnapshot at the moment. for (LiveDataSpecification requestedSpecification : snapshots) { String errorMsg = "Problem obtaining snapshot: " + ex.getMessage(); responses.add(buildErrorMessageResponse(requestedSpecification, LiveDataSubscriptionResult.INTERNAL_ERROR, errorMsg)); } } } // handle subscriptions if (!subscriptions.isEmpty()) { try { responses.addAll(subscribe(subscriptions, persistent)); } catch (Exception ex) { s_logger.error("Error obtaining subscriptions for {}: {}", subscriptions, (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getName())); if (s_logger.isDebugEnabled()) { s_logger.debug("Underlying exception in subscription error " + subscriptions, ex); } for (LiveDataSpecification requestedSpecification : subscriptions) { responses.add(buildErrorResponse(requestedSpecification, ex)); } } } return new LiveDataSubscriptionResponseMsg(subscriptionRequest.getUser(), responses); } //------------------------------------------------------------------------- /** * Unsubscribes from market data. All distributors related to that subscription will be stopped. * * @param securityUniqueId Security unique ID * @return true if a market data subscription was actually removed. false otherwise. */ public boolean unsubscribe(String securityUniqueId) { Subscription sub = getSubscription(securityUniqueId); if (sub == null) { return false; } return unsubscribe(sub); } /** * Unsubscribes from market data. All distributors related to that subscription will be stopped. * * @param subscription What to unsubscribe from * @return true if a market data subscription was actually removed. false otherwise. */ public boolean unsubscribe(Subscription subscription) { ArgumentChecker.notNull(subscription, "Subscription"); verifyConnectionOk(); boolean actuallyUnsubscribed = false; _subscriptionLock.lock(); try { if (isSubscribedTo(subscription)) { s_logger.info("Unsubscribing from {}", subscription); actuallyUnsubscribed = true; Object subscriptionHandle = subscription.getHandle(); if (subscriptionHandle != null) { doUnsubscribe(Collections.singleton(subscriptionHandle)); // todo, optimize to use batch } _currentlyActiveSubscriptions.remove(subscription); _securityUniqueId2Subscription.remove(subscription .getSecurityUniqueId()); for (MarketDataDistributor distributor : subscription.getDistributors()) { _fullyQualifiedSpec2Distributor.remove(distributor.getFullyQualifiedLiveDataSpecification()); } subscription.removeAllDistributors(); for (SubscriptionListener listener : _subscriptionListeners) { try { listener.unsubscribed(subscription); } catch (RuntimeException e) { s_logger.error("Listener unsubscribe failed", e); } } s_logger.info("Unsubscribed from {}", subscription); } else { s_logger.warn("Received unsubscription request for non-active subscription: {}", subscription); } } finally { _subscriptionLock.unlock(); } return actuallyUnsubscribed; } /** * Stops a market data distributor. If the distributor is persistent, this call will be a no-op. If you want to stop a persistent distributor, make it non-persistent first. * <p> * If the subscription to which the distributor belongs no longer has any active distributors after this, that subscription will be deleted. * * @param distributor The distributor to stop * @return true if a distributor was actually stopped. false otherwise. */ public boolean stopDistributor(MarketDataDistributor distributor) { ArgumentChecker.notNull(distributor, "Distributor"); _subscriptionLock.lock(); try { MarketDataDistributor realDistributor = getMarketDataDistributor(distributor.getDistributionSpec()); if (realDistributor != distributor) { return false; } if (distributor.isPersistent()) { return false; } distributor.getSubscription().removeDistributor(distributor); _fullyQualifiedSpec2Distributor.remove(distributor.getFullyQualifiedLiveDataSpecification()); if (distributor.getSubscription().getDistributors().isEmpty()) { unsubscribe(distributor.getSubscription()); } } finally { _subscriptionLock.unlock(); } return true; } /** * Stops any expired, non-persistent, market data distributors. * <p> * This holds the subscription lock for its duration. Checking each individual distributor for expiry and then calling {@link #stopDistributor} may incorrectly stop the distribution if another * thread is currently subscribing to it. * <p> * This is normally called by the expiration manager. * * @return the number of expired distributors that were stopped */ public int expireSubscriptions() { int expired = 0; _subscriptionLock.lock(); try { for (MarketDataDistributor distributor : new ArrayList<>(_fullyQualifiedSpec2Distributor.values())) { if (distributor.hasExpired()) { if (stopDistributor(distributor)) { expired++; } } } } finally { _subscriptionLock.unlock(); } return expired; } public boolean isSubscribedTo(String securityUniqueId) { return _securityUniqueId2Subscription.containsKey(securityUniqueId); } public boolean isSubscribedTo(LiveDataSpecification fullyQualifiedSpec) { _subscriptionLock.lock(); try { return _fullyQualifiedSpec2Distributor.containsKey(fullyQualifiedSpec); } finally { _subscriptionLock.unlock(); } } public boolean isSubscribedTo(Subscription subscription) { return getSubscriptions().contains(subscription); } public void liveDataReceived(String securityUniqueId, FudgeMsg liveDataFields) { s_logger.debug("Live data received: {}", liveDataFields); _numMarketDataUpdatesReceived.incrementAndGet(); if (_performanceCounter != null) { _performanceCounter.hit(); } Subscription subscription = getSubscription(securityUniqueId); if (subscription == null) { // REVIEW kirk 2013-04-26 -- Should this really be a WARN? I believe some gateway systems // handle unsubscribes asynchronously so it's totally valid to get a few ticks after // unsubscribe has pulled it out of the subscription list. s_logger.warn("Unexpectedly got data for security unique ID {} - " + "no subscription is held for this data (has it recently expired?)", securityUniqueId); return; } subscription.liveDataReceived(liveDataFields); } public Set<String> getActiveDistributionSpecs() { Set<String> subscriptions = new HashSet<>(); for (Subscription subscription : getSubscriptions()) { for (DistributionSpecification distributionSpec : subscription.getDistributionSpecifications()) { subscriptions.add(distributionSpec.toString()); } } return subscriptions; } public Set<String> getActiveSubscriptionIds() { Set<String> subscriptions = new HashSet<>(); for (Subscription subscription : getSubscriptions()) { subscriptions.add(subscription.getSecurityUniqueId()); } return subscriptions; } public int getNumActiveSubscriptions() { return getSubscriptions().size(); } public long getNumMarketDataUpdatesReceived() { return _numMarketDataUpdatesReceived.get(); } /** * @return The approximate rate of live data updates received, or -1 if tracking is disabled */ public double getNumLiveDataUpdatesSentPerSecondOverLastMinute() { return _performanceCounter == null ? -1.0 : _performanceCounter.getHitsPerSecond(); } public Set<Subscription> getSubscriptions() { _subscriptionLock.lock(); try { return new HashSet<>(_currentlyActiveSubscriptions); } finally { _subscriptionLock.unlock(); } } public Subscription getSubscription(LiveDataSpecification fullyQualifiedSpec) { MarketDataDistributor distributor = getMarketDataDistributor(fullyQualifiedSpec); if (distributor == null) { return null; } return distributor.getSubscription(); } public Subscription getSubscription(String securityUniqueId) { //NOTE: don't need lock here, map is safe, and this operation isn't really atomic anyway return _securityUniqueId2Subscription.get(securityUniqueId); } public MarketDataDistributor getMarketDataDistributor(DistributionSpecification distributionSpec) { Subscription subscription = getSubscription(distributionSpec.getFullyQualifiedLiveDataSpecification()); if (subscription == null) { return null; } return subscription.getMarketDataDistributor(distributionSpec); } public Map<LiveDataSpecification, MarketDataDistributor> getMarketDataDistributors(Collection<LiveDataSpecification> fullyQualifiedSpecs) { //NOTE: this is not much (if any) faster here, but for subclasses it can be _subscriptionLock.lock(); try { HashMap<LiveDataSpecification, MarketDataDistributor> hashMap = new HashMap<>(); for (LiveDataSpecification liveDataSpecification : fullyQualifiedSpecs) { hashMap.put(liveDataSpecification, _fullyQualifiedSpec2Distributor.get(liveDataSpecification)); } return hashMap; } finally { _subscriptionLock.unlock(); } } public MarketDataDistributor getMarketDataDistributor(LiveDataSpecification fullyQualifiedSpec) { _subscriptionLock.lock(); try { return _fullyQualifiedSpec2Distributor.get(fullyQualifiedSpec); } finally { _subscriptionLock.unlock(); } } /** * This method is mainly useful in tests. * * @param securityUniqueId Security unique ID * @return The only market data distributor associated with the security unique ID. * @throws OpenGammaRuntimeException If there is no distributor associated with the given {@code securityUniqueId}, or if there is more than 1 such distributor. */ public MarketDataDistributor getMarketDataDistributor(String securityUniqueId) { Subscription sub = getSubscription(securityUniqueId); if (sub == null) { throw new OpenGammaRuntimeException("Subscription " + securityUniqueId + " not found"); } Collection<MarketDataDistributor> distributors = sub.getDistributors(); if (distributors.size() != 1) { throw new OpenGammaRuntimeException(distributors.size() + " distributors found for subscription " + securityUniqueId); } return distributors.iterator().next(); } //------------------------------------------------------------------------- /** * Helper to build an error response. * * @param liveDataSpecificationFromClient the original specification * @param throwable the error, not null * @return the response, not null */ protected LiveDataSubscriptionResponse buildErrorResponse( LiveDataSpecification liveDataSpecificationFromClient, Throwable throwable) { return buildErrorMessageResponse(liveDataSpecificationFromClient, LiveDataSubscriptionResult.INTERNAL_ERROR, throwable.toString()); } /** * Helper to build an error response. * * @param liveDataSpecificationFromClient the original specification * @param result the result enum * @param message the error message, not null * @return the response, not null */ protected LiveDataSubscriptionResponse buildErrorMessageResponse( LiveDataSpecification liveDataSpecificationFromClient, LiveDataSubscriptionResult result, String message) { return new LiveDataSubscriptionResponse(liveDataSpecificationFromClient, result, message, null, null, null); } /** * Helper to build a snapshot response. * * @param liveDataSpecificationFromClient the original specification * @param snapshot the snapshot, not null * @return the response, not null */ protected LiveDataSubscriptionResponse buildSnapshotResponse( LiveDataSpecification liveDataSpecificationFromClient, LiveDataValueUpdateBean snapshot) { return new LiveDataSubscriptionResponse( liveDataSpecificationFromClient, LiveDataSubscriptionResult.SUCCESS, null, snapshot.getSpecification(), null, snapshot); } /** * Helper to build a subscription response. * * @param liveDataSpecificationFromClient the original specification * @param distributionSpec the subscription, not null * @return the response, not null */ protected LiveDataSubscriptionResponse buildSubscriptionResponse( LiveDataSpecification liveDataSpecificationFromClient, DistributionSpecification distributionSpec) { return new LiveDataSubscriptionResponse( liveDataSpecificationFromClient, LiveDataSubscriptionResult.SUCCESS, null, distributionSpec.getFullyQualifiedLiveDataSpecification(), distributionSpec.getJmsTopic(), null); } @Override public SubscriptionTrace getSubscriptionTrace(String identifier) { if (_securityUniqueId2Subscription.containsKey(identifier)) { Subscription sub = _securityUniqueId2Subscription.get(identifier); Date creationTime = sub.getCreationTime(); Instant instant = Instant.ofEpochMilli(creationTime.getTime()); Set<DistributorTrace> distributors = new HashSet<>(); for (MarketDataDistributor distributor : sub.getDistributors()) { distributors.add(new DistributorTrace( distributor.getDistributionSpec().getJmsTopic(), Instant.ofEpochMilli(distributor.getExpiry()).toString(), distributor.hasExpired(), distributor.isPersistent(), distributor.getNumMessagesSent())); } return new SubscriptionTrace(identifier, instant.toString(), distributors, sub.getLiveDataHistory().getLastKnownValues().toString()); } else { return new SubscriptionTrace(identifier); } } }