/** * 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.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.fudgemsg.FudgeContext; import org.fudgemsg.FudgeMsg; import org.fudgemsg.FudgeMsgEnvelope; import org.fudgemsg.mapping.FudgeDeserializer; import org.fudgemsg.mapping.FudgeSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.livedata.LiveDataListener; import com.opengamma.livedata.LiveDataSpecification; import com.opengamma.livedata.LiveDataValueUpdateBean; import com.opengamma.livedata.LiveDataValueUpdateBeanFudgeBuilder; import com.opengamma.livedata.UserPrincipal; 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.transport.FudgeMessageReceiver; import com.opengamma.transport.FudgeRequestSender; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.PoolExecutor; import com.opengamma.util.PublicAPI; import com.opengamma.util.fudgemsg.OpenGammaFudgeContext; /** * A client that talks to a remote LiveData server through an unspecified protocol. Possibilities are JMS, Fudge, direct socket connection, and so on. */ @PublicAPI public class DistributedLiveDataClient extends AbstractLiveDataClient implements FudgeMessageReceiver { private static final Logger s_logger = LoggerFactory.getLogger(DistributedLiveDataClient.class); // Injected Inputs: private final FudgeContext _fudgeContext; private final FudgeRequestSender _subscriptionRequestSender; private final DistributedEntitlementChecker _entitlementChecker; /** * An exception will be thrown when doing a snapshot if no reply is received from the server within this time. Milliseconds. */ private static final long TIMEOUT = 1800000; public DistributedLiveDataClient(FudgeRequestSender subscriptionRequestSender, FudgeRequestSender entitlementRequestSender) { this(subscriptionRequestSender, entitlementRequestSender, OpenGammaFudgeContext.getInstance()); } public DistributedLiveDataClient(FudgeRequestSender subscriptionRequestSender, FudgeRequestSender entitlementRequestSender, FudgeContext fudgeContext) { ArgumentChecker.notNull(subscriptionRequestSender, "Subscription request sender"); ArgumentChecker.notNull(entitlementRequestSender, "Entitlement request sender"); ArgumentChecker.notNull(fudgeContext, "Fudge Context"); _subscriptionRequestSender = subscriptionRequestSender; _fudgeContext = fudgeContext; _entitlementChecker = new DistributedEntitlementChecker(entitlementRequestSender, fudgeContext); } /** * @return the subscriptionRequestSender */ public FudgeRequestSender getSubscriptionRequestSender() { return _subscriptionRequestSender; } /** * @return the fudgeContext */ @Override public FudgeContext getFudgeContext() { return _fudgeContext; } @Override protected void cancelPublication(LiveDataSpecification fullyQualifiedSpecification) { s_logger.info("Request made to cancel publication of {}", fullyQualifiedSpecification); // TODO kirk 2009-10-28 -- This should handle an unsubscription request. For now, // however, we can just make do with allowing the heartbeat to time out. } @Override protected void handleSubscriptionRequest(Collection<SubscriptionHandle> subHandles) { ArgumentChecker.notEmpty(subHandles, "Subscription handle collection"); // Determine common user and subscription type UserPrincipal user = null; SubscriptionType type = null; List<LiveDataSpecification> specs = Lists.newArrayListWithCapacity(subHandles.size()); for (SubscriptionHandle subHandle : subHandles) { specs.add(subHandle.getRequestedSpecification()); if (user == null) { user = subHandle.getUser(); } else if (!user.equals(subHandle.getUser())) { throw new OpenGammaRuntimeException("Not all usernames are equal"); } if (type == null) { type = subHandle.getSubscriptionType(); } else if (!type.equals(subHandle.getSubscriptionType())) { throw new OpenGammaRuntimeException("Not all subscription types are equal"); } } // Build request message LiveDataSubscriptionRequest subReqMessage = new LiveDataSubscriptionRequest(user, type, specs); FudgeMsg requestMessage = subReqMessage.toFudgeMsg(new FudgeSerializer(getFudgeContext())); // Build response receiver FudgeMessageReceiver responseReceiver; if (type == SubscriptionType.SNAPSHOT) { responseReceiver = new SnapshotResponseReceiver(subHandles); } else { responseReceiver = new TopicBasedSubscriptionResponseReceiver(subHandles); } getSubscriptionRequestSender().sendRequest(requestMessage, responseReceiver); } /** * Common functionality for receiving subscription responses from the server. */ private abstract class AbstractSubscriptionResponseReceiver implements FudgeMessageReceiver { private final Map<LiveDataSpecification, SubscriptionHandle> _spec2SubHandle; private final Map<SubscriptionHandle, LiveDataSubscriptionResponse> _successResponses = new HashMap<>(); private final Map<SubscriptionHandle, LiveDataSubscriptionResponse> _failedResponses = new HashMap<>(); private UserPrincipal _user; public AbstractSubscriptionResponseReceiver(Collection<SubscriptionHandle> subHandles) { _spec2SubHandle = new HashMap<>(); for (SubscriptionHandle subHandle : subHandles) { _spec2SubHandle.put(subHandle.getRequestedSpecification(), subHandle); } } public UserPrincipal getUser() { return _user; } public void setUser(UserPrincipal user) { _user = user; } public Map<LiveDataSpecification, SubscriptionHandle> getSpec2SubHandle() { return _spec2SubHandle; } public Map<SubscriptionHandle, LiveDataSubscriptionResponse> getSuccessResponses() { return _successResponses; } public Map<SubscriptionHandle, LiveDataSubscriptionResponse> getFailedResponses() { return _failedResponses; } @Override public void messageReceived(FudgeContext fudgeContext, FudgeMsgEnvelope envelope) { final PoolExecutor.CompletionListener<Void> callback = new PoolExecutor.CompletionListener<Void>() { @Override public void success(final Void result) { // No-op } @Override public void failure(final Throwable error) { s_logger.error("Failed to process response message", error); for (SubscriptionHandle handle : getSpec2SubHandle().values()) { if (handle.getSubscriptionType() != SubscriptionType.SNAPSHOT) { subscriptionRequestFailed(handle, new LiveDataSubscriptionResponse(handle.getRequestedSpecification(), LiveDataSubscriptionResult.INTERNAL_ERROR, error.toString(), null, null, null)); } } } }; try { if ((envelope == null) || (envelope.getMessage() == null)) { throw new OpenGammaRuntimeException("Got a message that can't be deserialized from a Fudge message."); } FudgeMsg msg = envelope.getMessage(); LiveDataSubscriptionResponseMsg responseMessage = LiveDataSubscriptionResponseMsg.fromFudgeMsg(new FudgeDeserializer(getFudgeContext()), msg); if (responseMessage.getResponses().isEmpty()) { throw new OpenGammaRuntimeException("Got empty subscription response " + responseMessage); } messageReceived(responseMessage, callback); } catch (Exception e) { callback.failure(e); } } private void messageReceived(LiveDataSubscriptionResponseMsg responseMessage, final PoolExecutor.CompletionListener<Void> callback) { parseResponse(responseMessage); processResponse(new PoolExecutor.CompletionListener<Void>() { @Override public void success(final Void result) { try { sendResponse(); } catch (Throwable t) { callback.failure(t); return; } callback.success(null); } @Override public void failure(final Throwable error) { callback.failure(error); } }); } private void parseResponse(LiveDataSubscriptionResponseMsg responseMessage) { for (LiveDataSubscriptionResponse response : responseMessage.getResponses()) { SubscriptionHandle handle = getSpec2SubHandle().get(response.getRequestedSpecification()); if (handle == null) { throw new OpenGammaRuntimeException("Could not find handle corresponding to request " + response.getRequestedSpecification()); } if (getUser() != null && !getUser().equals(handle.getUser())) { throw new OpenGammaRuntimeException("Not all usernames are equal"); } setUser(handle.getUser()); if (response.getSubscriptionResult() == LiveDataSubscriptionResult.SUCCESS) { getSuccessResponses().put(handle, response); } else { getFailedResponses().put(handle, response); } } } protected void processResponse(final PoolExecutor.CompletionListener<Void> callback) { callback.success(null); } protected void sendResponse() { Map<SubscriptionHandle, LiveDataSubscriptionResponse> responses = new HashMap<>(); responses.putAll(getSuccessResponses()); responses.putAll(getFailedResponses()); int total = responses.size(); s_logger.info("{} subscription responses received", total); Map<LiveDataListener, Collection<LiveDataSubscriptionResponse>> batch = new HashMap<>(); for (Map.Entry<SubscriptionHandle, LiveDataSubscriptionResponse> successEntry : responses.entrySet()) { SubscriptionHandle handle = successEntry.getKey(); LiveDataSubscriptionResponse response = successEntry.getValue(); Collection<LiveDataSubscriptionResponse> responseBatch = batch.get(handle.getListener()); if (responseBatch == null) { responseBatch = new LinkedList<>(); batch.put(handle.getListener(), responseBatch); } responseBatch.add(response); } for (Map.Entry<LiveDataListener, Collection<LiveDataSubscriptionResponse>> batchEntry : batch.entrySet()) { batchEntry.getKey().subscriptionResultsReceived(batchEntry.getValue()); } } } /** * Some market data requests are snapshot requests; this means that they do not require a JMS subscription. */ private class SnapshotResponseReceiver extends AbstractSubscriptionResponseReceiver { public SnapshotResponseReceiver(Collection<SubscriptionHandle> subHandles) { super(subHandles); } } /** * Some market data requests are non-snapshot requests where market data is continuously read from a JMS topic; this means they require a JMS subscription. * <p> * As per LIV-19, after we've subscribed to the market data (and started getting deltas), we do a snapshot to make sure we get a full initial image of the data. Things are done in this order (first * subscribe, then snapshot) so we don't lose any ticks. See LIV-19. */ private class TopicBasedSubscriptionResponseReceiver extends AbstractSubscriptionResponseReceiver { public TopicBasedSubscriptionResponseReceiver(Collection<SubscriptionHandle> subHandles) { super(subHandles); } @Override protected void processResponse(final PoolExecutor.CompletionListener<Void> result) { try { final PoolExecutor.CompletionListener<Void> callback = new PoolExecutor.CompletionListener<Void>() { @Override public void success(final Void reserved) { result.success(null); } @Override public void failure(final Throwable error) { try { s_logger.error("Failed to process subscription response", error); // This is unexpected. Fail everything. for (LiveDataSubscriptionResponse response : getSuccessResponses().values()) { response.setSubscriptionResult(LiveDataSubscriptionResult.INTERNAL_ERROR); response.setUserMessage(error.toString()); } getFailedResponses().putAll(getSuccessResponses()); getSuccessResponses().clear(); } catch (Throwable e) { result.failure(e); return; } result.success(null); } }; try { // Phase 1. Create a subscription to market data topic startReceivingTicks(); // Phase 2. After we've subscribed to the market data (and started getting deltas), snapshot it snapshot(callback); } catch (Throwable e) { callback.failure(e); } } catch (Throwable e) { result.failure(e); } } private void startReceivingTicks() { Map<SubscriptionHandle, LiveDataSubscriptionResponse> resps = getSuccessResponses(); // tick distribution specifications can be duplicated, only pass each down once to startReceivingTicks() Collection<String> distributionSpecs = new HashSet<>(resps.size()); for (Map.Entry<SubscriptionHandle, LiveDataSubscriptionResponse> entry : resps.entrySet()) { DistributedLiveDataClient.this.subscriptionStartingToReceiveTicks(entry.getKey(), entry.getValue()); distributionSpecs.add(entry.getValue().getTickDistributionSpecification()); } DistributedLiveDataClient.this.startReceivingTicks(distributionSpecs); } private void snapshot(final PoolExecutor.CompletionListener<Void> callback) { ArrayList<LiveDataSpecification> successLiveDataSpecs = new ArrayList<>(); for (LiveDataSubscriptionResponse response : getSuccessResponses().values()) { successLiveDataSpecs.add(response.getRequestedSpecification()); } DistributedLiveDataClient.this.snapshot(getUser(), successLiveDataSpecs, TIMEOUT, new PoolExecutor.CompletionListener<Collection<LiveDataSubscriptionResponse>>() { @Override public void success(final Collection<LiveDataSubscriptionResponse> snapshots) { try { for (LiveDataSubscriptionResponse response : snapshots) { final SubscriptionHandle handle = getSpec2SubHandle().get(response.getRequestedSpecification()); if (handle == null) { throw new OpenGammaRuntimeException("Could not find handle corresponding to request " + response.getRequestedSpecification()); } // could be that even though subscription to the JMS topic (phase 1) succeeded, snapshot (phase 2) for some reason failed. // since phase 1 already validated everything, this should mainly happen when user permissions are modified // in the sub-second interval between phases 1 and 2! // Not so. In fact for a system like Bloomberg, because of the lag in subscription, the LiveDataServer // may in fact think that you can successfully subscribe, but then when the snapshot is requested we detect // that it's not a valid code. So this is the time that we've actually poked the underlying data provider // to check. // In addition, it may be that for a FireHose server we didn't have the full SoW on the initial request // but now we do. if (response.getSubscriptionResult() == LiveDataSubscriptionResult.SUCCESS) { handle.addSnapshotOnHold(response.getSnapshot()); } else { getSuccessResponses().remove(handle); getFailedResponses().put(handle, response); } } } catch (Throwable t) { callback.failure(t); return; } callback.success(null); } @Override public void failure(final Throwable error) { callback.failure(error); } }); } @Override protected void sendResponse() { super.sendResponse(); for (Map.Entry<SubscriptionHandle, LiveDataSubscriptionResponse> successEntry : getSuccessResponses().entrySet()) { SubscriptionHandle handle = successEntry.getKey(); LiveDataSubscriptionResponse response = successEntry.getValue(); subscriptionRequestSatisfied(handle, response); } for (Map.Entry<SubscriptionHandle, LiveDataSubscriptionResponse> failedEntry : getFailedResponses().entrySet()) { SubscriptionHandle handle = failedEntry.getKey(); LiveDataSubscriptionResponse response = failedEntry.getValue(); subscriptionRequestFailed(handle, response); // this is here just to clean up. It's safe to call stopReceivingTicks() // even if no JMS subscription actually exists. stopReceivingTicks(response.getTickDistributionSpecification()); } } } /** * @param tickDistributionSpecification JMS topic name */ public void startReceivingTicks(Collection<String> tickDistributionSpecification) { // Default no-op. } public void stopReceivingTicks(String tickDistributionSpecification) { // Default no-op. } // REVIEW kirk 2009-10-28 -- This is just a braindead way of getting ticks to come in // until we can get a handle on the construction of receivers based on responses. @Override public void messageReceived(FudgeContext fudgeContext, FudgeMsgEnvelope msgEnvelope) { FudgeMsg fudgeMsg = msgEnvelope.getMessage(); LiveDataValueUpdateBean update = LiveDataValueUpdateBeanFudgeBuilder.fromFudgeMsg(new FudgeDeserializer(fudgeContext), fudgeMsg); valueUpdate(update); } @Override public Map<LiveDataSpecification, Boolean> isEntitled(UserPrincipal user, Collection<LiveDataSpecification> requestedSpecifications) { return _entitlementChecker.isEntitled(user, requestedSpecifications); } @Override public boolean isEntitled(UserPrincipal user, LiveDataSpecification requestedSpecification) { return _entitlementChecker.isEntitled(user, requestedSpecification); } }