/** * 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.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.jms.JMSException; import javax.jms.MessageConsumer; import javax.jms.Session; import javax.jms.Topic; import org.fudgemsg.FudgeContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jms.support.JmsUtils; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.transport.FudgeRequestSender; import com.opengamma.transport.jms.JmsByteArrayMessageDispatcher; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.PublicAPI; import com.opengamma.util.jms.JmsConnector; /** * An ActiveMQ LiveData client. Behaves the same as {@link JmsLiveDataClient} except * that subscriptions are made to Composite Destinations in order to reduce * the time made to make subscriptions. * * Currently subscriptions are only removed when none of the block is in use any more, this could be improved later. */ @PublicAPI public class ActiveMQLiveDataClient extends JmsLiveDataClient { //TODO: Could migrate to individual/smaller subscriptions in background private static final Logger s_logger = LoggerFactory.getLogger(ActiveMQLiveDataClient.class); public ActiveMQLiveDataClient(FudgeRequestSender subscriptionRequestSender, FudgeRequestSender entitlementRequestSender, JmsConnector jmsConnector) { super(subscriptionRequestSender, entitlementRequestSender, jmsConnector); } public ActiveMQLiveDataClient(FudgeRequestSender subscriptionRequestSender, FudgeRequestSender entitlementRequestSender, JmsConnector jmsConnector, FudgeContext fudgeContext, int maxSessions) { super(subscriptionRequestSender, entitlementRequestSender, jmsConnector, fudgeContext, maxSessions); } public ActiveMQLiveDataClient(FudgeRequestSender subscriptionRequestSender, FudgeRequestSender entitlementRequestSender, JmsConnector jmsConnector, FudgeContext fudgeContext) { super(subscriptionRequestSender, entitlementRequestSender, jmsConnector, fudgeContext); } private final Map<String, ConsumerRecord> _messageConsumersBySpec = new ConcurrentHashMap<String, ConsumerRecord>(); private class ConsumerRecord { private final MessageConsumer _consumer; private final Set<String> _allReceiving; private final Set<String> _receiving; public ConsumerRecord(MessageConsumer consumer, Collection<String> receiving) { super(); _consumer = consumer; _receiving = new HashSet<String>(receiving); _allReceiving = Collections.unmodifiableSet(new HashSet<String>(receiving)); } public MessageConsumer getConsumer() { return _consumer; } public Set<String> getReceiving() { return _receiving; } public Set<String> getAllReceiving() { return _allReceiving; } } @Override protected Map<String, Runnable> startReceivingTicks(List<String> specs, Session session, JmsByteArrayMessageDispatcher jmsDispatcher) { Map<String, Runnable> ret = new HashMap<String, Runnable>(); if (specs.isEmpty()) { return ret; } for (String spec : specs) { ConsumerRecord record = _messageConsumersBySpec.get(spec); if (record != null) { //NOTE: could be on the wrong session, but we don't touch it record.getReceiving().add(spec); ret.put(spec, getCloseAction(spec, record)); } } SetView<String> remaining = Sets.difference(new HashSet<String>(specs), ret.keySet()); List<String> remainingList = new ArrayList<String>(remaining); for (List<String> partition : Lists.partition(remainingList, 100)) { String topicName = getCompositeTopicName(partition); try { Topic topic = session.createTopic(topicName); final MessageConsumer messageConsumer = session.createConsumer(topic); messageConsumer.setMessageListener(jmsDispatcher); ConsumerRecord record = new ConsumerRecord(messageConsumer, partition); for (String tickDistributionSpecification : partition) { _messageConsumersBySpec.put(tickDistributionSpecification, record); ret.put(tickDistributionSpecification, getCloseAction(tickDistributionSpecification, record)); } } catch (JMSException e) { throw new OpenGammaRuntimeException("Failed to create subscription to JMS topics " + partition, e); } } return ret; } private Runnable getCloseAction(final String tickDistributionSpecification, final ConsumerRecord record) { return new Runnable() { @Override public void run() { record.getReceiving().remove(tickDistributionSpecification); if (record.getReceiving().isEmpty()) { s_logger.debug("Closing connection after last unsubscribe {}", tickDistributionSpecification); JmsUtils.closeMessageConsumer(record.getConsumer()); for (String receiving : record.getAllReceiving()) { _messageConsumersBySpec.remove(receiving); } } else { //TODO: Should I shrink the subscription? s_logger.debug("Not closing composite connection remaining subscribtions {}", record.getReceiving()); } } }; } private String getCompositeTopicName(Collection<String> specs) { ArgumentChecker.notEmpty(specs, "specs"); StringBuilder topicNameBuilder = new StringBuilder(); for (String spec : specs) { topicNameBuilder.append(spec); topicNameBuilder.append(','); } topicNameBuilder.setLength(topicNameBuilder.length() - 1); String topicName = topicNameBuilder.toString(); return topicName; } @Override public synchronized void close() { _messageConsumersBySpec.clear(); super.close(); } }