/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.activemq.bugs; import static org.junit.Assert.*; import java.net.URI; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import javax.jms.DeliveryMode; 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.Session; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnection; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQMessageProducer; import org.apache.activemq.ActiveMQSession; import org.apache.activemq.RedeliveryPolicy; import org.apache.activemq.broker.BrokerFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.util.Wait; import org.apache.activemq.util.Wait.Condition; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Test validates that the AMQ consumer blocks on redelivery of a message, * through all redeliveries, until the message is either successfully consumed * or sent to the DLQ. */ public class AMQ1853Test { private static BrokerService broker; private static final Logger LOG = LoggerFactory.getLogger(AMQ1853Test.class); static final String jmsConnectionURI = "failover:(vm://localhost)"; // Virtual Topic that the test publishes 10 messages to private static final String queueFail = "Queue.BlockingConsumer.QueueFail"; // Number of messages private final int producerMessages = 5; private final int totalNumberMessages = producerMessages * 2; private final int maxRedeliveries = 2; private final int redeliveryDelay = 1000; private Map<String, AtomicInteger> messageList = null; @Before public void setUp() throws Exception { broker = BrokerFactory.createBroker(new URI("broker:()/localhost?persistent=false")); broker.setUseJmx(false); broker.setDeleteAllMessagesOnStartup(true); broker.start(); broker.waitUntilStarted(); } @After public void tearDown() throws Exception { if (broker != null) { broker.stop(); broker.waitUntilStopped(); broker = null; } } @Test public void testConsumerMessagesAreNotOrdered() throws Exception { TestConsumer consumerAllFail = null; messageList = new Hashtable<String, AtomicInteger>(); try { // The first 2 consumers will rollback, ultimately causing messages to land on the DLQ TestProducer producerAllFail = new TestProducer(queueFail); thread(producerAllFail, false); consumerAllFail = new TestConsumer(queueFail, true); thread(consumerAllFail, false); // Give the consumers a second to start Thread.sleep(1000); thread(producerAllFail, false); // Give the consumers a second to start Thread.sleep(1000); producerAllFail.getLatch().await(); LOG.info("producer successful, count = " + producerAllFail.getLatch().getCount()); LOG.info("final message list size = " + messageList.size()); assertTrue("message list size = " + messageList.size() + " exptected:" + totalNumberMessages, Wait.waitFor(new Condition() { @Override public boolean isSatisified() throws Exception { return totalNumberMessages == messageList.size(); } })); consumerAllFail.getLatch().await(); LOG.info("consumerAllFail successful, count = " + consumerAllFail.getLatch().getCount()); Iterator<String> keys = messageList.keySet().iterator(); for (AtomicInteger counter : messageList.values()) { String message = keys.next(); LOG.info("final count for message " + message + " counter = " + counter.get()); assertTrue("for message " + message + " counter = " + counter.get(), counter.get() == maxRedeliveries + 1); } assertFalse(consumerAllFail.messageReceiptIsOrdered()); } finally { if (consumerAllFail != null) { consumerAllFail.setStop(true); } } } private static Thread thread(Runnable runnable, boolean daemon) { Thread brokerThread = new Thread(runnable); brokerThread.setDaemon(daemon); brokerThread.start(); return brokerThread; } private class TestProducer implements Runnable { private CountDownLatch latch = null; private String destinationName = null; public TestProducer(String destinationName) { this.destinationName = destinationName; // We run the producer 2 times latch = new CountDownLatch(totalNumberMessages); } public CountDownLatch getLatch() { return latch; } public void run() { ActiveMQConnectionFactory connectionFactory = null; ActiveMQConnection connection = null; ActiveMQSession session = null; Destination destination = null; try { LOG.info("Started TestProducer for destination (" + destinationName + ")"); connectionFactory = new ActiveMQConnectionFactory(jmsConnectionURI); connection = (ActiveMQConnection) connectionFactory.createConnection(); connection.setCopyMessageOnSend(false); connection.start(); session = (ActiveMQSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); destination = session.createQueue(this.destinationName); // Create a MessageProducer from the Session to the Topic or Queue ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(destination); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); for (int i = 0; i < (producerMessages); i++) { TextMessage message = (TextMessage) session.createTextMessage(); message.setLongProperty("TestTime", (System.currentTimeMillis())); try { producer.send(message); LOG.info("Producer (" + destinationName + ")\n" + message.getJMSMessageID() + " = sent messageId\n"); latch.countDown(); LOG.info(" Latch count " + latch.getCount()); LOG.info("Producer message list size = " + messageList.keySet().size()); messageList.put(message.getJMSMessageID(), new AtomicInteger(0)); LOG.info("Producer message list size = " + messageList.keySet().size()); } catch (Exception deeperException) { LOG.info("Producer for destination (" + destinationName + ") Caught: " + deeperException); } Thread.sleep(1000); } LOG.info("Finished TestProducer for destination (" + destinationName + ")"); } catch (Exception e) { LOG.error("Terminating TestProducer(" + destinationName + ")Caught: " + e); } finally { try { if (session != null) { session.close(); } if (connection != null) { connection.close(); } } catch (Exception e) { LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e); } } } } private class TestConsumer implements Runnable, ExceptionListener, MessageListener { private CountDownLatch latch = null; private int receivedMessageCounter = 0; private boolean bFakeFail = false; String destinationName = null; boolean bMessageReceiptIsOrdered = true; boolean bStop = false; String previousMessageId = null; private ActiveMQConnectionFactory connectionFactory = null; private ActiveMQConnection connection = null; private Session session = null; private MessageConsumer consumer = null; public TestConsumer(String destinationName, boolean bFakeFail) { this.bFakeFail = bFakeFail; latch = new CountDownLatch(totalNumberMessages * (this.bFakeFail ? (maxRedeliveries + 1) : 1)); this.destinationName = destinationName; } public CountDownLatch getLatch() { return latch; } public boolean messageReceiptIsOrdered() { return bMessageReceiptIsOrdered; } public void run() { try { LOG.info("Started TestConsumer for destination (" + destinationName + ")"); connectionFactory = new ActiveMQConnectionFactory(jmsConnectionURI); connection = (ActiveMQConnection) connectionFactory.createConnection(); connection.setNonBlockingRedelivery(true); session = connection.createSession(true, Session.SESSION_TRANSACTED); RedeliveryPolicy policy = connection.getRedeliveryPolicy(); policy.setInitialRedeliveryDelay(redeliveryDelay); policy.setBackOffMultiplier(-1); policy.setRedeliveryDelay(redeliveryDelay); policy.setMaximumRedeliveryDelay(-1); policy.setUseExponentialBackOff(false); policy.setMaximumRedeliveries(maxRedeliveries); connection.setExceptionListener(this); Destination destination = session.createQueue(destinationName); consumer = session.createConsumer(destination); consumer.setMessageListener(this); connection.start(); while (!bStop) { Thread.sleep(100); } LOG.info("Finished TestConsumer for destination name (" + destinationName + ") remaining " + this.latch.getCount() + " messages " + this.toString()); } catch (Exception e) { LOG.error("Consumer (" + destinationName + ") Caught: " + e); } finally { try { if (consumer != null) { consumer.close(); } if (session != null) { session.close(); } if (connection != null) { connection.close(); } } catch (Exception e) { LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e); } } } public synchronized void onException(JMSException ex) { LOG.error("Consumer for destination, (" + destinationName + "), JMS Exception occured. Shutting down client."); } public synchronized void setStop(boolean bStop) { this.bStop = bStop; } public synchronized void onMessage(Message message) { receivedMessageCounter++; latch.countDown(); LOG.info("Consumer for destination (" + destinationName + ") latch countdown: " + latch.getCount() + " :: Number messages received " + this.receivedMessageCounter); try { if (receivedMessageCounter % (maxRedeliveries + 1) == 1) { previousMessageId = message.getJMSMessageID(); } if (bMessageReceiptIsOrdered) { bMessageReceiptIsOrdered = previousMessageId.trim().equals(message.getJMSMessageID()); } final String jmsMessageId = message.getJMSMessageID(); assertTrue("Did not find expected ", Wait.waitFor(new Wait.Condition() { @Override public boolean isSatisified() throws Exception { return messageList.containsKey(jmsMessageId); } })); AtomicInteger counter = messageList.get(jmsMessageId); counter.incrementAndGet(); LOG.info("Consumer for destination (" + destinationName + ")\n" + message.getJMSMessageID() + " = currentMessageId\n" + previousMessageId + " = previousMessageId\n" + bMessageReceiptIsOrdered + "= bMessageReceiptIsOrdered\n" + ">>LATENCY " + (System.currentTimeMillis() - message.getLongProperty("TestTime")) + "\n" + "message counter = " + counter.get()); if (!bFakeFail) { LOG.debug("Consumer on destination " + destinationName + " committing JMS Session for message: " + message.toString()); session.commit(); } else { LOG.debug("Consumer on destination " + destinationName + " rolling back JMS Session for message: " + message.toString()); session.rollback(); // rolls back all the consumed messages on the session to } } catch (Exception ex) { ex.printStackTrace(); LOG.error("Error reading JMS Message from destination " + destinationName + "."); } } } }