package org.openamq.client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.openamq.AMQException; import org.openamq.AMQUndeliveredException; import org.openamq.client.protocol.AMQProtocolHandler; import org.openamq.client.protocol.FailoverSupport; import org.openamq.client.state.AMQState; import org.openamq.client.state.listener.SpecificMethodFrameListener; import org.openamq.client.transport.TransportConnection; import org.openamq.framing.AMQFrame; import org.openamq.framing.BasicQosBody; import org.openamq.framing.BasicQosOkBody; import org.openamq.framing.ChannelOpenBody; import org.openamq.framing.ChannelOpenOkBody; import org.openamq.framing.TxSelectBody; import org.openamq.framing.TxSelectOkBody; import org.openamq.jms.ChannelLimitReachedException; import org.openamq.jms.Connection; import org.openamq.jms.ConnectionListener; import javax.jms.ConnectionConsumer; import javax.jms.ConnectionMetaData; import javax.jms.Destination; import javax.jms.ExceptionListener; import javax.jms.JMSException; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueSession; import javax.jms.ServerSessionPool; import javax.jms.Session; import javax.jms.Topic; import javax.jms.TopicConnection; import javax.jms.TopicSession; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.Map; import java.util.StringTokenizer; public class AMQConnection extends Closeable implements Connection, QueueConnection, TopicConnection { private static final Logger _logger = LoggerFactory.getLogger(AMQConnection.class); private final IdFactory _idFactory = new IdFactory(); /** * This is the "root" mutex that must be held when doing anything that could be impacted by failover. * This must be held by any child objects of this connection such as the session, producers and consumers. */ private final Object _failoverMutex = new Object(); /** * Details of a broker that we can connect to. For failover we can know about a number of brokers each of which * we can attempt to use. These brokers must be a member of the cluster. */ public static class BrokerDetail { public String host; public int port; public BrokerDetail(String host, int port) { this.host = host; this.port = port; } public String toString() { return (host + ":" + port); } public boolean equals(Object o) { if (!(o instanceof BrokerDetail)) { return false; } BrokerDetail bd = (BrokerDetail) o; return host.equals(bd.host) && (port == bd.port); } } /** * A channel is roughly analogous to a session. The server can negotiate the maximum number of channels * per session and we must prevent the client from opening too many. Zero means unlimited. */ private long _maximumChannelCount; /** * The maximum size of frame supported by the server */ private long _maximumFrameSize; /** * The protocol handler dispatches protocol events for this connection. For example, when the connection is dropped * the handler deals with this. It also deals with the initial dispatch of any protocol frames to their appropriate * handler. */ private AMQProtocolHandler _protocolHandler; /** * Maps from session id (Integer) to AMQSession instance */ private final Map _sessions = new LinkedHashMap(); private String _clientName; private BrokerDetail[] _brokerDetails; /** * The index into the hostDetails array of the broker to which we are connected */ private int _activeBrokerIndex = -1; /** * The user name to use for authentication */ private String _username; /** * The password to use for authentication */ private String _password; /** * The virtual path to connect to on the AMQ server */ private String _virtualPath; private ExceptionListener _exceptionListener; private ConnectionListener _connectionListener; /** * Whether this connection is started, i.e. whether messages are flowing to consumers. It has no meaning for * message publication. */ private boolean _started; public AMQConnection(String host, int port, String username, String password, String clientName, String virtualPath) throws AMQException { this(new BrokerDetail(host, port), username, password, clientName, virtualPath); } public AMQConnection(String brokerDetails, String username, String password, String clientName, String virtualPath) throws AMQException { this(parseBrokerDetails(brokerDetails), username, password, clientName, virtualPath); } public AMQConnection(BrokerDetail brokerDetail, String username, String password, String clientName, String virtualPath) throws AMQException { this(new BrokerDetail[]{brokerDetail}, username, password, clientName, virtualPath); } public AMQConnection(BrokerDetail[] brokerDetails, String username, String password, String clientName, String virtualPath) throws AMQException { if (brokerDetails == null || brokerDetails.length == 0) { throw new IllegalArgumentException("Broker details must specify at least one broker"); } _brokerDetails = brokerDetails; _clientName = clientName; _username = username; _password = password; _virtualPath = virtualPath; _protocolHandler = new AMQProtocolHandler(this); for (int i = 0; i < brokerDetails.length; i++) { try { makeBrokerConnection(brokerDetails[i]); _activeBrokerIndex = i; // has succeeded so break out the loop now break; } catch (Exception e) { _logger.info("Unable to connect to broker at " + brokerDetails[i], e); } } if (_activeBrokerIndex == -1) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < brokerDetails.length; i++) { buf.append(brokerDetails[i].toString()).append(' '); } throw new AMQException("Unable to connect to any specified broker in list " + buf.toString()); } } protected AMQConnection(String username, String password, String clientName, String virtualPath) { _clientName = clientName; _username = username; _password = password; _virtualPath = virtualPath; } private static BrokerDetail[] parseBrokerDetails(String brokerDetails) { if (brokerDetails == null) { throw new IllegalArgumentException("Broker string cannot be null"); } LinkedList ll = new LinkedList(); StringTokenizer tokenizer = new StringTokenizer(brokerDetails, ";"); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); int index = token.indexOf(":"); if (index == -1) { throw new IllegalArgumentException("Invalid broker string: " + token + ". Must be in format host:port"); } else { int port = Integer.parseInt(token.substring(index + 1)); int hostStart = 0; if (token.charAt(0) == '$') { hostStart = 1; } BrokerDetail bd = new BrokerDetail(token.substring(hostStart, index), port); ll.add(bd); } } BrokerDetail[] bd = new BrokerDetail[ll.size()]; return (BrokerDetail[]) ll.toArray(bd); } private void makeBrokerConnection(BrokerDetail brokerDetail) throws IOException, AMQException { TransportConnection.getInstance().connect(_protocolHandler, brokerDetail); // this blocks until the connection has been set up or when an error has prevented the connection being // set up _protocolHandler.attainState(AMQState.CONNECTION_OPEN); } public boolean attemptReconnection(String host, int port) { // first we find the host port combo in the broker details array int index = -1; BrokerDetail bd = new BrokerDetail(host, port); for (int i = 0; i < _brokerDetails.length; i++) { if (_brokerDetails[i].equals(bd)) { index = i; break; } } if (index == -1) { int len = _brokerDetails.length + 1; BrokerDetail[] newDetails = new BrokerDetail[len]; System.arraycopy(_brokerDetails, 0, newDetails, 0, _brokerDetails.length); index = len - 1; newDetails[index] = bd; } try { makeBrokerConnection(bd); _activeBrokerIndex = index; return true; } catch (Exception e) { _logger.info("Unable to connect to broker at " + bd); } return false; } public boolean attemptReconnection() { if (_activeBrokerIndex < 0) { //failed to connect to first broker _activeBrokerIndex = 0; } else { //retry the current broker first: try { _logger.info("Retrying " + _brokerDetails[_activeBrokerIndex]); makeBrokerConnection(_brokerDetails[_activeBrokerIndex]); return true; } catch (Exception e) { _logger.info("Unable to reconnect to broker at " + _brokerDetails[_activeBrokerIndex]); } } //then try the others: for (int i = 0; i < _brokerDetails.length; i++) { if (i == _activeBrokerIndex) { continue; } try { makeBrokerConnection(_brokerDetails[i]); _activeBrokerIndex = i; return true; } catch (Exception e) { _logger.info("Unable to connect to broker at " + _brokerDetails[i]); } } //connection unsuccessful return false; } /** * Get the details of the currently active broker * * @return null if no broker is active (i.e. no successful connection has been made, or * the BrokerDetail instance otherwise */ public BrokerDetail getActiveBrokerDetails() { if (_activeBrokerIndex < 0) { return null; } return _brokerDetails[_activeBrokerIndex]; } public Session createSession(final boolean transacted, final int acknowledgeMode) throws JMSException { return createSession(transacted, acknowledgeMode, AMQSession.DEFAULT_PREFETCH); } public org.openamq.jms.Session createSession(final boolean transacted, final int acknowledgeMode, final int prefetch) throws JMSException { checkNotClosed(); if (channelLimitReached()) { throw new ChannelLimitReachedException(_maximumChannelCount); } else { return (org.openamq.jms.Session) new FailoverSupport() { public Object operation() throws JMSException { int channelId = _idFactory.getChannelId(); AMQFrame frame = ChannelOpenBody.createAMQFrame(channelId, null); if (_logger.isDebugEnabled()) { _logger.debug("Write channel open frame for channel id " + channelId); } // we must create the session and register it before actually sending the frame to the server to // open it, so that there is no window where we could receive data on the channel and not be set up to // handle it appropriately. AMQSession session = new AMQSession(AMQConnection.this, channelId, transacted, acknowledgeMode, prefetch); _protocolHandler.addSessionByChannel(channelId, session); registerSession(channelId, session); try { _protocolHandler.writeCommandFrameAndWaitForReply(frame, new SpecificMethodFrameListener(channelId, ChannelOpenOkBody.class)); _protocolHandler.writeCommandFrameAndWaitForReply(BasicQosBody.createAMQFrame(channelId, 0, prefetch, false), new SpecificMethodFrameListener(channelId, BasicQosOkBody.class)); if(transacted) { if (_logger.isDebugEnabled()) { _logger.debug("Issuing TxSelect for " + channelId); } _protocolHandler.writeCommandFrameAndWaitForReply( TxSelectBody.createAMQFrame(channelId), new SpecificMethodFrameListener(channelId, TxSelectOkBody.class) ); } } catch (AMQException e) { _protocolHandler.removeSessionByChannel(channelId); deregisterSession(channelId); throw new JMSException("Error creating session: " + e); } if (_started) { session.start(); } return session; } }.execute(this); } } public QueueSession createQueueSession(boolean transacted, int acknowledgeMode) throws JMSException { return (QueueSession) createSession(transacted, acknowledgeMode); } public TopicSession createTopicSession(boolean transacted, int acknowledgeMode) throws JMSException { return (TopicSession) createSession(transacted, acknowledgeMode); } private boolean channelLimitReached() { return _maximumChannelCount != 0 && _sessions.size() == _maximumChannelCount; } public String getClientID() throws JMSException { checkNotClosed(); return _clientName; } public void setClientID(String clientID) throws JMSException { checkNotClosed(); _clientName = clientID; } public ConnectionMetaData getMetaData() throws JMSException { checkNotClosed(); // TODO Auto-generated method stub return null; } public ExceptionListener getExceptionListener() throws JMSException { checkNotClosed(); return _exceptionListener; } public void setExceptionListener(ExceptionListener listener) throws JMSException { checkNotClosed(); _exceptionListener = listener; } /** * Start the connection, i.e. start flowing messages. Note that this method must be called only from a single thread * and is not thread safe (which is legal according to the JMS specification). * * @throws JMSException */ public void start() throws JMSException { checkNotClosed(); if (!_started) { final Iterator it = _sessions.entrySet().iterator(); while (it.hasNext()) { final AMQSession s = (AMQSession) ((Map.Entry) it.next()).getValue(); s.start(); } _started = true; } } public void stop() throws JMSException { checkNotClosed(); if (_started) { for (Iterator i = _sessions.values().iterator(); i.hasNext();) { ((AMQSession) i.next()).stop(); } _started = false; } } public void close() throws JMSException { synchronized (getFailoverMutex()) { if (!_closed.getAndSet(true)) { try { closeAllSessions(null); _protocolHandler.closeConnection(); } catch (AMQException e) { throw new JMSException("Error closing connection: " + e); } } } } /** * Marks all sessions and their children as closed without sending any protocol messages. Useful when * you need to mark objects "visible" in userland as closed after failover or other significant event that * impacts the connection. * <p/> * The caller must hold the failover mutex before calling this method. */ private void markAllSessionsClosed() { final LinkedList sessionCopy = new LinkedList(_sessions.values()); final Iterator it = sessionCopy.iterator(); while (it.hasNext()) { final AMQSession session = (AMQSession) it.next(); session.markClosed(); } _sessions.clear(); } /** * Close all the sessions, either due to normal connection closure or due to an error occurring. * * @param cause if not null, the error that is causing this shutdown * <p/> * The caller must hold the failover mutex before calling this method. */ private void closeAllSessions(Throwable cause) throws JMSException { final LinkedList sessionCopy = new LinkedList(_sessions.values()); final Iterator it = sessionCopy.iterator(); JMSException sessionException = null; while (it.hasNext()) { final AMQSession session = (AMQSession) it.next(); if (cause != null) { session.closed(cause); } else { try { session.close(); } catch (JMSException e) { _logger.error("Error closing session: " + e); sessionException = e; } } } _sessions.clear(); if (sessionException != null) { throw sessionException; } } public ConnectionConsumer createConnectionConsumer(Destination destination, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { checkNotClosed(); return null; } public ConnectionConsumer createConnectionConsumer(Queue queue, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { checkNotClosed(); return null; } public ConnectionConsumer createConnectionConsumer(Topic topic, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { checkNotClosed(); return null; } public ConnectionConsumer createDurableConnectionConsumer(Topic topic, String subscriptionName, String messageSelector, ServerSessionPool sessionPool, int maxMessages) throws JMSException { // TODO Auto-generated method stub checkNotClosed(); return null; } IdFactory getIdFactory() { return _idFactory; } public long getMaximumChannelCount() { checkNotClosed(); return _maximumChannelCount; } public void setConnectionListener(ConnectionListener listener) { _connectionListener = listener; } public ConnectionListener getConnectionListener() { return _connectionListener; } public void setMaximumChannelCount(long maximumChannelCount) { checkNotClosed(); _maximumChannelCount = maximumChannelCount; } public void setMaximumFrameSize(long frameMax) { _maximumFrameSize = frameMax; } public long getMaximumFrameSize() { return _maximumFrameSize; } public Map getSessions() { return _sessions; } public String getUsername() { return _username; } public String getPassword() { return _password; } public String getVirtualPath() { return _virtualPath; } public AMQProtocolHandler getProtocolHandler() { return _protocolHandler; } public void bytesSent(long writtenBytes) { if (_connectionListener != null) { _connectionListener.bytesSent(writtenBytes); } } public void bytesReceived(long receivedBytes) { if (_connectionListener != null) { _connectionListener.bytesReceived(receivedBytes); } } /** * Fire the preFailover event to the registered connection listener (if any) * * @param redirect true if this is the result of a redirect request rather than a connection error * @return true if no listener or listener does not veto change */ public boolean firePreFailover(boolean redirect) { boolean proceed = true; if (_connectionListener != null) { proceed = _connectionListener.preFailover(redirect); } return proceed; } /** * Fire the preResubscribe event to the registered connection listener (if any). If the listener * vetoes resubscription then all the sessions are closed. * * @return true if no listener or listener does not veto resubscription. * @throws JMSException */ public boolean firePreResubscribe() throws JMSException { if (_connectionListener != null) { boolean resubscribe = _connectionListener.preResubscribe(); if (!resubscribe) { markAllSessionsClosed(); } return resubscribe; } else { return true; } } /** * Fires a failover complete event to the registered connection listener (if any). */ public void fireFailoverComplete() { if (_connectionListener != null) { _connectionListener.failoverComplete(); } } /** * In order to protect the consistency of the connection and its child sessions, consumers and producers, * the "failover mutex" must be held when doing any operations that could be corrupted during failover. * * @return a mutex. Guaranteed never to change for the lifetime of this connection even if failover occurs. */ public final Object getFailoverMutex() { return _failoverMutex; } /** * If failover is taking place this will block until it has completed. If failover * is not taking place it will return immediately. * * @throws InterruptedException */ public void blockUntilNotFailingOver() throws InterruptedException { _protocolHandler.blockUntilNotFailingOver(); } /** * Invoked by the AMQProtocolSession when a protocol session exception has occurred. * This method sends the exception to a JMS exception listener, if configured, and * propagates the exception to sessions, which in turn will propagate to consumers. * This allows synchronous consumers to have exceptions thrown to them. * * @param cause the exception */ public void exceptionReceived(Throwable cause) { JMSException je = null; if (_exceptionListener != null) { if (cause instanceof JMSException) { je = (JMSException) cause; } else { je = new JMSException("Exception thrown against " + toString() + ": " + cause); if (cause instanceof Exception) { je.setLinkedException((Exception) cause); } } // in the case of an IOException, MINA has closed the protocol session so we set _closed to true // so that any generic client code that tries to close the connection will not mess up this error // handling sequence if (cause instanceof IOException) { _closed.set(true); } _exceptionListener.onException(je); } // TODO: this is nasty. Review this (GB change) if (je == null || !(je.getLinkedException() instanceof AMQUndeliveredException)) { try { _closed.set(true); closeAllSessions(cause); } catch (JMSException e) { _logger.error("Error closing all sessions: " + e, e); } } } void registerSession(int channelId, AMQSession session) { _sessions.put(new Integer(channelId), session); } void deregisterSession(int channelId) { _sessions.remove(new Integer(channelId)); } /** * For all sessions, and for all consumers in those sessions, resubscribe. This is called during failover handling. * The caller must hold the failover mutex before calling this method. */ public void resubscribeSessions() throws AMQException { ArrayList sessions = new ArrayList(_sessions.values()); for (Iterator it = sessions.iterator(); it.hasNext();) { AMQSession s = (AMQSession) it.next(); _protocolHandler.addSessionByChannel(s.getChannelId(), s); reopenChannel(s.getChannelId(), s.getDefaultPrefetch()); s.resubscribe(); } } private void reopenChannel(int channelId, int prefetch) throws AMQException { AMQFrame frame = ChannelOpenBody.createAMQFrame(channelId, null); try { _protocolHandler.writeCommandFrameAndWaitForReply(frame, new SpecificMethodFrameListener(channelId, ChannelOpenOkBody.class)); _protocolHandler.writeFrame(BasicQosBody.createAMQFrame(channelId, 0, prefetch, false)); } catch (AMQException e) { _protocolHandler.removeSessionByChannel(channelId); deregisterSession(channelId); throw new AMQException("Error reopening channel " + channelId + " after failover: " + e); } } public String toString() { StringBuffer buf = new StringBuffer("AMQConnection:\n"); if (_activeBrokerIndex == -1) { buf.append("No active broker connection"); } else { buf.append("Host: ").append(String.valueOf(_brokerDetails[_activeBrokerIndex].host)); buf.append("\nPort: ").append(String.valueOf(_brokerDetails[_activeBrokerIndex].port)); } buf.append("\nVirtual Path: ").append(String.valueOf(_virtualPath)); buf.append("\nClient ID: ").append(String.valueOf(_clientName)); buf.append("\nActive session count: ").append(_sessions == null ? 0 : _sessions.size()); return buf.toString(); } }