/* * Copyright 2002-2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jadira.jms.container; import java.util.ArrayList; import java.util.List; import javax.jms.Connection; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Session; import org.jadira.jms.mdp.AbstractMessageDriven; import org.springframework.jms.connection.ConnectionFactoryUtils; import org.springframework.jms.connection.JmsResourceHolder; import org.springframework.jms.connection.SingleConnectionFactory; import org.springframework.jms.listener.AbstractMessageListenerContainer; import org.springframework.jms.listener.AbstractPollingMessageListenerContainer; import org.springframework.jms.listener.DefaultMessageListenerContainer; import org.springframework.jms.support.JmsUtils; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionSynchronizationManager; /** * BatchedMessageListenerContainer is an extension of Spring's {@link DefaultMessageListenerContainer} which batches multiple message reads into each single transaction. Batching message reads * typically provides significant enhancement to the throughput of message reads in message queue environments. * <p> * To use this class you must inject a transaction manager via {@link AbstractPollingMessageListenerContainer#setTransactionManager(org.springframework.transaction.PlatformTransactionManager)}. * </p> * <p> * The class performs a blocking read for the first message read in any transaction. The blocking duration is determined by {@link AbstractPollingMessageListenerContainer#setReceiveTimeout(long)}. * Subsequent messages up to the configured {@link #setMaxMessagesPerTransaction(int)} batch limit (which defaults to 150) are performed as non-blocking reads, with the bach completing as soon as the * message queue cannot provide further messages. * </p> * <p> * Users of this class must handle rollback appropriately. A rollback triggered by failure processing a single message will cause all the messages in the transaction to rollback. It is recommended to * design you message processing so that rollback only occurs for fatal, unexpected and unrecoverable errors such as a failure in the infrastructure. You should handle other errors by, for example, * delivering messages directly to an error queue rather than throwing an exception. To assist in constructing this pattern, the {@link AbstractMessageDriven} POJO is also provided which provides the * basic framework for implementing a {@link MessageListener} that is aligned with this contract. * </p> * <p> * The class contains an optional feature called RetryMitigation which is enabled by default. RetryMitigation prevents further messages being read in a batch if any message is identified as being one that * is redelivered. When retryMitigation is enabled, any failure in processing will also trigger a pessimistic message mode. Once in pessimistic message mode messages are read one message at a time. This mode remains in place * until either the number of messages in a batch multiplied by the number of concurrent consumers have been read since the last failure, or the queue cannot provide further messages (i.e. is empty). * The aim of this feature is to reduce the likelihood of messages reaching the redelivery limit due to a bad message in the batch. * </p> * You can also configure the class to conclude any batch when a redelivered message is encountered (again the default behaviour). This feature complements RetryMitigation. * <p> * NB. Due to the design and structure of Spring's {@link DefaultMessageListenerContainer} and its superclasses, implementing this class must by necessity duplicate certain parts of * {@link DefaultMessageListenerContainer}. Consequently, this class has been managed at a source code level as a derivative of {@link DefaultMessageListenerContainer} and copyright messages and * attributions reflect this. * </p> * @author Juergen Hoeller was the original author of the {@link DefaultMessageListenerContainer}. The class was modified, extended and renamed to enable batching by Chris Pheby. */ public class BatchedMessageListenerContainer extends DefaultMessageListenerContainer { /** * Default number of messages to read per transaction */ public static final int DEFAULT_BATCH_SIZE = 150; /** * Number of messages to read per transaction */ private int maxMessagesPerTransaction = DEFAULT_BATCH_SIZE; private final MessageListenerContainerResourceFactory transactionalResourceFactory = new MessageListenerContainerResourceFactory(); private boolean retryMitigation = true; private boolean concludeBatchOnRedeliveredMessage = true; private volatile boolean pessimisticMessageMode = false; private volatile int pessimisticMessageReads = 0; /** * Create a new instance */ public BatchedMessageListenerContainer() { } /** * Configures the maximum number of messages that can be read in a single transaction * @param maxMessagesPerTransaction The requested maximum number of messages per transaction */ public void setMaxMessagesPerTransaction(int maxMessagesPerTransaction) { this.maxMessagesPerTransaction = maxMessagesPerTransaction; } /** * Get the configured maximum number of messages per transaction * @return The maximum number of messages per transaction */ public int getMaxMessagesPerTransaction() { return maxMessagesPerTransaction; } /** * True if the instance attempts to mitigate the problems arising when messages in a batch * are all retried when a poison message is encountered * @return True if RetryMitigation is enabled */ public boolean isRetryMitigation() { return retryMitigation; } /** * Enables or disables the RetryMitigation functionality * @param retryMitigation True if RetryMitigation is to be enabled, false otherwise */ public void setRetryMitigation(boolean retryMitigation) { this.retryMitigation = retryMitigation; } /** * True if seeing a redelivered message will conclude the current batch * @return True if ConcludeBatchOnRedeliveredMessage is enabled */ public boolean isConcludeBatchOnRedeliveredMessage() { return concludeBatchOnRedeliveredMessage; } /** * Enables or disables the ConcludeBatchOnRedeliveredMessage functionality * @param concludeBatchOnRedeliveredMessage True if ConcludeBatchOnRedeliveredMessage is to be enabled, false otherwise */ public void setConcludeBatchOnRedeliveredMessage(boolean concludeBatchOnRedeliveredMessage) { this.concludeBatchOnRedeliveredMessage = concludeBatchOnRedeliveredMessage; } @Override protected boolean doReceiveAndExecute(Object invoker, Session session, MessageConsumer consumer, TransactionStatus status) throws JMSException { Connection connectionToClose = null; Session sessionToClose = null; MessageConsumer consumerToClose = null; final List<Message> messages; Message message; try { boolean transactional = false; if (session == null) { session = ConnectionFactoryUtils.doGetTransactionalSession(getConnectionFactory(), transactionalResourceFactory, true); transactional = (session != null); } if (session == null) { final Connection connection; if (sharedConnectionEnabled()) { connection = getSharedConnection(); } else { connection = createConnection(); connectionToClose = connection; connection.start(); } session = createSession(connection); sessionToClose = session; } if (consumer == null) { consumer = createListenerConsumer(session); consumerToClose = consumer; } messages = new ArrayList<Message>(); message = receiveMessage(consumer); if (message != null) { messages.add(message); if (logger.isDebugEnabled()) { logger.debug("Received message of type [" + message.getClass() + "] from consumer [" + consumer + "] of " + (transactional ? "transactional " : "") + "session [" + session + "]"); } if (pessimisticMessageMode) { pessimisticMessageReads = pessimisticMessageReads + 1; } } else { pessimisticMessageMode = false; } int count = 0; // Check the delivery account so we can stop batching when we hit a redelivered message final int deliveryCount = (concludeBatchOnRedeliveredMessage && message.propertyExists("JMSXDeliveryCount")) ? message.getIntProperty("JMSXDeliveryCount") : -1; while ((message != null) && (++count < maxMessagesPerTransaction) && (!retryMitigation || !pessimisticMessageMode) && (!concludeBatchOnRedeliveredMessage || deliveryCount < 2)) { message = receiveMessageNoWait(consumer); if (message != null) { messages.add(message); if (logger.isDebugEnabled()) { logger.debug("Received message of type [" + message.getClass() + "] from consumer [" + consumer + "] of " + (transactional ? "transactional " : "") + "session [" + session + "]"); } if (pessimisticMessageMode) { pessimisticMessageReads = pessimisticMessageReads + 1; } } else { pessimisticMessageMode = false; } } if (pessimisticMessageMode && (pessimisticMessageReads == (maxMessagesPerTransaction * this.getConcurrentConsumers()))) { pessimisticMessageMode = false; } if (messages.size() > 0) { // Only if messages were collected, notify the listener to consume the same. boolean exposeResource = (!transactional && isExposeListenerSession() && !TransactionSynchronizationManager .hasResource(getConnectionFactory())); if (exposeResource) { TransactionSynchronizationManager.bindResource(getConnectionFactory(), new JmsResourceHolder( session)); } try { doExecuteListener(session, messages); } catch (Throwable ex) { if (status != null) { if (logger.isDebugEnabled()) { logger.debug("Rolling back transaction because of listener exception thrown: " + ex); } status.setRollbackOnly(); } handleListenerException(ex); if (ex instanceof JMSException) { throw (JMSException) ex; } } finally { if (exposeResource) { TransactionSynchronizationManager.unbindResource(getConnectionFactory()); } } return true; } else { if (logger.isTraceEnabled()) { logger.trace("Consumer [" + consumer + "] of " + (transactional ? "transactional " : "") + "session [" + session + "] did not receive a message"); } noMessageReceived(invoker, session); // Nevertheless call commit, in order to reset the transaction timeout (if any). // However, don't do this on Tibco since this may lead to a deadlock there. if (shouldCommitAfterNoMessageReceived(session)) { commitIfNecessary(session, message); } // Indicate that no message has been received. return false; } } catch (JMSException e) { // We record that we last saw an exception - that ensures that only single messages will // be read until we hit the redelivered messages if (retryMitigation) { this.pessimisticMessageMode = true; pessimisticMessageReads = 0; } throw e; } catch (RuntimeException e) { // We record that we last saw an exception - that ensures that only single messages will // be read until we hit the redelivered messages if (retryMitigation) { this.pessimisticMessageMode = true; pessimisticMessageReads = 0; } throw e; } finally { JmsUtils.closeMessageConsumer(consumerToClose); JmsUtils.closeSession(sessionToClose); ConnectionFactoryUtils.releaseConnection(connectionToClose, getConnectionFactory(), true); } } /** * A batched variant of {@link DefaultMessageListenerContainer#doExecuteListener(Session, Message)}. * * @param session The session * @param messages A list of messages * @throws JMSException Indicates a problem during processing */ protected void doExecuteListener(Session session, List<Message> messages) throws JMSException { if (!isAcceptMessagesWhileStopping() && !isRunning()) { if (logger.isWarnEnabled()) { logger.warn("Rejecting received messages because of the listener container " + "having been stopped in the meantime: " + messages); } rollbackIfNecessary(session); throw new MessageRejectedWhileStoppingException(); } try { for (Message message : messages) { invokeListener(session, message); } } catch (JMSException ex) { rollbackOnExceptionIfNecessary(session, ex); throw ex; } catch (RuntimeException ex) { rollbackOnExceptionIfNecessary(session, ex); throw ex; } catch (Error err) { rollbackOnExceptionIfNecessary(session, err); throw err; } commitIfNecessary(session, messages); } /** * Variant of {@link AbstractMessageListenerContainer#commitIfNecessary(Session, Message)} that performs the activity for a batch of messages. * @param session the JMS Session to commit * @param messages the messages to acknowledge * @throws javax.jms.JMSException in case of commit failure */ protected void commitIfNecessary(Session session, List<Message> messages) throws JMSException { // Commit session or acknowledge message. if (session.getTransacted()) { // Commit necessary - but avoid commit call within a JTA transaction. if (isSessionLocallyTransacted(session)) { // Transacted session created by this container -> commit. JmsUtils.commitIfNecessary(session); } } else if (messages != null && isClientAcknowledge(session)) { for (Message message : messages) { message.acknowledge(); } } } @Override protected void validateConfiguration() { if (maxMessagesPerTransaction < 1) { throw new IllegalArgumentException("maxMessagesPerTransaction property must have a value of at least 1"); } } /** * This is the {@link BatchedMessageListenerContainer}'s equivalent to {@link AbstractPollingMessageListenerContainer#receiveMessage}. Does not block if no message is available. * @param consumer The MessageConsumer to use * @return The Message, if any * @throws JMSException Indicates a problem occurred */ protected Message receiveMessageNoWait(MessageConsumer consumer) throws JMSException { return consumer.receiveNoWait(); } /** * Internal exception class that indicates a rejected message on shutdown. Used to trigger a rollback for an external transaction manager in that case. */ private static class MessageRejectedWhileStoppingException extends RuntimeException { private static final long serialVersionUID = -318011666513960841L; } /** * ResourceFactory implementation that delegates to this listener container's protected callback methods. */ private class MessageListenerContainerResourceFactory implements ConnectionFactoryUtils.ResourceFactory { public Connection getConnection(JmsResourceHolder holder) { return BatchedMessageListenerContainer.this.getConnection(holder); } public Session getSession(JmsResourceHolder holder) { return BatchedMessageListenerContainer.this.getSession(holder); } public Connection createConnection() throws JMSException { if (BatchedMessageListenerContainer.this.sharedConnectionEnabled()) { Connection sharedCon = BatchedMessageListenerContainer.this.getSharedConnection(); return new SingleConnectionFactory(sharedCon).createConnection(); } else { return BatchedMessageListenerContainer.this.createConnection(); } } public Session createSession(Connection con) throws JMSException { return BatchedMessageListenerContainer.this.createSession(con); } public boolean isSynchedLocalTransactionAllowed() { return BatchedMessageListenerContainer.this.isSessionTransacted(); } } }