/*
* Copyright (C) 2012 - present by Yann Le Tallec.
* Please see distribution for license.
*/
package com.assylias.jbloomberg;
import static com.assylias.jbloomberg.SessionState.NEW;
import static com.assylias.jbloomberg.SessionState.STARTED;
import static com.assylias.jbloomberg.SessionState.STARTING;
import static com.assylias.jbloomberg.SessionState.STARTUP_FAILURE;
import static com.assylias.jbloomberg.SessionState.TERMINATED;
import com.bloomberglp.blpapi.CorrelationID;
import com.bloomberglp.blpapi.DuplicateCorrelationIDException;
import com.bloomberglp.blpapi.InvalidRequestException;
import com.bloomberglp.blpapi.Request;
import com.bloomberglp.blpapi.RequestQueueOverflowException;
import com.bloomberglp.blpapi.Session;
import com.bloomberglp.blpapi.SessionOptions;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import static java.util.Objects.requireNonNull;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default implementation of the BloombergSession interface.
* <p>
* See the documentation of the parent class for more details.
* <p>
* This implementation is thread safe. Memory consistency effects: actions in a thread prior to submitting an request or
* subscribing to real time information happen-before actions subsequent to the access to the data returned by those
* requests / subscriptions.
*/
public class DefaultBloombergSession implements BloombergSession {
private final static Logger logger = LoggerFactory.getLogger(DefaultBloombergSession.class);
/**
* A unique session id generator to easily identify the sessions
*/
private final static AtomicInteger sessionIdGenerator = new AtomicInteger();
/**
* Session options used to create the Bloomberg session
*/
private final SessionOptions sessionOptions;
/**
* Called whenever the SessionState of this session changes
*/
private Consumer<SessionState> sessionStateListener;
/**
* The underlying Bloomberg session object
*/
private final Session session;
/**
* This session's unique ID - used for logging essentially (in toString)
*/
private final int sessionId = sessionIdGenerator.incrementAndGet();
/**
* Used to check if the session startup process is over (in which case either the session is started or startup
* failed)
*/
private final CountDownLatch sessionStartup = new CountDownLatch(1);
/**
* The state of this session - Also used to avoid starting the session more than once (which would throw an
* exception).
*/
private final AtomicReference<SessionState> state = new AtomicReference<>(SessionState.NEW);
/**
* The queue that is used to transfer subscription data from Bloomberg to the interested parties
*/
private final BlockingQueue<Data> subscriptionDataQueue = new LinkedBlockingQueue<>();
/**
* The event handler used by this session to process results asynchronously
*/
private final BloombergEventHandler eventHandler;
/**
* Collection that keeps track of services that have been asynchronously started. They might not be started yet.
*/
private final Set<BloombergServiceType> openingServices = EnumSet.noneOf(BloombergServiceType.class);
private final ExecutorService executor = Executors.newFixedThreadPool(10, new ThreadFactory() {
private final AtomicInteger threadId = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Bloomberg Session # " + sessionId + " - " + threadId.incrementAndGet());
}
});
private final EventsManager eventsManager = new ConcurrentConflatedEventsManager();
private final SubscriptionManager subscriptionManager = new SubscriptionManager(subscriptionDataQueue,
eventsManager);
/**
* Creates a new BloombergSession using default SessionOptions {@link SessionOptions#SessionOptions()}.
*/
public DefaultBloombergSession() {
this(new SessionOptions());
}
/**
* Creates a new BloombergSession using the provided SessionOptions.
*
* @param sessionOptions a non null {@link SessionOptions}.
*
* @throws NullPointerException if the argument is null.
*/
public DefaultBloombergSession(SessionOptions sessionOptions) {
this(sessionOptions, x -> {/*no-op*/});
}
/**
* Creates a new BloombergSession using the provided SessionOptions and SessionState listener. Note that the listener will be called promptly after a
* state change of the underlying Bloomberg connection but there may be a slight delay (in particular if calling
* {@link DefaultBloombergSession#start(java.util.function.Consumer)}, that consumer may be called first.<br><br>
* See also {@link SessionState} for a description of a typical session lifecycle.
*
* @param sessionOptions a non null {@link SessionOptions}.
* @param sessionStateListener a listener that will be called every time the {@link SessionState} of this BloombergSession changes.
*
* @throws NullPointerException if any of the arguments are null.
*/
public DefaultBloombergSession(SessionOptions sessionOptions, Consumer<SessionState> sessionStateListener) {
this.sessionOptions = requireNonNull(sessionOptions);
this.sessionStateListener = requireNonNull(sessionStateListener);
this.eventHandler = new BloombergEventHandler(subscriptionDataQueue, sessionStateListener);
session = new Session(sessionOptions, eventHandler);
updateStateListener();
}
/**
* WARNING: only call this for custom states (i.e. NEW, STARTING) - the other states are set by the EventHandler.
*/
private void updateStateListener() {
sessionStateListener.accept(state.get());
}
@Override
public synchronized void start() throws BloombergException {
start(x -> {/*no-op*/});
}
@Override
public synchronized void start(Consumer<BloombergException> onStartupFailure) throws BloombergException {
requireNonNull(onStartupFailure);
if (state.get() != NEW) {
throw new IllegalStateException("Session has already been started: " + this);
}
if (onlyConnectToLocalAddresses() && !BloombergUtils.startBloombergProcessIfNecessary()) { //could not be started for some reason
state.set(STARTUP_FAILURE);
updateStateListener();
throw new BloombergException("Failed to start session: bbcomm process could not be started");
}
logger.info("Starting Bloomberg session #{} with options: {}", sessionId, getOptions());
try {
eventHandler.onSessionStarted(() -> {
subscriptionManager.start(DefaultBloombergSession.this); //needs to be before the countdown (see subscribe method)
state.set(STARTED);
sessionStartup.countDown();
});
eventHandler.onSessionStartupFailure((BloombergException e) -> {
state.set(STARTUP_FAILURE);
sessionStartup.countDown();
onStartupFailure.accept(e);
});
if (!state.compareAndSet(NEW, STARTING)) {
throw new AssertionError("State was expected to be NEW but found " + state.get());
}
updateStateListener();
session.startAsync();
logger.info("Session #{} started asynchronously", sessionId);
} catch (IOException | IllegalStateException e) {
throw new BloombergException("Failed to start session", e);
}
}
Session getBloombergSession() {
return session;
}
/**
* Closes the session. If the session has not been started yet, does nothing. This call will block until the session
* is actually stopped.<br>
* If the session startup encountered a problem (typically: can't connect to a Bloomberg session), this may block
* for a few seconds....
* A stopped session can't be restarted.
*/
@Override
public synchronized void stop() {
if (state.get() == NEW) {
logger.warn("Ignoring call to stop: session not started");
return;
}
try {
logger.info("Stopping Bloomberg session #{}", sessionId);
boolean started = sessionStartup.await(1, TimeUnit.SECONDS); //with 3.6.1.0, if the session is not started yet, the call to stop can block
if (!started) logger.info("I waited for 1 second but Bloomberg session #{} is still not started...");
executor.shutdownNow();
subscriptionManager.stop(this);
session.stop();//started ? SYNC : ASYNC); //if not started, something's wrong, don't spend too much time here...
state.set(TERMINATED);
logger.info("Stopped Bloomberg session #{}", sessionId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Submits a request to the Bloomberg Session and returns immediately.
*
* Calling get() on the returned future may block for a very long time - it is advised to use the get(timeout)
* version.<br>
* Additional exceptions may be thrown within the future (causing an ExecutionException when calling
* future.get()). It is the responsibility of the caller to check and handle those exceptions:
* <ul>
* <li><code>BloombergException</code> - if the session or the required service could not be started or if the
* request execution could not be completed
* <li><code>CancellationException</code> - if the request execution was cancelled (interrupted) before completion
* </ul>
*
* @return a Future that contains the result of the request. The future can be cancelled to cancel a long running
* request.
*
* @throws IllegalStateException if the start method was not called before this method
* @throws NullPointerException if request is null
*
*/
@Override
public <T extends RequestResult> Future<T> submit(final RequestBuilder<T> request) {
requireNonNull(request, "request cannot be null");
if (state.get() == NEW) {
throw new IllegalStateException("A request can't be submitted before the session is started");
}
logger.debug("Submitting request {}", request);
Callable<T> task = () -> {
BloombergServiceType serviceType = request.getServiceType();
CorrelationID cId = getNextCorrelationId();
try {
openService(serviceType);
ResultParser<T> parser = request.getResultParser();
eventHandler.setParser(cId, parser);
sendRequest(request, cId);
return parser.getResult();
} catch (IOException | InvalidRequestException | RequestQueueOverflowException | DuplicateCorrelationIDException |
IllegalStateException e) {
throw new BloombergException("Could not process the request", e);
} catch (InterruptedException e) {
session.cancel(cId);
throw new CancellationException("The request was cancelled");
}
};
return executor.submit(task);
}
@Override
public void subscribe(SubscriptionBuilder subscription) {
if (state.get() == SessionState.NEW) {
throw new IllegalStateException("A request can't be submitted before the session is started");
}
try {
sessionStartup.await(); //once the latch counts down, we know that the session has been set.
if (state.get() != SessionState.STARTED) {
throw new RuntimeException("The Bloomberg session could not be started");
}
subscriptionManager.subscribe(subscription);
} catch (IOException e) {
throw new RuntimeException("Could not complete subscription request", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override public SessionState getSessionState() {
return state.get();
}
/**
* Opens the the service if it has not been opened before, otherwise does nothing. This call blocks until the
* service is opened or an exception is thrown.
* <p>
* @throws IOException if the service could not be opened (Bloomberg API exception)
* @throws InterruptedException if the current thread is interrupted while opening the service
*/
private synchronized void openService(final BloombergServiceType serviceType) throws IOException, InterruptedException, BloombergException {
if (openingServices.contains(serviceType)) {
return; //only start the session once
}
logger.debug("Waiting for session to start while opening service {}", serviceType);
sessionStartup.await();
if (state.get() != SessionState.STARTED) {
throw new BloombergException("The Bloomberg session could not be started");
}
logger.debug("Opening service {}", serviceType);
if (!session.openService(serviceType.getUri())) {
throw new IllegalStateException("The service could not be opened (openService returned false)");
}
openingServices.add(serviceType);
}
private boolean onlyConnectToLocalAddresses() {
return Arrays.stream(sessionOptions.getServerAddresses())
.map(SessionOptions.ServerAddress::host)
.allMatch(NetworkUtils::isLocalhost);
}
/**
*
* @return the result of the request
* <p>
* @throws IllegalStateException If the session is not established
* @throws InvalidRequestException If the request is not compliant with the schema for the request
* @throws RequestQueueOverflowException If this session has too many enqueued requests
* @throws IOException If any error occurs while sending the request
* @throws DuplicateCorrelationIDException If the specified correlationId is already active for this Session
*/
private CorrelationID sendRequest(final RequestBuilder<?> request, CorrelationID cId) throws IOException {
Request bbRequest = request.buildRequest(session);
session.sendRequest(bbRequest, cId);
return cId;
}
CorrelationID getNextCorrelationId() {
return new CorrelationID(); //returns a new unique correlation ID in a thread safe way
}
/**
* Used for logging
*/
private Map<String, Object> getOptions() {
Class<?> c = SessionOptions.class;
Map<String, Object> options = new TreeMap<>();
for (Method m : c.getMethods()) {
if (m.getName().contains("get") && !m.getName().contains("getClass")) {
String name = m.getName().substring(3);
try {
Object option = m.invoke(sessionOptions);
boolean isArray = option != null && option.getClass().isArray();
options.put(name, isArray ? Arrays.deepToString((Object[]) option) : Objects.toString(option));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
options.put(name, "n.a.");
}
}
}
return options;
}
@Override
public String toString() {
return "Session #" + sessionId + " [" + state.get() + "]";
}
}