/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.bbg;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.Lifecycle;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;
import org.threeten.bp.temporal.ChronoUnit;
import com.bloomberglp.blpapi.EventHandler;
import com.bloomberglp.blpapi.Service;
import com.bloomberglp.blpapi.Session;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.livedata.ConnectionUnavailableException;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.OpenGammaClock;
/**
* Supplies a Bloomberg session and service, creating the connection and managaing reconnection if necessary. The main purpose of this class is to throttle the rate of attempts to reconnect. If
* Bloomberg is down and a connection is attempted every time a request is made the Bloomberg API runs out of memory, possibly due to an unconstrained thread pool.
* <p>
* This class creates a session on demand but if connection fails it won't retry until a delay has elapsed to avoid overwhelming the Bloomberg API.
*/
public class SessionProvider implements Lifecycle, BloombergConnector.AvailabilityListener {
/** The logger. */
private static final Logger s_logger = LoggerFactory.getLogger(SessionProvider.class);
/** Default time in ms to wait before retrying after an unsuccessful attempt to connect. */
private static final long DEFAULT_RETRY_DELAY = 30000;
/** The connection details. */
private final BloombergConnector _connector;
/** Names of the Bloomberg service to create. */
private final Set<String> _serviceNames = new HashSet<>();
/** Controls access to the session. */
private final Object _lock = new Object();
/** Duration to wait before retrying after an unsuccessful attempt to connect. */
private final Duration _retryDuration;
/** The session. */
private Session _session;
/** The time of the last connection attempt. Also used as a lifecycle indicator - being null if the provider is not running */
private final AtomicReference<Instant> _lastRetry = new AtomicReference<Instant>();
private final EventHandler _eventHandler;
/**
* @param connector Bloomberg connection details
* @param serviceName Name of the service to open
*/
public SessionProvider(BloombergConnector connector, String serviceName) {
this(connector, serviceName, null);
}
/**
* @param connector Bloomberg connection details
* @param serviceName Name of the service to open
* @param eventHandler the event handler of bloomberg session. if null session is synchronous
*/
public SessionProvider(BloombergConnector connector, String serviceName, EventHandler eventHandler) {
this(connector, Collections.singletonList(ArgumentChecker.notNull(serviceName, "serviceName")), eventHandler);
}
/**
* @param connector Bloomberg connection details
* @param serviceNames List of service names to open, not null.
*/
public SessionProvider(BloombergConnector connector, Iterable<String> serviceNames) {
this(connector, serviceNames, null);
}
/**
* @param connector Bloomberg connection details
* @param serviceNames List of service names to open, not null.
* @param eventHandler the event handler of bloomberg session. if null session is synchronous
*/
public SessionProvider(BloombergConnector connector, Iterable<String> serviceNames, EventHandler eventHandler) {
this(connector, DEFAULT_RETRY_DELAY, serviceNames, eventHandler);
}
/**
* @param connector Bloomberg connection details
* @param retryDelay Time to wait between unsuccessful connection attempts
* @param serviceNames List of service names to open, not null.
* @param eventHandler the event handler of bloomberg session. if null session is synchronous
*/
public SessionProvider(BloombergConnector connector, long retryDelay, Iterable<String> serviceNames, EventHandler eventHandler) {
ArgumentChecker.notNull(connector, "connector");
ArgumentChecker.notNull(serviceNames, "serviceNames");
ArgumentChecker.notNull(connector.getSessionOptions(), "connector.sessionOptions");
_retryDuration = Duration.of(retryDelay, ChronoUnit.MILLIS);
_connector = connector;
for (String serviceName : serviceNames) {
ArgumentChecker.notEmpty(serviceName, "serviceName");
_serviceNames.add(serviceName);
}
_eventHandler = eventHandler;
}
/**
* Returns a session, creating it if necessary. If this method is called after the provider is closed a new session will be created.
*
* @return The session, not null
* @throws ConnectionUnavailableException If no connection is available
*/
public Session getSession() {
final Session newSession;
synchronized (_lock) {
if (_session != null) {
return _session;
} else {
Instant lastRetry = _lastRetry.get();
if (lastRetry == null) {
throw new ConnectionUnavailableException("Session provider has not been started");
}
Instant now = OpenGammaClock.getInstance().instant();
if (Duration.between(lastRetry, now).compareTo(_retryDuration) < 0) {
throw new ConnectionUnavailableException("No Bloomberg connection is available");
}
_lastRetry.set(now);
s_logger.info("Bloomberg session being opened...");
Session session = null;
try {
try {
session = _connector.createOpenSession(_eventHandler);
} catch (OpenGammaRuntimeException e) {
throw new ConnectionUnavailableException("Failed to open session", e);
}
s_logger.info("Bloomberg session open");
s_logger.info("Bloomberg service being opened...");
for (String serviceName : _serviceNames) {
try {
if (!session.openService(serviceName)) {
throw new ConnectionUnavailableException("Bloomberg service failed to start: " + serviceName);
}
} catch (InterruptedException ex) {
Thread.interrupted();
throw new ConnectionUnavailableException("Bloomberg service failed to start: " + serviceName, ex);
} catch (Exception ex) {
throw new ConnectionUnavailableException("Bloomberg service failed to start: " + serviceName, ex);
}
s_logger.info("Bloomberg service open: {}", serviceName);
}
_session = session;
newSession = session;
session = null;
} finally {
if (session != null) {
// If the session was started but the service not opened, then there will be sockets open and threads allocated by
// the Bloomberg API which need to be killed. Just letting the session fall out of scope doesn't work (PLAT-5309)
s_logger.debug("Attempting to stop partially constructed session");
try {
session.stop();
} catch (Exception e) {
s_logger.error("Error stopping partial session", e);
}
}
}
}
}
// We've got a connection open; let any other components that Bloomberg is back up
_connector.notifyAvailabilityListeners();
return newSession;
}
/**
* @return The service, not null
* @param serviceName the service name, not null
* @throws ConnectionUnavailableException If no connection is available
*/
public Service getService(String serviceName) {
ArgumentChecker.notNull(serviceName, "serviceName");
ArgumentChecker.isTrue(_serviceNames.contains(serviceName), "service name is not recognized");
return getSession().getService(serviceName);
}
/**
* @return true if there is an open connection to Bloomberg.
*/
public boolean isConnected() {
synchronized (_lock) {
return _session != null;
}
}
/**
* Stops the session. If {@link #getSession()} is called after this method a new session will be created.
* <p>
* Calling this method when there is no active session does nothing.
*/
public void invalidateSession() {
synchronized (_lock) {
if (_session != null) {
try {
s_logger.info("Bloomberg session being stopped...");
_session.stop();
s_logger.info("Bloomberg session stopped");
} catch (InterruptedException e) {
s_logger.warn("Interrupted closing session " + _session, e);
} finally {
_session = null;
}
}
}
}
// Lifecycle
/**
* Enables the provider, starting it so that {@link #getSession} will connect on demand to Bloomberg.
* <p>
* Calling this method when the provider is already started does nothing.
*/
@Override
public void start() {
if (_lastRetry.compareAndSet(null, Instant.EPOCH)) {
// First call to {@link #getSession} will now attempt to connect
_connector.addAvailabilityListener(this);
}
}
/**
* Tests if the provider is running, that is a call has been made to {@link #start}. This does not mean that there is a connection to Bloomberg - use {@link #isConnected} to test for that. If this
* returns false it will mean there is no active connection and none will be made by calls to {@link #getSession}.
*
* @return true if the provider is started, false otherwise.
*/
@Override
public boolean isRunning() {
return _lastRetry.get() != null;
}
/**
* Disables the provider, stopping it so that any active session is closed and {@link #getSession} will no longer connect to Bloomberg.
* <p>
* Calling this method when the provider is not started does nothing.
*/
@Override
public void stop() {
_lastRetry.set(null);
invalidateSession();
_connector.removeAvailabilityListener(this);
}
// AvailabilityListener
@Override
public void bloombergAvailable() {
// If the session is already invalid the next call to {@link #getSession} will try and connect if we set the timestamp to the epoch.
// If the session is connected (for example we triggered the notify) then we'll be set for immediate reconnection - this recovers from temporary network problems
Instant lastRetry;
do {
lastRetry = _lastRetry.get();
if (lastRetry == null) {
// Already stopped
return;
}
} while (!_lastRetry.compareAndSet(lastRetry, Instant.EPOCH));
s_logger.info("Bloomberg connection available for {}", _serviceNames);
}
/**
* Gets the connector.
* @return the connector
*/
public BloombergConnector getConnector() {
return _connector;
}
}