/* * Copyright (c) 2012 EMC Corporation * All Rights Reserved */ package com.emc.storageos.cimadapter.connections; import java.io.IOException; import java.net.Socket; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.MessageFormatter; import com.emc.storageos.cimadapter.connections.celerra.CelerraConnection; import com.emc.storageos.cimadapter.connections.cim.CimConnection; import com.emc.storageos.cimadapter.connections.cim.CimConnectionInfo; import com.emc.storageos.cimadapter.connections.cim.CimConstants; import com.emc.storageos.cimadapter.connections.cim.CimFilterInfo; import com.emc.storageos.cimadapter.connections.cim.CimFilterMap; import com.emc.storageos.cimadapter.connections.cim.CimListener; import com.emc.storageos.cimadapter.connections.cim.CimListenerInfo; import com.emc.storageos.cimadapter.connections.ecom.EcomConnection; import com.emc.storageos.cimadapter.consumers.CimIndicationConsumerList; import com.emc.storageos.model.property.PropertyInfo; /** * The ConnectionManager manages the connections to the storage arrays whose * indications are to be monitored. The ConnectionManager also creates and * starts the Listener which is notified when indications occur. It also loads * the filter map which specifies the indications for which the connections are * subscribed. */ public class ConnectionManager { private static final int ONE_MINUTE = 1; private static final int INITIAL_DELAY = ONE_MINUTE; private static final long MS_IN_SECONDS = 1000; // # Milliseconds in a second private static long maxConnectionTTL = 0; private static final String CIM_CONNECTION_MAX_INACTIVE_TIME = "cim_connection_max_inactive_time"; private static boolean configured = false; // A reference to the connection manager configuration. private ConnectionManagerConfiguration _configuration; // A reference to the CIM listener; private CimListener _listener; // A map of cache keys (host/port) to their connections private Map<String, CimConnection> _connections = new HashMap<String, CimConnection>(); // A synchronization object to control access to shared objects private Lock connectionLock = new ReentrantLock(); // A scheduled execution service that cleans up connections private ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); // A map of cache keys in _connections to the last time the connection was retrieved private Map<String, Long> connectionLastTouch = new HashMap<>(); // This map will be used to keep track of connections that are pinned. These are connections // that should not be reaped. We will be keeping a count since you can have multiple arrays // behind the same provider IP. When pinConnection is called, the count for the connection is // incremented; when unpinConnection is called, the count is decremented. When the count // reaches zero, it will be removed from the pinnedConnections map and it becomes eligible // for reaping. private Map<String, Integer> pinnedConnections = new HashMap<>(); // The logger. private static final Logger s_logger = LoggerFactory.getLogger(ConnectionManager.class); // Separator for the host/port cache connection entry private static final String HOST_PORT_SEPARATOR = ":"; /** * Constructs a connection manager instance. * * @param configuration A reference to the configuration. * * @throws Exception When an error occurs initializing the connection * manager. */ public ConnectionManager(ConnectionManagerConfiguration configuration) throws Exception { // Set the configuration. _configuration = configuration; if (_configuration == null) { throw new ConnectionManagerException("Invalid null connection manager configuration."); } } /** * Private default constructor. */ @SuppressWarnings("unused") private ConnectionManager() { } /** * This will place the connection for the host+port into the pinned list. This * will be used as a way to prevent the connection from getting reaped while * it is in use for a long period of time. * * We're keeping count of the times that pinConnection is being called for the * connection. This is because you can have multiple arrays behind a single * provider IP. If discovery is run against the provider, then the same client * will be used for all of its arrays. We would need to keep count, so only * when the last array's discovery is complete, we can safely remove the * connection from the pinned list. * * @param host [IN] - Host name/IP * @param port [IN] - port */ public void pinConnection(String host, Integer port) { connectionLock.lock(); try { String hostAndPort = ConnectionManager.generateConnectionCacheKey(host, port); if (_connections.containsKey(hostAndPort)) { Integer count = pinnedConnections.get(hostAndPort); if (count == null) { // No entry yet, so initialize count count = 1; } else { // Increase the count for this hostAndPort connection count++; } pinnedConnections.put(hostAndPort, count); } s_logger.info("CimConnection {} is pinned, count = {}", hostAndPort, pinnedConnections.get(hostAndPort)); } finally { connectionLock.unlock(); } } /** * This will remove the connection for host+port from the pinned list. It will make * the connection eligible for reaping again, if the pin count has reached zero. * * @param host [IN] - Host name/IP * @param port [IN] - port */ public void unpinConnection(String host, Integer port) { connectionLock.lock(); try { String hostAndPort = ConnectionManager.generateConnectionCacheKey(host, port); if (pinnedConnections.containsKey(hostAndPort)) { // Decrement the current count for the connection Integer count = pinnedConnections.get(hostAndPort) - 1; if (count == 0) { s_logger.info("CimConnection {} pin count has reached zero; it will be unpinned", hostAndPort); pinnedConnections.remove(hostAndPort); } else { s_logger.info("CimConnection {} pin count set to {}", hostAndPort, count); pinnedConnections.put(hostAndPort, count); } } } finally { connectionLock.unlock(); } } /** * Using the propertyInfo retrieved from the CoordinatorClient, we will configure the ConnectionManager. * * @param propertyInfo [IN] - PropertyInfo representing configuration parameters */ public void configure(PropertyInfo propertyInfo) { connectionLock.lock(); try { // Allow the configure() to be run only once by the first thread that calls it if (configured) { return; } s_logger.info("Configuring ConnectionManager"); Long maxTTLSeconds = 0L; // Default value ==> disabled String maxTTLString = propertyInfo.getProperty(CIM_CONNECTION_MAX_INACTIVE_TIME); // If there is a value specified for the configuration properties and it's a number ... if (maxTTLString != null && maxTTLString.matches("\\d+")) { maxTTLSeconds = Long.valueOf(maxTTLString); // Value's unit should be N seconds maxConnectionTTL = maxTTLSeconds * MS_IN_SECONDS; } if (maxTTLSeconds != 0) { // Start up the CimConnection reaper: checks connection times every minute ... executorService.scheduleAtFixedRate(new CimConnectionReaper(), INITIAL_DELAY, ONE_MINUTE, TimeUnit.MINUTES); s_logger.info("ConnectionManager config: CimConnections that have been inactive for more than {} seconds will be reaped", maxTTLSeconds); } else { s_logger.info("ConnectionManager config: {} was set to {}, CIMConnection reaper is disabled", CIM_CONNECTION_MAX_INACTIVE_TIME, maxTTLString); } configured = true; } finally { connectionLock.unlock(); } } /** * Creates a new connection for which indications are to be monitored based * on the passed connection information. * * @param connectionInfo Specifies the information necessary to establish a * connection. * * @throws ConnectionManagerException When a error occurs establishing the * connection. */ public void addConnection(CimConnectionInfo connectionInfo) throws ConnectionManagerException { connectionLock.lock(); try { if (connectionInfo == null) { throw new ConnectionManagerException("Passed connection information is null."); } // If the listener has yet to be created, then create it now. if (_listener == null) { createIndicationListener(connectionInfo); } String hostAndPort = generateConnectionCacheKey(connectionInfo.getHost(), connectionInfo.getPort()); // Only add a connection if there is not already a connection to the // provider specified in the passed connection information. if (isConnected(hostAndPort)) { s_logger.info("There is already a connection to the CIM provider on host/port {}", hostAndPort); return; } try { s_logger.info("Attempting to connect to the provider on host/port {}", hostAndPort); // Pause the listener when adding a new connection. _listener.pause(); // Create a connection as specified by the passed connection // information. String connectionType = connectionInfo.getType(); if (connectionType.equals(CimConstants.CIM_CONNECTION_TYPE)) { createCimConnection(connectionInfo); } else if (connectionType.equals(CimConstants.ECOM_CONNECTION_TYPE)) { createECOMConnection(connectionInfo); } else if (connectionType.equals(CimConstants.ECOM_FILE_CONNECTION_TYPE)) { createCelerraConnection(connectionInfo); } else { throw new ConnectionManagerException(MessageFormatter.format("Unsupported connection type {}", connectionType).getMessage()); } /** * Get client's public certificate and persist them into trustStore. */ _listener.getClientCertificate(connectionInfo); } catch (ConnectionManagerException e) { throw e; } catch (Exception e) { throw new ConnectionManagerException(MessageFormatter.format( "Failed establishing a connection to the provider on host/port {}", hostAndPort).getMessage(), e); } finally { // Now resume the listener. _listener.resume(); } } finally { connectionLock.unlock(); } } private void createIndicationListener(CimConnectionInfo connectionInfo) throws ConnectionManagerException { CimListenerInfo listenerInfo = _configuration.getListenerInfo(); if (listenerInfo == null) { throw new ConnectionManagerException("CIM listener configuration is null."); } try { // We create a temporary connection to the provider host specified // by the passed connection information. We use this temporary // connection to extract the IP address of the local host on which // the connection manager is executing. We need to dynamically get // the IP address of the local host to create the CIM listener on // that host. Socket tempSocket = new Socket(connectionInfo.getHost(), connectionInfo.getPort()); String listenerHostIP = tempSocket.getLocalAddress().toString(); if (listenerHostIP.startsWith("/")) { listenerHostIP = listenerHostIP.substring(1); } s_logger.info("Listener host IP address is {}", listenerHostIP); listenerInfo.setHostIP(listenerHostIP); try { tempSocket.close(); } catch (IOException ioe) { s_logger.warn("Error closing socket connection to provider host.", ioe); } } catch (IOException ioe) { throw new ConnectionManagerException("An error occurred obtaining the listener host IP address", ioe); } // Set the names for the subscription filters. The filters are named // using the IP address for the indication listener host that will // receive indications resulting from the filters. CimFilterMap filters = _configuration.getIndicationFilterMap(); Iterator<CimFilterInfo> filtersIter = filters.getFilters().values().iterator(); while (filtersIter.hasNext()) { filtersIter.next().setName(listenerInfo.getHostIP()); } // Now create and start the listener. try { CimIndicationConsumerList indicationConsumers = _configuration.getIndicationConsumers(); _listener = new CimListener(listenerInfo, indicationConsumers); _listener.startup(); } catch (Exception e) { throw new ConnectionManagerException("Failed creating and starting the indication listener", e); } } /** * Removes an existing connection for which indication monitoring is no * longer desired. * * @param hostAndPort Specifies the host/port for which the CIM connection was * established. * * @throws ConnectionManagerException When a error occurs removing the * connection. */ public void removeConnection(String host, Integer port) throws ConnectionManagerException { connectionLock.lock(); try { String hostAndPort = ConnectionManager.generateConnectionCacheKey(host, port); internalRemoveConnection(hostAndPort); } finally { connectionLock.unlock(); } } /** * Determines whether or not a connection has already been established for * the passed host. * * @param hostAndPort The name of the host to verify. * * @return true if a connection has been created for the passed host, false * otherwise. * * @throws ConnectionManagerException When the passed host is null or blank. */ public boolean isConnected(String hostAndPort) throws ConnectionManagerException { connectionLock.lock(); boolean isConnected = false; try { // Verify the passed host/port is not null or blank. if ((hostAndPort == null) || (hostAndPort.length() == 0)) { throw new ConnectionManagerException("Passed host/port is null or blank."); } CimConnection connection = _connections.get(hostAndPort); if (connection != null) { isConnected = true; } } finally { connectionLock.unlock(); } return isConnected; } /** * Generate the key that is used to cache the connection. * * @param host hostname * @param port port number * @return a hash of the two or null if host is null/empty */ public static String generateConnectionCacheKey(String host, int port) { return (host == null || host.isEmpty()) ? null : host + HOST_PORT_SEPARATOR + port; } /** * Returns a reference to the connection for the provider at the passed * host and port * * @param hostAndPort The name of the host/port on which the provider is executing. * * @return A reference to the provider connection. * * @throws ConnectionManagerException When the passed host is null or blank. */ public CimConnection getConnection(String host, Integer port) throws ConnectionManagerException { connectionLock.lock(); CimConnection connection = null; try { String hostAndPort = generateConnectionCacheKey(host, port); // Verify the passed host/port is not null or blank. if ((hostAndPort == null) || (hostAndPort.length() == 0)) { throw new ConnectionManagerException("Passed host/port is null or blank."); } connection = _connections.get(hostAndPort); if (connection != null) { // Every time the connection is returned, update the last get time connectionLastTouch.put(hostAndPort, System.currentTimeMillis()); } } finally { connectionLock.unlock(); } return connection; } /** * Shutdown the application. * * Stops the listener (which releases its TCP port). * * @throws ConnectionManagerException When an error occurs shutting don the * connection manager. */ public void shutdown() throws ConnectionManagerException { s_logger.info("Shutting down CIM adapter."); connectionLock.lock(); try { // Need to close all the connections and undo their subscriptions. closeAllConnections(); // Stop and destroy the listener. if (_listener != null) { _listener.stop(); _listener = null; } executorService.shutdown(); } catch (Exception e) { throw new ConnectionManagerException("An error occurred shutting down the connection manager", e); } finally { connectionLock.unlock(); } } /** * Creates a connection to a CIM provider using the passed connection info. * * @param connectionInfo Contains the information required to establish the * connection. * * @throws Exception When an error occurs establishing the connection to the * CIM provider. */ private void createCimConnection(CimConnectionInfo connectionInfo) throws Exception { String hostAndPort = generateConnectionCacheKey(connectionInfo.getHost(), connectionInfo.getPort()); s_logger.info("Creating connection to CIM provider on host/port {}", hostAndPort); try { // Create the CIM connection. CimConnection connection = new CimConnection(connectionInfo, _listener, _configuration.getIndicationFilterMap()); connection.connect(_configuration.getSubscriptionsIdentifier(), _configuration.getDeleteStaleSubscriptionsOnConnect()); _connections.put(hostAndPort, connection); connectionLastTouch.put(hostAndPort, System.currentTimeMillis()); } catch (Exception e) { throw new Exception(MessageFormatter.format("Failed creating connection to CIM provider on host/port {}", hostAndPort).getMessage(), e); } } /** * Creates a connection to an ECOM provider using the passed connection * info. * * @param connectionInfo Contains the information required to establish the * connection. * * @throws Exception When an error occurs establishing the connection to the * ECOM provider. */ private void createECOMConnection(CimConnectionInfo connectionInfo) throws Exception { String hostAndPort = generateConnectionCacheKey(connectionInfo.getHost(), connectionInfo.getPort()); s_logger.info("Creating connection to ECOM provider on host/port {}", hostAndPort); try { // Create the ECOM connection. EcomConnection connection = new EcomConnection(connectionInfo, _listener, _configuration.getIndicationFilterMap()); connection.connect(_configuration.getSubscriptionsIdentifier(), _configuration.getDeleteStaleSubscriptionsOnConnect()); _connections.put(hostAndPort, connection); connectionLastTouch.put(hostAndPort, System.currentTimeMillis()); } catch (Exception e) { throw new Exception(MessageFormatter.format("Failed creating connection to ECOM provider on host/port {}", hostAndPort).getMessage(), e); } } /** * Creates a connection to an ECOM provider for a Celerra array using the * passed connection info. * * @param connectionInfo Contains the information required to establish the * connection. * * @throws Exception When an error occurs establishing the connection to the * ECOM provider for the Celerra array. */ private void createCelerraConnection(CimConnectionInfo connectionInfo) throws Exception { String hostAndPort = generateConnectionCacheKey(connectionInfo.getHost(), connectionInfo.getPort()); s_logger.info("Creating connection to Celerra ECOM provider on host/port {}", hostAndPort); try { // Create the ECOM connection. CelerraConnection connection = new CelerraConnection(connectionInfo, _listener, _configuration.getIndicationFilterMap(), _configuration.getCelerraMessageSpecs()); connection.connect(_configuration.getSubscriptionsIdentifier(), _configuration.getDeleteStaleSubscriptionsOnConnect()); _connections.put(hostAndPort, connection); connectionLastTouch.put(hostAndPort, System.currentTimeMillis()); } catch (Exception e) { throw new Exception(MessageFormatter.format( "Failed creating connection to Celerra ECOM provider on host/port {}", hostAndPort).getMessage(), e); } } /** * Closes all the connections being managed. */ private void closeAllConnections() { // Need to close the connection which in turns removes all the // subscriptions for the connection. for (Entry<String, CimConnection> connectionEntry : _connections.entrySet()) { connectionEntry.getValue().close(); } _connections.clear(); connectionLastTouch.clear(); } /** * Make subscription for the given CIM Connection * * @param cimConnection {@link CimConnection} to make subscription for monitoring * @throws Exception Exception */ public void subscribe(CimConnection cimConnection) throws Exception { s_logger.debug("Entering {}", Thread.currentThread().getStackTrace()[1].getMethodName()); s_logger.debug("Subscription Identifier for subscribe action :{}", _configuration.getSubscriptionsIdentifier()); cimConnection.subscribeForIndications(_configuration.getSubscriptionsIdentifier()); s_logger.debug("Exiting {}", Thread.currentThread().getStackTrace()[1].getMethodName()); } /** * Un-Subscribe cimConnection for the given passive SMIS provider connection * * @param cimConnection {@link CimConnection} clear subscription for the given cimConnection */ public void unsubscribe(CimConnection cimConnection) { s_logger.debug("Entering {}", Thread.currentThread().getStackTrace()[1].getMethodName()); s_logger.debug("Subscription Identifier for unsubscribe action :{}", _configuration.getSubscriptionsIdentifier()); cimConnection.unsubscribeForIndications(_configuration.getSubscriptionsIdentifier()); s_logger.debug("Exiting {}", Thread.currentThread().getStackTrace()[1].getMethodName()); } /** * * @param cimConnection {@link CimConnection} delete stale subscription for the given cimConnection */ public void deleteStaleSubscriptions(CimConnection cimConnection) { s_logger.debug("Entering {}", Thread.currentThread().getStackTrace()[1].getMethodName()); s_logger.debug("Subscription Identifier for delete subscription action :{}", _configuration.getSubscriptionsIdentifier()); cimConnection.deleteStaleSubscriptions(_configuration.getSubscriptionsIdentifier()); s_logger.debug("Exiting {}", Thread.currentThread().getStackTrace()[1].getMethodName()); } /** * Looks up the 'hostAndPort' connection in the map. If it exists, the underlying * connection will be closed, it will be removed from the map, and related data * structures will be updated. * * @param hostAndPort [IN] - Host + Port key used for looking up connection */ private void internalRemoveConnection(String hostAndPort) { // Verify the passed host is not null or blank. if ((hostAndPort == null) || (hostAndPort.length() == 0)) { throw new ConnectionManagerException("Passed host/port is null or blank."); } try { // Verify we are managing a connection to the passed host. if (!isConnected(hostAndPort)) { throw new ConnectionManagerException(MessageFormatter.format( "The connection manager is not managing a connection to host {}", hostAndPort).getMessage()); } // Pause the listener when removing a connection. _listener.pause(); // Remove the connection to the passed host. CimConnection connection = _connections.get(hostAndPort); if (connection != null) { s_logger.info("Closing connection to the CIM provider on host/port {}", hostAndPort); connection.close(); _connections.remove(hostAndPort); connectionLastTouch.remove(hostAndPort); pinnedConnections.remove(hostAndPort); } } catch (ConnectionManagerException e) { throw e; } catch (Exception e) { throw new ConnectionManagerException(MessageFormatter.format( "Failed removing the connection to the provider on host/port {}", hostAndPort).getMessage(), e); } finally { // Now resume the listener. _listener.resume(); } } /** * Implementation to reap CimConnections that have not be in use for some time. */ private class CimConnectionReaper implements Runnable { @Override public void run() { connectionLock.lock(); Thread currentThread = Thread.currentThread(); currentThread.setName(String.format("CimConnectionReaper %d", currentThread.getId())); try { s_logger.debug("CimConnectionReaper start"); int connectionsReaped = 0; // Copy the keys to prevent ConcurrentUpdate exception Set<String> connectionKeys = new HashSet<>(connectionLastTouch.keySet()); for (String hostAndPort : connectionKeys) { // If the connection is in the pinned list, then it's probably being // used for a long period of time, so we will not allow it be reaped // until it is no longer in the pinned list. if (pinnedConnections.containsKey(hostAndPort)) { s_logger.info("Connection {} was pinned, it will not be reaped until it is unpinned", hostAndPort); continue; } Long lastTime = connectionLastTouch.get(hostAndPort); Long diff = System.currentTimeMillis() - lastTime; String timeAndDate = new Date(lastTime).toString(); if (diff >= maxConnectionTTL) { s_logger.info( String.format("Reaping connection %s that was last touched %s (%s)", hostAndPort, timeAndDate, lastTime)); internalRemoveConnection(hostAndPort); connectionsReaped++; } else { s_logger.debug(String.format("Connection %s was last touched %s (%s)", hostAndPort, timeAndDate, lastTime)); } } s_logger.debug("CimConnectionReaper end - There were {} connections reaped", connectionsReaped); } catch (Exception exp) { s_logger.error("Exception occurred", exp); } finally { connectionLock.unlock(); } } } }