package org.openamq.client.protocol;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandlerAdapter;
import org.apache.mina.common.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.openamq.AMQDisconnectedException;
import org.openamq.AMQException;
import org.openamq.client.AMQConnection;
import org.openamq.client.AMQSession;
import org.openamq.client.state.AMQState;
import org.openamq.client.state.AMQStateManager;
import org.openamq.client.state.listener.ConnectionCloseOkListener;
import org.openamq.client.state.listener.SpecificMethodFrameListener;
import org.openamq.codec.AMQCodecFactory;
import org.openamq.framing.*;
import java.util.Iterator;
public class AMQProtocolHandler extends IoHandlerAdapter
{
private static final Logger _logger = LoggerFactory.getLogger(AMQProtocolHandler.class);
/**
* The connection that this protocol handler is associated with. There is a 1-1
* mapping between connection instances and protocol handler instances.
*/
private AMQConnection _connection;
/**
* Our wrapper for a protocol session that provides access to session values
* in a typesafe manner.
*/
private volatile AMQProtocolSession _protocolSession;
private AMQStateManager _stateManager = new AMQStateManager();
private final CopyOnWriteArraySet _frameListeners = new CopyOnWriteArraySet();
/**
* We create the failover handler when the session is created since it needs a reference to the IoSession in order
* to be able to send errors during failover back to the client application. The session won't be available in the
* case where we failing over due to a Connection.Redirect message from the broker.
*/
private FailoverHandler _failoverHandler;
/**
* This flag is used to track whether failover is being attempted. It is used to prevent the application constantly
* attempting failover where it is failing.
*/
private FailoverState _failoverState = FailoverState.NOT_STARTED;
private CountDownLatch _failoverLatch;
/**
* When failover is required, we need a separate thread to handle the establishment of the new connection and
* the transfer of subscriptions.
*
* The reason this needs to be a separate thread is because you cannot do this work inside the MINA IO processor
* thread. One significant task is the connection setup which involves a protocol exchange until a particular state
* is achieved. However if you do this in the MINA thread, you have to block until the state is achieved which means
* the IO processor is not able to do anything at all.
*/
private class FailoverHandler implements Runnable
{
private final IoSession _session;
/**
* Used where forcing the failover host
*/
private String _host;
/**
* Used where forcing the failover port
*/
private int _port;
public FailoverHandler(IoSession session)
{
_session = session;
}
public void run()
{
_failoverLatch = new CountDownLatch(1);
// we wake up listeners. If they can handle failover, they will extend the
// FailoverSupport class and will in turn block on the latch until failover
// has completed before retrying the operation
propagateExceptionToWaiters(new FailoverException("Failing over about to start"));
// since failover impacts several structures we protect them all with a single mutex. These structures
// are also in child objects of the connection. This allows us to manipulate them without affecting
// client code which runs in a separate thread
synchronized (_connection.getFailoverMutex())
{
_logger.info("Starting failover process");
// we switch in a new state manager temporarily so that the interaction to get to the "connection open"
// state works, without us having to terminate any existing "state waiters". We could theoretically
// have a state waiter waiting until the connection is closed for some reason. Or in future we may have
// a slightly more complex state model therefore I felt it was worthwhile doing this
AMQStateManager existingStateManager = _stateManager;
_stateManager = new AMQStateManager();
if (!_connection.firePreFailover(_host != null))
{
_stateManager = existingStateManager;
if (_host != null)
{
_connection.exceptionReceived(new AMQDisconnectedException("Redirect was vetoed by client"));
}
else
{
_connection.exceptionReceived(new AMQDisconnectedException("Failover was vetoed by client"));
}
_failoverLatch.countDown();
_failoverLatch = null;
return;
}
boolean failoverSucceeded;
// when host is non null we have a specified failover host otherwise we all the client to cycle through
// all specified hosts
if (_host != null)
{
failoverSucceeded = _connection.attemptReconnection(_host, _port);
}
else
{
failoverSucceeded = _connection.attemptReconnection();
}
if (!failoverSucceeded)
{
_stateManager = existingStateManager;
_connection.exceptionReceived(new AMQDisconnectedException("Server closed connection and no failover " +
"was successful"));
}
else
{
_stateManager = existingStateManager;
try
{
if (_connection.firePreResubscribe())
{
_logger.info("Resubscribing on new connection");
_connection.resubscribeSessions();
}
else
{
_logger.info("Client vetoed automatic resubscription");
}
_connection.fireFailoverComplete();
_failoverState = FailoverState.NOT_STARTED;
_logger.info("Connection failover completed successfully");
}
catch (Exception e)
{
_logger.info("Failover process failed - exception being propagated by protocol handler");
_failoverState = FailoverState.FAILED;
try
{
exceptionCaught(_session, e);
}
catch (Exception ex)
{
_logger.error("Error notifying protocol session of error: " + ex, ex);
}
}
}
}
_failoverLatch.countDown();
}
}
public AMQProtocolHandler(AMQConnection con)
{
_connection = con;
// we add a proxy for the state manager so that we can substitute the state manager easily in this class.
// We substitute the state manager when performing failover
_frameListeners.add(new AMQMethodListener()
{
public boolean methodReceived(AMQMethodEvent evt) throws AMQException
{
return _stateManager.methodReceived(evt);
}
public void error(Exception e)
{
_stateManager.error(e);
}
});
}
public void sessionCreated(IoSession session) throws Exception
{
_logger.debug("Protocol session created for session " + System.identityHashCode(session));
_failoverHandler = new FailoverHandler(session);
final ProtocolCodecFilter pcf = new ProtocolCodecFilter(new AMQCodecFactory(false));
if (Boolean.getBoolean("amqj.shared_read_write_pool"))
{
session.getFilterChain().addBefore("AsynchronousWriteFilter", "protocolFilter", pcf);
}
else
{
session.getFilterChain().addLast("protocolFilter", pcf);
}
_protocolSession = new AMQProtocolSession(this, session, _connection);
_protocolSession.init();
}
public void sessionOpened(IoSession session) throws Exception
{
}
/**
* When the broker connection dies we can either get sessionClosed() called or exceptionCaught() followed by
* sessionClosed() depending on whether we were trying to send data at the time of failure.
* @param session
* @throws Exception
*/
public void sessionClosed(IoSession session) throws Exception
{
_logger.info("Session closed called with failover state currently " + _failoverState);
if(_failoverState == FailoverState.IN_PROGRESS && !_connection.isClosed())
{
String message=session.getRemoteAddress().toString()+" closed. Remote server closed connection";
_logger.warn(message);
_stateManager.error(new Exception(message));
}
else if (_failoverState == FailoverState.NOT_STARTED && !_connection.isClosed())
{
_failoverState = FailoverState.IN_PROGRESS;
// see javadoc for FailoverHandler to see rationale for separate thread
new Thread(_failoverHandler).start();
}
_logger.info("Protocol Session [" + this + "] closed");
}
public void sessionIdle(IoSession session, IdleStatus status) throws Exception
{
_logger.debug("Protocol Session [" + this + ":" + session + "] idle: " + status);
if(IdleStatus.WRITER_IDLE.equals(status))
{
//write heartbeat frame:
_logger.debug("Sent heartbeat");
session.write(HeartbeatBody.FRAME);
HeartbeatDiagnostics.sent();
}
else if(IdleStatus.READER_IDLE.equals(status))
{
//failover:
HeartbeatDiagnostics.timeout();
_logger.warn("Timed out while waiting for heartbeat from peer.");
session.close();
}
}
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if (_failoverState == FailoverState.NOT_STARTED)
{
_logger.info("Exception caught therefore going to attempt failover: " + cause, cause);
// this will attemp failover
sessionClosed(session);
}
// we reach this point if failover was attempted and failed therefore we need to let the calling app
// know since we cannot recover the situation
else if (_failoverState == FailoverState.FAILED)
{
_logger.error("Exception caught by protocol handler: " + cause, cause);
_connection.exceptionReceived(cause);
// we notify the state manager of the error in case we have any clients waiting on a state
// change. Those "waiters" will be interrupted and can handle the exception
AMQException amqe = new AMQException("Protocol handler error: " + cause, cause);
propagateExceptionToWaiters(amqe);
}
}
/**
* There are two cases where we have other threads potentially blocking for events to be handled by this
* class. These are for the state manager (waiting for a state change) or a frame listener (waiting for a
* particular type of frame to arrive). When an error occurs we need to notify these waiters so that they can
* react appropriately.
* @param e the exception to propagate
*/
private void propagateExceptionToWaiters(Exception e)
{
_stateManager.error(e);
final Iterator it = _frameListeners.iterator();
while (it.hasNext())
{
final AMQMethodListener ml = (AMQMethodListener) it.next();
ml.error(e);
}
}
private static int _messageReceivedCount;
public void messageReceived(IoSession session, Object message) throws Exception
{
if (_messageReceivedCount++ % 1000 == 0)
{
_logger.debug("Received " + _messageReceivedCount + " protocol messages");
}
Iterator it = _frameListeners.iterator();
AMQFrame frame = (AMQFrame) message;
HeartbeatDiagnostics.received(frame.bodyFrame instanceof HeartbeatBody);
if (frame.bodyFrame instanceof AMQMethodBody)
{
if (_logger.isDebugEnabled())
{
_logger.debug("Method frame received: " + frame);
}
final AMQMethodEvent evt = new AMQMethodEvent(frame.channel, (AMQMethodBody)frame.bodyFrame, _protocolSession);
try
{
boolean wasAnyoneInterested = false;
while (it.hasNext())
{
final AMQMethodListener listener = (AMQMethodListener) it.next();
wasAnyoneInterested = listener.methodReceived(evt) || wasAnyoneInterested;
}
if (!wasAnyoneInterested)
{
throw new AMQException("AMQMethodEvent " + evt + " was not processed by any listener.");
}
}
catch (AMQException e)
{
it = _frameListeners.iterator();
while (it.hasNext())
{
final AMQMethodListener listener = (AMQMethodListener) it.next();
listener.error(e);
}
exceptionCaught(session, e);
}
}
else if (frame.bodyFrame instanceof ContentHeaderBody)
{
_protocolSession.messageContentHeaderReceived(frame.channel,
(ContentHeaderBody) frame.bodyFrame);
}
else if (frame.bodyFrame instanceof ContentBody)
{
_protocolSession.messageContentBodyReceived(frame.channel,
(ContentBody) frame.bodyFrame);
}
else if (frame.bodyFrame instanceof HeartbeatBody)
{
_logger.debug("Received heartbeat");
}
_connection.bytesReceived(_protocolSession.getIoSession().getReadBytes());
}
private static int _messagesOut;
public void messageSent(IoSession session, Object message) throws Exception
{
if (_messagesOut++ % 1000 == 0)
{
_logger.debug("Sent " + _messagesOut + " protocol messages");
}
_connection.bytesSent(session.getWrittenBytes());
if (_logger.isDebugEnabled())
{
_logger.debug("Sent frame " + message);
}
}
public void addFrameListener(AMQMethodListener listener)
{
_frameListeners.add(listener);
}
public void removeFrameListener(AMQMethodListener listener)
{
_frameListeners.remove(listener);
}
public void attainState(AMQState s) throws AMQException
{
_stateManager.attainState(s);
}
/**
* Convenience method that writes a frame to the protocol session. Equivalent
* to calling getProtocolSession().write().
*
* @param frame the frame to write
*/
public void writeFrame(AMQDataBlock frame)
{
_protocolSession.writeFrame(frame);
}
public void writeFrame(AMQDataBlock frame, boolean wait)
{
_protocolSession.writeFrame(frame, wait);
}
/**
* Convenience method that writes a frame to the protocol session and waits for
* a particular response. Equivalent to calling getProtocolSession().write() then
* waiting for the response.
* @param frame
* @param listener the blocking listener. Note the calling thread will block.
*/
public AMQMethodEvent writeCommandFrameAndWaitForReply(AMQFrame frame,
BlockingMethodFrameListener listener)
throws AMQException
{
_frameListeners.add(listener);
_protocolSession.writeFrame(frame);
return listener.blockForFrame();
// When control resumes before this line, a reply will have been received
// that matches the criteria defined in the blocking listener
}
/**
* Convenience method to register an AMQSession with the protocol handler. Registering
* a session with the protocol handler will ensure that messages are delivered to the
* consumer(s) on that session.
*
* @param channelId the channel id of the session
* @param session the session instance.
*/
public void addSessionByChannel(int channelId, AMQSession session)
{
_protocolSession.addSessionByChannel(channelId, session);
}
/**
* Convenience method to deregister an AMQSession with the protocol handler.
* @param channelId then channel id of the session
*/
public void removeSessionByChannel(int channelId)
{
_protocolSession.removeSessionByChannel(channelId);
}
public void closeSession(AMQSession session) throws AMQException
{
BlockingMethodFrameListener listener = new SpecificMethodFrameListener(session.getChannelId(),
ChannelCloseOkBody.class);
_frameListeners.add(listener);
_protocolSession.closeSession(session);
_logger.debug("Blocking for channel close frame for channel " + session.getChannelId());
listener.blockForFrame();
_logger.debug("Received channel close frame");
// When control resumes at this point, a reply will have been received that
// indicates the broker has closed the channel successfully
}
public void closeConnection() throws AMQException
{
BlockingMethodFrameListener listener = new ConnectionCloseOkListener();
_frameListeners.add(listener);
_stateManager.changeState(AMQState.CONNECTION_CLOSING);
// TODO: Polish
final AMQFrame frame = ConnectionCloseBody.createAMQFrame(0, AMQConstant.REPLY_SUCCESS.getCode(),
"JMS client is closing the connection.", 0, 0);
writeFrame(frame);
_logger.debug("Blocking for connection close ok frame");
listener.blockForFrame();
_protocolSession.closeProtocolSession();
}
/**
* @return the number of bytes read from this protocol session
*/
public long getReadBytes()
{
return _protocolSession.getIoSession().getReadBytes();
}
/**
* @return the number of bytes written to this protocol session
*/
public long getWrittenBytes()
{
return _protocolSession.getIoSession().getWrittenBytes();
}
public void failover(String host, int port)
{
_failoverHandler._host = host;
_failoverHandler._port = port;
// see javadoc for FailoverHandler to see rationale for separate thread
new Thread(_failoverHandler).start();
}
public void blockUntilNotFailingOver() throws InterruptedException
{
if (_failoverLatch != null)
{
_failoverLatch.await();
}
}
public String generateQueueName()
{
return _protocolSession.generateQueueName();
}
}