/* * Copyright (C) 2012 - present by Yann Le Tallec. * Please see distribution for license. */ package com.assylias.jbloomberg; import com.bloomberglp.blpapi.CorrelationID; import com.bloomberglp.blpapi.Subscription; import com.bloomberglp.blpapi.SubscriptionList; import com.google.common.base.Preconditions; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A class to which the main session object delegates the real time subscriptions management. */ final class SubscriptionManager { private final static Logger logger = LoggerFactory.getLogger(SubscriptionManager.class); private DefaultBloombergSession session; /** * A map that keeps track of what has been submitted to the session so far to better handle resubscriptions and * cancellations. All reads and writes must be done in synchronized blocks. */ private final Map<String, SubscriptionHolder> subscriptionsByTicker = new HashMap<>(); /** * A map that links CorrelationIDs and subscriptions. */ private final ConcurrentMap<CorrelationID, SubscriptionHolder> subscriptionsById = new ConcurrentHashMap<>(); /** * The queue that is used to transfer subscription data from Bloomberg to the interested parties */ private final BlockingQueue<Data> subscriptionDataQueue; /** * Everything runs in the same thread */ private static final int NUM_THREADS = 1; /** * An executor to forward events from the queue to listeners */ private final ExecutorService edt = Executors.newFixedThreadPool(NUM_THREADS, new ThreadFactory() { private final AtomicInteger number = new AtomicInteger(); @Override public Thread newThread(Runnable r) { return new Thread(r, "Bloomberg EDT #" + number.incrementAndGet()); } }); /** * The Events manager that will forward events to the listeners */ private final EventsManager eventsManager; public SubscriptionManager(BlockingQueue<Data> subscriptionDataQueue, EventsManager eventsManager) { this.subscriptionDataQueue = subscriptionDataQueue; this.eventsManager = eventsManager; } /** * This method needs to be called to start the Subscription Manager. The session must be started when this method is * called. * * @param session a started Bloomberg session */ synchronized void start(DefaultBloombergSession session) { logger.info("Starting the SubscriptionManager for {}", session); this.session = Preconditions.checkNotNull(session, "session can't be null"); startDispatching(); } synchronized void stop(DefaultBloombergSession stoppingSession) { if (session == null) { logger.info("Stopping the SubscriptionManager for {}", stoppingSession); } else if (session == stoppingSession) { logger.info("Stopping the SubscriptionManager for {}", session); } else { throw new IllegalStateException("The starting and stopping sessions are not the same: [start] " + session + " [stop]" + stoppingSession); } edt.shutdownNow(); } private void startDispatching() { Runnable r = new Runnable() { @Override public void run() { try { while (!Thread.currentThread().isInterrupted()) { Data data = subscriptionDataQueue.take(); CorrelationID id = data.getCorrelationId(); if (RealtimeField.containsIgnoreCase(data.getField())) { RealtimeField field = RealtimeField.valueOfIgnoreCase(data.getField()); eventsManager.fireEvent(id, field, data.getValue()); } else if (data.getValue() instanceof SubscriptionError) { SubscriptionError error = (SubscriptionError) data.getValue(); logger.info("Subscription error [{}]: {}", error.getTopic(), error.getDescription()); if ("SubscriptionFailure".equals(error.getType())) { //we need to remove the subscription from our maps otherwise a resubscribe could throw an exception. String ticker = error.getTopic(); subscriptionsByTicker.remove(ticker); subscriptionsById.remove(id); } eventsManager.fireError(id, error); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } logger.info("Exiting Subscription Manager dispatching loop"); } }; for (int i = 0; i < NUM_THREADS; i++) { edt.submit(r); } } /** * Updates the Bloomberg session to subscribe to the securities and fields specified in the builder. * * @param subscriptionBuilder the builder containing the details of the securities and fields to subscribe * * @throws IllegalStateException if a started session has not been set before this method is called * @throws IOException if there is a communication error on subscribe */ synchronized void subscribe(SubscriptionBuilder subscriptionBuilder) throws IOException { if (session == null) { throw new IllegalStateException("Can't subscribe to a session before it is started"); } SubscriptionList list; list = getReSubscriptionsList(subscriptionBuilder); if (!list.isEmpty()) { session.getBloombergSession().resubscribe(list); } list = getNewSubscriptionsList(subscriptionBuilder); if (!list.isEmpty()) { session.getBloombergSession().subscribe(list); } } private SubscriptionList getNewSubscriptionsList(SubscriptionBuilder builder) { SubscriptionList list = new SubscriptionList(); for (String ticker : builder.getSecurities()) { if (!subscriptionsByTicker.containsKey(ticker)) { //only include tickers that had no previous subscriptions list.add(getSubscription(ticker, builder)); } } return list; } private Subscription getSubscription(String ticker, SubscriptionBuilder builder) { CorrelationID id = session.getNextCorrelationId(); SubscriptionHolder sh = new SubscriptionHolder(id); logger.debug("Correlation id for {}: {}", ticker, sh.id); sh.update(builder); addListenersToEventsManager(builder, ticker, sh.id); subscriptionsByTicker.put(ticker, sh); subscriptionsById.put(sh.id, sh); //THIS IS THE ONLY PLACE WHERE WE WRITE TO THAT MAP if (sh.throttle != 0) { return new Subscription(ticker, sh.getFieldsAsList(), sh.getThrottleAsList(), sh.id); } else { return new Subscription(ticker, sh.getFieldsAsList(), sh.id); } } private SubscriptionList getReSubscriptionsList(SubscriptionBuilder builder) { SubscriptionList list = new SubscriptionList(); for (String ticker : builder.getSecurities()) { if (subscriptionsByTicker.containsKey(ticker)) { //only include tickers that have previously been subscribed list.add(getReSubscription(ticker, builder)); } } return list; } private Subscription getReSubscription(String ticker, SubscriptionBuilder builder) { SubscriptionHolder sh = subscriptionsByTicker.get(ticker); sh.update(builder); addListenersToEventsManager(builder, ticker, sh.id); if (sh.throttle != 0) { return new Subscription(ticker, sh.getFieldsAsList(), sh.getThrottleAsList(), sh.id); } else { return new Subscription(ticker, sh.getFieldsAsList(), sh.id); } } private void addListenersToEventsManager(SubscriptionBuilder builder, String ticker, CorrelationID id) { for (RealtimeField field : builder.getFields()) { for (DataChangeListener lst : builder.getListeners()) { eventsManager.addEventListener(ticker, id, field, lst); } } eventsManager.onError(id, builder.getErrorListener()); } private static class SubscriptionHolder { private final CorrelationID id; private final Set<RealtimeField> fields = EnumSet.noneOf(RealtimeField.class); private final Set<DataChangeListener> listeners = new HashSet<>(); private double throttle = 0; public SubscriptionHolder(CorrelationID id) { this.id = id; } List<String> getFieldsAsList() { List<String> list = new ArrayList<>(fields.size()); for (RealtimeField f : fields) { list.add(f.toString()); } return list; } List<String> getThrottleAsList() { return Arrays.asList("interval=" + throttle); } void update(SubscriptionBuilder builder) { fields.addAll(builder.getFields()); listeners.addAll(builder.getListeners()); throttle = builder.getThrottle(); } } }