/* * Copyright (C) 2012 - present by Yann Le Tallec. * Please see distribution for license. */ package com.assylias.jbloomberg; import com.assylias.bigblue.utils.TypedObject; import com.bloomberglp.blpapi.CorrelationID; import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An EventsManager that skips no-change events. For example, if the same "BID" is received twice for the same security, * only the first one will be relayed to the listeners as a change. * * This implementation is thread safe. */ final class ConcurrentConflatedEventsManager implements EventsManager { private static final Logger logger = LoggerFactory.getLogger(ConcurrentConflatedEventsManager.class); private static final ExecutorService fireListeners = Executors.newFixedThreadPool(10, new ThreadFactory() { private final AtomicInteger number = new AtomicInteger(); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "Bloomberg Listeners Thread #" + number.incrementAndGet()); t.setDaemon(true); //daemon to allow JVM exit return t; } }); private final ConcurrentMap<EventsKey, Listeners> listenersMap = new ConcurrentHashMap<>(); private final ConcurrentMap<CorrelationID, SubscriptionErrorListener> errorListeners = new ConcurrentHashMap<>(); @Override public void addEventListener(String ticker, CorrelationID id, RealtimeField field, DataChangeListener lst) { logger.debug("addEventListener({}, {}, {}, {})", new Object[]{ticker, id, field, lst}); EventsKey key = EventsKey.of(id, field); Listeners listenersInMap = listenersMap.computeIfAbsent(key, k -> new Listeners(ticker)); listenersInMap.addListener(lst); } @Override public void fireEvent(CorrelationID id, RealtimeField field, Object value) { final EventsKey key = EventsKey.of(id, field); Listeners lst = listenersMap.get(key); if (lst == null) { return; //skip that event: nobody's listening anyway } String ticker = lst.ticker; DataChangeEvent evt = null; TypedObject newValue = TypedObject.of(value); synchronized (lst) { if (!newValue.equals(lst.previousValue)) { evt = new DataChangeEvent(ticker, field.toString(), lst.previousValue, newValue); lst.previousValue = newValue; } } if (evt != null) lst.fireEvent(evt); } @Override public void fireError(CorrelationID id, SubscriptionError error) { SubscriptionErrorListener lst = errorListeners.get(id); if (lst != null) { Future<?> f = fireListeners.submit(() -> lst.onError(error)); monitorListenerExecution(f, lst, error); } } void monitorListenerExecution(Future<?> f, SubscriptionErrorListener lst, SubscriptionError error) { fireListeners.submit(new Callable<Void>() { @Override public Void call() throws Exception { try { f.get(1, TimeUnit.SECONDS); } catch (TimeoutException e) { logger.warn("Slow error listener {} has not processed error {} in one second", lst, error); } catch (ExecutionException e) { logger.error("Listener " + lst + " has thrown exception on error " + error, e.getCause()); } return null; } }); } @Override public void onError(CorrelationID id, SubscriptionErrorListener lst) { errorListeners.put(id, lst); } private static class Listeners { private final String ticker; //Using a set so that a listener that registers twice is only called once private final Set<DataChangeListener> listeners = Collections.newSetFromMap(new ConcurrentHashMap<>()); private TypedObject previousValue; Listeners(String ticker) { this.ticker = ticker; } void addListener(DataChangeListener lst) { listeners.add(lst); } void fireEvent(DataChangeEvent evt) { for (DataChangeListener lst : listeners) { //(i) if a listener gets stuck, the others can still make progress //(ii) if a listener throws an exception, a new thread will be created Future<?> f = fireListeners.submit(() -> lst.dataChanged(evt)); monitorListenerExecution(f, lst, evt); } } void monitorListenerExecution(Future<?> f, DataChangeListener lst, DataChangeEvent evt) { fireListeners.submit(new Callable<Void>() { @Override public Void call() throws Exception { try { f.get(1, TimeUnit.SECONDS); } catch (TimeoutException e) { logger.warn("Slow listener {} has not processed event {} in one second", lst, evt); } catch (ExecutionException e) { logger.error("Listener " + lst + " has thrown exception on event " + evt, e.getCause()); } return null; } }); } } }