/** * Copyright (C) 2000-2016 Atomikos <info@atomikos.com> * * LICENSE CONDITIONS * * See http://www.atomikos.com/Main/WhichLicenseApplies for details. */ package com.atomikos.jms.extra; import java.util.HashMap; import java.util.Map; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.ExceptionListener; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Queue; import javax.jms.Session; import javax.jms.Topic; import javax.transaction.HeuristicMixedException; import javax.transaction.HeuristicRollbackException; import javax.transaction.RollbackException; import javax.transaction.SystemException; import com.atomikos.icatch.jta.UserTransactionManager; import com.atomikos.jms.AtomikosConnectionFactoryBean; import com.atomikos.logging.Logger; import com.atomikos.logging.LoggerFactory; /** * * * Common message-driven session functionality. * */ class MessageConsumerSession { private static final Logger LOGGER = LoggerFactory.createLogger(MessageConsumerSession.class); private AtomikosConnectionFactoryBean factory; private String user; private String password; private Destination destination; private String destinationName; private MessageConsumerSessionProperties properties; private boolean notifyListenerOnClose; private String messageSelector; private boolean daemonThreads; private transient MessageListener listener; protected transient ReceiverThread current; private UserTransactionManager tm; private boolean active; private ExceptionListener exceptionListener; //for durable subscribers only private boolean noLocal; private String subscriberName; private String clientID; private Map<String,Long> messageCounterMap = new HashMap<>(); protected MessageConsumerSession ( MessageConsumerSessionProperties properties ) { this.properties = properties; tm = new UserTransactionManager(); noLocal = false; subscriberName = null; } protected String getSubscriberName() { return subscriberName; } protected void setSubscriberName ( String name ) { this.subscriberName = name; } protected void setNoLocal ( boolean value ) { this.noLocal = value; } protected boolean getNoLocal() { return noLocal; } protected void setAtomikosConnectionFactoryBean ( AtomikosConnectionFactoryBean bean ) { this.factory = bean; } protected AtomikosConnectionFactoryBean getAtomikosConnectionFactoryBean() { return factory; } /** * Sets whether threads should be daemon threads or not. * Default is false. * @param value If true then threads will be daemon threads. */ public void setDaemonThreads ( boolean value ) { this.daemonThreads = value; } /** * Tests whether threads are daemon threads. * @return True if threads are deamons. */ public boolean getDaemonThreads() { return daemonThreads; } /** * Get the message selector (if any) * * @return The selector, or null if none. */ public String getMessageSelector() { return messageSelector; } /** * Set the message selector to use. * * @param selector */ public void setMessageSelector(String selector) { this.messageSelector = selector; } /** * Set the user to create connections with. If the user is not set then the * default connection will be used. * * @param user */ public void setUser(String user) { this.user = user; } /** * Get the user to connect with. * * @return The user or null if no explicit authentication is to be used. */ public String getUser() { return user; } /** * Set the password to use for connecting. This property only needs to be * set if the User property was also set. * * @param password */ public void setPassword(String password) { this.password = password; } /** * Gets the destination. * * @return Null if none was set. */ public Destination getDestination() { return destination; } /** * Sets the destination to listen on. * @param destination */ public void setDestination ( Destination destination ) { this.destination = destination; } /** * Get the transaction timeout in seconds. * * @return */ public int getTransactionTimeout() { return properties.getTransactionTimeout(); } /** * Set the message listener for this session. Only one message listener per * session is allowed. After this method is called, the listener will * receive incoming messages in its onMessage method, in a JTA transaction. * By default, the receiver will commit the transaction unless the onMessage * method throws a runtime exception (in which case rollback will happen). * * If no more messages are desired, then this method should be called a * second time with a null argument. * * @param listener */ public void setMessageListener(MessageListener listener) { this.listener = listener; } /** * Get the message listener of this session, if any. * * @return */ public MessageListener getMessageListener() { return listener; } /** * Start listening for messages. * */ public void startListening() throws JMSException, SystemException { if ( active ) throw new IllegalStateException ( "MessageConsumerSession: startListening() called a second time without stopListening() in between" ); if ( getDestinationName() == null ) throw new JMSException ( "Please set property 'destination' or 'destinationName' first" ); if ( factory == null ) throw new JMSException ( "Please set the ConnectionFactory first" ); tm.setStartupTransactionService ( true ); tm.init(); //disable startup to avoid threads re-start the core //during shutdown!!! (see ISSUE 10084) tm.setStartupTransactionService ( false ); active = true; startNewThread(); StringBuffer msg = new StringBuffer(); msg.append ( "MessageConsumerSession configured with [" ); msg.append ( "user=" ).append( getUser() ).append ( ", " ); msg.append ( "password=" ).append ( password ).append ( ", " ); msg.append ( "transactionTimeout=" ).append ( getTransactionTimeout() ).append ( ", " ); msg.append ( "destination=" ).append( getDestinationName() ).append ( ", " ); msg.append ( "notifyListenerOnClose= " ).append( getNotifyListenerOnClose() ).append( ", " ); msg.append ( "messageSelector=" ).append( getMessageSelector() ).append( ", " ); msg.append ( "daemonThreads=" ).append ( getDaemonThreads() ).append ( ", " ); msg.append ( "messageListener=" ).append ( getMessageListener() ).append ( ", " ); msg.append ( "exceptionListener=" ).append ( getExceptionListener() ).append ( ", " ); msg.append ( "connectionFactory=" ).append ( getAtomikosConnectionFactoryBean() ); msg.append ( "]" ); if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( msg.toString() ); } /** * Gets the destination name, either as set directly or * as set by the destinationName property. * * @return The destination's provider-specific name, or null if none set. */ public String getDestinationName() { String ret = destinationName; if ( ret == null ) { if ( destination instanceof Queue ) { Queue q = ( Queue ) destination; try { ret = q.getQueueName(); } catch (JMSException e) { if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error retrieving queue name" , e ); } } else if ( destination instanceof Topic ) { Topic t = ( Topic ) destination; try { ret = t.getTopicName(); } catch (JMSException e) { if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Error retrieving topic name" , e ); } } } return ret; } /** * Sets the provider-specific destination name. Required, unless * setDestination is called instead. * * @param destinationName */ public void setDestinationName ( String destinationName ) { this.destinationName = destinationName; } protected void startNewThread() { if ( active ) { current = new ReceiverThread (); //FIXED 10082 current.setDaemon ( daemonThreads ); current.start (); if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: started new thread: " + current ); } //if not active: ignore } private synchronized void notifyExceptionListener ( JMSException e ) { if ( exceptionListener != null ) exceptionListener.onException ( e ); } /** * Stop listening for messages. If <b>notifyListenerOnClose</b> is set then * calling this method will indirectly lead to the invocation of the * listener's onMessage method with a null argument (and without a * transaction). This allows receivers to detect shutdown. * */ public void stopListening() { if ( current != null ) { ReceiverThread t = current; // cf issue 62452: next, FIRST set current to null // to allow listener thread to exit // needed because the subsequent JMS cleanup will wait // for the listener thread to finish!!! current = null; t.closeJmsResources ( true ); } tm.close(); active = false; } /** * * Check wether the session is configured to notify the listener upon close. * * @return boolean If true then the listener will receive a null message * when the session is closed. * */ public boolean getNotifyListenerOnClose() { return notifyListenerOnClose; } /** * Set whether the listener should be notified on close. * * @param b */ public void setNotifyListenerOnClose(boolean b) { notifyListenerOnClose = b; } class ReceiverThread extends Thread { private Connection connection; private Session session; private ReceiverThread () { } private synchronized MessageConsumer refreshJmsResources () throws JMSException { MessageConsumer ret = null; if ( user != null ) { connection = factory.createConnection ( user, password ); } else { connection = factory.createConnection (); } if (clientID != null ) { //see http://activemq.apache.org/virtual-destinations.html String connectionClientID = connection.getClientID(); if ( connectionClientID == null ) connection.setClientID(clientID); else LOGGER.logWarning ( "Reusing connection with preset clientID: " + connectionClientID ); } connection.start (); session = connection.createSession ( true, 0 ); if ( getDestination() == null ) { Destination d = DestinationHelper.findDestination ( getDestinationName() , session ); setDestination ( d ); } String subscriberName = getSubscriberName(); if ( subscriberName == null ) { // cf case 33305: only use the noLocal flag if the destination is a topic if ( destination instanceof Topic ) { // topic -> use noLocal ret = session.createConsumer ( destination, getMessageSelector () , getNoLocal() ); } else { // queue -> noLocal flag not defined in JMS 1.1! ret = session.createConsumer ( destination , getMessageSelector() ); } } else { // subscriberName is not null -> topic -> use noLocal flag too ret = session.createDurableSubscriber( ( Topic ) destination , subscriberName , getMessageSelector() , getNoLocal() ); } return ret; } private synchronized void closeJmsResources ( boolean threadWillStop ) { try { if ( session != null ) { if ( threadWillStop ) { try { LOGGER.logInfo ( "MessageConsumerSession: unsubscribing " + subscriberName + "..."); if ( Thread.currentThread() != this ) { //see case 62452 and 80464: wait for listener thread to exit so the subscriber is no longer in use if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: waiting for listener thread to finish..." ); this.join(); if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: waiting done." ); } if (subscriberName != null && properties.getUnsubscribeOnClose()) { LOGGER.logInfo ( "MessageConsumerSession: unsubscribing " + subscriberName + "..."); session.unsubscribe ( subscriberName ); } } catch ( JMSException e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.logDebug ("MessageConsumerSession: Error closing on JMS session", e ); LOGGER.logDebug ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); } } } try { if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: closing JMS session..." ); session.close (); session = null; if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS session closed." ); } catch ( JMSException e ) { if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: Error closing JMS session", e ); if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); } } if ( connection != null ) try { if ( LOGGER.isDebugEnabled() ) LOGGER.logDebug ( "MessageConsumerSession: closing JMS connection..." ); connection.close (); connection = null; if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS connection closed." ); } catch ( JMSException e ) { LOGGER.logWarning ( "MessageConsumerSession: Error closing JMS connection", e ); LOGGER.logWarning ( "MessageConsumerSession: linked exception is " , e.getLinkedException() ); } } catch ( Throwable e ) { LOGGER.logWarning ( "MessageConsumerSession: Unexpected error during close: " , e ); //DON'T rethrow } } public void run () { MessageConsumer receiver = null; try { // FIRST set transaction timeout, to trigger // TM startup if needed; otherwise the logging // to Configuration will not work! tm.setTransactionTimeout ( getTransactionTimeout() ); } catch ( SystemException e ) { LOGGER.logError ( "MessageConsumerSession: Error in JMS thread while setting transaction timeout", e ); } LOGGER.logDebug ( "MessageConsumerSession: Starting JMS listener thread." ); while ( Thread.currentThread () == current ) { if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: JMS listener thread iterating..." ); boolean refresh = false; boolean commit = true; try { Message msg = null; while ( receiver == null ) { try { receiver = refreshJmsResources (); } catch ( JMSException connectionGone ) { LOGGER.logWarning ( "Error refreshing JMS connection" , connectionGone ); closeJmsResources(false); // wait a while to avoid OutOfMemoryError with MQSeries // cf case 73406 Thread.sleep ( getTransactionTimeout() * 1000 / 4 ); } } tm.setTransactionTimeout ( getTransactionTimeout() ); if ( tm.getTransaction () != null ) { LOGGER.logFatal ( "MessageConsumerSession: detected pending transaction: " + tm.getTransaction () ); // this is fatal and should not happen due to cleanup in previous iteration throw new IllegalStateException ( "Can't reuse listener thread with pending transaction!" ); } tm.begin (); // wait for at most half of the tx timeout // cf case 83599: use separate timeout for receive to speedup shutdown msg = receiver.receive ( getReceiveTimeout() * 1000 ); try { if ( msg != null && listener != null && Thread.currentThread () == current ) { LOGGER.logDebug ( "MessageConsumerSession: Consuming message: " + msg.toString () ); checkRedeliveryLimit(msg); listener.onMessage ( msg ); LOGGER.logTrace ( "MessageConsumerSession: Consumed message: " + msg.toString () ); cleanRedeliveryLimit(msg); } else { commit = false; } } catch ( Exception e ) { if ( LOGGER.isDebugEnabled() ) { LOGGER.logDebug ("MessageConsumerSession: Error during JMS processing of message " + msg.toString () + " - rolling back.", e ); } // This happens if the listener generated the error. // In that case, don't refresh the connection but rather // only rollback. There is no reason to assume that the // connection is corrupted here. commit = false; } } catch ( JMSException e ) { LOGGER.logWarning ( "MessageConsumerSession: Error in JMS thread", e ); Exception linkedException = e.getLinkedException(); if ( linkedException != null ) { LOGGER.logWarning ( "Linked JMS exception is: " , linkedException ); } // refresh connection to avoid corruption of thread state. refresh = true; commit = false; notifyExceptionListener ( e ); } catch ( Throwable e ) { LOGGER.logError ("MessageConsumerSession: Error in JMS thread", e ); // Happens if there is an error not generated by the listener; // refresh connection to avoid corruption of thread state. refresh = true; commit = false; JMSException listenerError = new JMSException ( "Unexpected error - please see application log for more info" ); notifyExceptionListener ( listenerError ); } finally { // Make sure no tx exists for thread, or we can't reuse // the thread for later transactions! try { if ( commit ) tm.commit (); else { tm.rollback (); } } catch ( RollbackException e ) { // thread still OK LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); } catch ( HeuristicMixedException e ) { // thread still OK LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); } catch ( HeuristicRollbackException e ) { // thread still OK LOGGER.logWarning ( "MessageConsumerSession: Error in ending transaction", e ); } catch ( Throwable e ) { LOGGER.logWarning ("MessageConsumerSession: Error ending thread tx association", e ); // In this case, we suspend the tx so that it is no longer associated with this thread. // This allows thread reuse for later messages. // If suspend fails, then we can only start a new thread. try { LOGGER.logTrace ( "MessageConsumerSession: Suspending any active transaction..." ); tm.suspend (); } catch ( SystemException err ) { LOGGER.logError ( "MessageConsumerSession: Error suspending transaction", err ); // start new thread to replace this one, because we can't risk a pending transaction try { LOGGER.logTrace ( "MessageConsumerSession: Starting new thread..." ); startNewThread(); } catch ( Throwable fatal ) { // happens if queue or factory no longer set // in this case, we can't do anything else - // just let the current thread exit and log LOGGER.logFatal ( "MessageConsumerSession: Error starting new thread - stopping listener", e ); // set current to null to make this thread exit, // since reuse is impossible due to risk of pending transaction! stopListening (); } } } if ( refresh && Thread.currentThread () == current) { // close resources here and let the actual refresh be done by the next iteration try { receiver.close(); } catch ( Throwable e ) { if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "MessageConsumerSession: Error closing receiver" , e ); } receiver = null; closeJmsResources ( false ); } } } // thread is going to die, close the receiver here. if(receiver != null) { try { receiver.close(); } catch ( Throwable e ) { LOGGER.logTrace ( "MessageConsumerSession: Error closing receiver" , e ); } receiver = null; } LOGGER.logDebug ( "MessageConsumerSession: JMS listener thread exiting." ); if ( listener != null && current == null && notifyListenerOnClose ) { // if this session stops listening (no more threads active) then // notify the listener of shutdown by calling with null argument listener.onMessage ( null ); } } } private void cleanRedeliveryLimit(Message msg) throws JMSException { messageCounterMap.remove(msg.getJMSMessageID()); } private void checkRedeliveryLimit(Message msg) throws JMSException { if (msg.getJMSRedelivered()) { String key = msg.getJMSMessageID(); Long redeliveryCount = messageCounterMap.get(key); if (redeliveryCount == null) { redeliveryCount = 1L; } else { redeliveryCount++; if (redeliveryCount > 5) { LOGGER.logWarning("Possible poison message detected - check https://www.atomikos.com/Documentation/PoisonMessage: " + msg.toString()); } } messageCounterMap.put(key, redeliveryCount); } } /** * Gets the exception listener (if any). * @return Null if no ExceptionListener was set. */ public ExceptionListener getExceptionListener() { return exceptionListener; } /** * Sets the exception listener. The listener will be * notified of connection-level JMS errors. * <b>IMPORTANT:</b> exception listeners will NOT be * notified of any errors thrown by the MessageListener. * Instead, the ExceptionListener mechanism is meant * for system-level connectivity errors towards and from * the underlying message system. * * @param exceptionListener */ public void setExceptionListener ( ExceptionListener exceptionListener ) { this.exceptionListener = exceptionListener; } public void setClientID(String clientID) { this.clientID = clientID; } /** * Gets the receive timeout in seconds. * * @return */ public int getReceiveTimeout() { return properties.getReceiveTimeout(); } }