/** * 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.broker.virtual; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; 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.broker.region.Queue; import org.apache.activemq.broker.region.RegionBroker; import org.apache.activemq.command.ActiveMQQueue; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import junit.framework.TestCase; /** * Unit test for virtual topics and DLQ messaging. See individual test for more * detail * */ public class VirtualTopicDLQTest extends TestCase { private static BrokerService broker; private static final Logger LOG = LoggerFactory.getLogger(VirtualTopicDLQTest.class); static final String jmsConnectionURI = "failover:(vm://localhost)"; // Virtual Topic that the test publishes 10 messages to private static final String virtualTopicName = "VirtualTopic.Test"; // Queues that receive all the messages send to the virtual topic private static final String consumer1Prefix = "Consumer.A."; private static final String consumer2Prefix = "Consumer.B."; private static final String consumer3Prefix = "Consumer.C."; // Expected Individual Dead Letter Queue names that are tied to the // Subscriber Queues private static final String dlqPrefix = "ActiveMQ.DLQ.Queue."; // Number of messages private static final int numberMessages = 6; @Override @Before public void setUp() throws Exception { try { broker = BrokerFactory.createBroker("xbean:org/apache/activemq/broker/virtual/virtual-individual-dlq.xml", true); broker.start(); broker.waitUntilStarted(); } catch (Exception e) { e.printStackTrace(); throw e; } } @Override @After public void tearDown() throws Exception { try { // Purge the DLQ's so counts are correct for next run purgeDestination(dlqPrefix + consumer1Prefix + virtualTopicName); purgeDestination(dlqPrefix + consumer2Prefix + virtualTopicName); purgeDestination(dlqPrefix + consumer3Prefix + virtualTopicName); } catch (Exception e) { e.printStackTrace(); } if (broker != null) { broker.stop(); broker.waitUntilStopped(); broker = null; } } /* * This test verifies that all undelivered messages sent to a consumers * listening on a queue associated with a virtual topic with be forwarded to * separate DLQ's. * * Note that the broker config, deadLetterStrategy need to have the enable * audit set to false so that duplicate message sent from a topic to * individual consumers are forwarded to the DLQ * * <deadLetterStrategy> <bean * xmlns="http://www.springframework.org/schema/beans" * class="org.apache.activemq.broker.region.policy.IndividualDeadLetterStrategy" * > <property name="useQueueForQueueMessages" value="true"></property> * <property name="processNonPersistent" value="true"></property> <property * name="processExpired" value="false"></property> <property * name="enableAudit" value="false"></property> * * </bean> </deadLetterStrategy> */ @Test public void testVirtualTopicSubscriberDeadLetterQueue() throws Exception { TestConsumer consumer1 = null; TestConsumer consumer2 = null; TestConsumer consumer3 = null; TestConsumer dlqConsumer1 = null; TestConsumer dlqConsumer2 = null; TestConsumer dlqConsumer3 = null; try { // The first 2 consumers will rollback, ultimately causing messages // to land on the DLQ consumer1 = new TestConsumer(consumer1Prefix + virtualTopicName, false, numberMessages, true); thread(consumer1, false); consumer2 = new TestConsumer(consumer2Prefix + virtualTopicName, false, numberMessages, true); thread(consumer2, false); // TestConsumer that does not throw exceptions, messages should not // land on DLQ consumer3 = new TestConsumer(consumer3Prefix + virtualTopicName, false, numberMessages, false); thread(consumer3, false); // TestConsumer to read the expected Dead Letter Queue dlqConsumer1 = new TestConsumer(dlqPrefix + consumer1Prefix + virtualTopicName, false, numberMessages, false); thread(dlqConsumer1, false); dlqConsumer2 = new TestConsumer(dlqPrefix + consumer2Prefix + virtualTopicName, false, numberMessages, false); thread(dlqConsumer2, false); dlqConsumer3 = new TestConsumer(dlqPrefix + consumer3Prefix + virtualTopicName, false, numberMessages, false); thread(dlqConsumer3, false); // Give the consumers a second to start Thread.sleep(1000); // Start the producer TestProducer producer = new TestProducer(virtualTopicName, true, numberMessages); thread(producer, false); assertTrue("sent all producer messages in time, count is: " + producer.getLatch().getCount(), producer.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("producer successful, count = " + producer.getLatch().getCount()); assertTrue("remaining consumer1 count should be zero, is: " + consumer1.getLatch().getCount(), consumer1.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("consumer1 successful, count = " + consumer1.getLatch().getCount()); assertTrue("remaining consumer2 count should be zero, is: " + consumer2.getLatch().getCount(), consumer2.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("consumer2 successful, count = " + consumer2.getLatch().getCount()); assertTrue("remaining consumer3 count should be zero, is: " + consumer3.getLatch().getCount(), consumer3.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("consumer3 successful, count = " + consumer3.getLatch().getCount()); assertTrue("remaining dlqConsumer1 count should be zero, is: " + dlqConsumer1.getLatch().getCount(), dlqConsumer1.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("dlqConsumer1 successful, count = " + dlqConsumer1.getLatch().getCount()); assertTrue("remaining dlqConsumer2 count should be zero, is: " + dlqConsumer2.getLatch().getCount(), dlqConsumer2.getLatch().await(10, TimeUnit.SECONDS)); LOG.info("dlqConsumer2 successful, count = " + dlqConsumer2.getLatch().getCount()); assertTrue("remaining dlqConsumer3 count should be " + numberMessages + ", is: " + dlqConsumer3.getLatch().getCount(), dlqConsumer3.getLatch() .getCount() == numberMessages); LOG.info("dlqConsumer2 successful, count = " + dlqConsumer2.getLatch().getCount()); } catch (Exception e) { e.printStackTrace(); throw e; } finally { // Tell consumers to stop (don't read any more messages after this) if (consumer1 != null) consumer1.setStop(true); if (consumer2 != null) consumer2.setStop(true); if (consumer3 != null) consumer3.setStop(true); if (dlqConsumer1 != null) dlqConsumer1.setStop(true); if (dlqConsumer2 != null) dlqConsumer2.setStop(true); if (dlqConsumer3 != null) dlqConsumer3.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 String destinationName = null; private boolean isTopic = true; private int numberMessages = 0; private CountDownLatch latch = null; public TestProducer(String destinationName, boolean isTopic, int numberMessages) { this.destinationName = destinationName; this.isTopic = isTopic; this.numberMessages = numberMessages; latch = new CountDownLatch(numberMessages); } public CountDownLatch getLatch() { return latch; } @Override 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.start(); session = (ActiveMQSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); if (isTopic) { destination = session.createTopic(this.destinationName); } else { 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 < numberMessages; i++) { TextMessage message = session.createTextMessage("I am a message :: " + String.valueOf(i)); try { producer.send(message); } catch (Exception deeperException) { LOG.info("Producer for destination (" + destinationName + ") Caught: " + deeperException); } latch.countDown(); Thread.sleep(1000); } LOG.info("Finished TestProducer for destination (" + destinationName + ")"); } catch (Exception e) { LOG.error("Terminating TestProducer(" + destinationName + ")Caught: " + e); } finally { try { // Clean up if (connection != null) { connection.close(); } } catch (Exception e) { LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e); } } } } private class TestConsumer implements Runnable, ExceptionListener, MessageListener { private String destinationName = null; private boolean isTopic = true; private CountDownLatch latch = null; private int maxRedeliveries = 0; private int receivedMessageCounter = 0; private boolean bFakeFail = false; private boolean bStop = false; private ActiveMQConnectionFactory connectionFactory = null; private ActiveMQConnection connection = null; private Session session = null; private MessageConsumer consumer = null; public TestConsumer(String destinationName, boolean isTopic, int expectedNumberMessages, boolean bFakeFail) { this.destinationName = destinationName; this.isTopic = isTopic; latch = new CountDownLatch(expectedNumberMessages * (this.bFakeFail ? (maxRedeliveries + 1) : 1)); this.bFakeFail = bFakeFail; } public CountDownLatch getLatch() { return latch; } @Override public void run() { try { LOG.info("Started TestConsumer for destination (" + destinationName + ")"); connectionFactory = new ActiveMQConnectionFactory(jmsConnectionURI); connection = (ActiveMQConnection) connectionFactory.createConnection(); connection.start(); session = connection.createSession(true, Session.SESSION_TRANSACTED); RedeliveryPolicy policy = connection.getRedeliveryPolicy(); policy.setInitialRedeliveryDelay(1); policy.setUseExponentialBackOff(false); policy.setMaximumRedeliveries(maxRedeliveries); connection.setExceptionListener(this); Destination destination = null; if (isTopic) { destination = session.createTopic(destinationName); } else { destination = session.createQueue(destinationName); } consumer = session.createConsumer(destination); consumer.setMessageListener(this); 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 (connection != null) { connection.close(); } } catch (Exception e) { LOG.error("Closing connection/session (" + destinationName + ")Caught: " + e); } } } @Override public synchronized void onException(JMSException ex) { ex.printStackTrace(); LOG.error("Consumer for destination, (" + destinationName + "), JMS Exception occured. Shutting down client."); } public synchronized void setStop(boolean bStop) { this.bStop = bStop; } @Override 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 { LOG.info("Consumer for destination (" + destinationName + ") Received message id :: " + message.getJMSMessageID()); if (!bFakeFail) { LOG.info("Consumer on destination " + destinationName + " committing JMS Session for message: " + message.toString()); session.commit(); } else { LOG.info("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 (JMSException ex) { LOG.error("Error reading JMS Message from destination " + destinationName + "."); } } } private static void purgeDestination(String destination) throws Exception { final Queue dest = (Queue) ((RegionBroker) broker.getRegionBroker()).getQueueRegion().getDestinationMap().get(new ActiveMQQueue(destination)); dest.purge(); assertEquals(0, dest.getDestinationStatistics().getMessages().getCount()); } }