/** * 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.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.jms.Connection; import javax.jms.ExceptionListener; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.MessageProducer; import javax.jms.Session; import javax.jms.TextMessage; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.command.ActiveMQQueue; import org.apache.activemq.command.ActiveMQTopic; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a test case for the issue reported at: https://issues.apache.org/activemq/browse/AMQ-2021 Bug is modification * of inflight message properties so the failure can manifest itself in a bunch or ways, from message receipt with null * properties to marshall errors */ public class AMQ2021Test implements ExceptionListener, UncaughtExceptionHandler { private static final Logger log = LoggerFactory.getLogger(AMQ2021Test.class); BrokerService brokerService; ArrayList<Thread> threads = new ArrayList<Thread>(); Vector<Throwable> exceptions; @Rule public TestName name = new TestName(); AMQ2021Test testCase; private final String ACTIVEMQ_BROKER_BIND = "tcp://localhost:0"; private String CONSUMER_BROKER_URL = "?jms.redeliveryPolicy.maximumRedeliveries=1&jms.redeliveryPolicy.initialRedeliveryDelay=0"; private String PRODUCER_BROKER_URL; private final int numMessages = 1000; private final int numConsumers = 2; private final int dlqMessages = numMessages / 2; private CountDownLatch receivedLatch; private ActiveMQTopic destination; private CountDownLatch started; @Before public void setUp() throws Exception { Thread.setDefaultUncaughtExceptionHandler(this); testCase = this; // Start an embedded broker up. brokerService = new BrokerService(); brokerService.setDeleteAllMessagesOnStartup(true); brokerService.addConnector(ACTIVEMQ_BROKER_BIND); brokerService.start(); destination = new ActiveMQTopic(name.getMethodName()); exceptions = new Vector<Throwable>(); CONSUMER_BROKER_URL = brokerService.getTransportConnectors().get(0).getPublishableConnectString() + CONSUMER_BROKER_URL; PRODUCER_BROKER_URL = brokerService.getTransportConnectors().get(0).getPublishableConnectString(); receivedLatch = new CountDownLatch(numConsumers * (numMessages + dlqMessages)); started = new CountDownLatch(1); } @After public void tearDown() throws Exception { for (Thread t : threads) { t.interrupt(); t.join(); } brokerService.stop(); } @Test(timeout=240000) public void testConcurrentTopicResendToDLQ() throws Exception { for (int i = 0; i < numConsumers; i++) { ConsumerThread c1 = new ConsumerThread("Consumer-" + i); threads.add(c1); c1.start(); } assertTrue(started.await(10, TimeUnit.SECONDS)); Thread producer = new Thread() { @Override public void run() { try { produce(numMessages); } catch (Exception e) { } } }; threads.add(producer); producer.start(); boolean allGood = receivedLatch.await(90, TimeUnit.SECONDS); for (Throwable t : exceptions) { log.error("failing test with first exception", t); fail("exception during test : " + t); } assertTrue("excepted messages received within time limit", allGood); assertEquals(0, exceptions.size()); for (int i = 0; i < numConsumers; i++) { // last recovery sends message to deq so is not received again assertEquals(dlqMessages * 2, ((ConsumerThread) threads.get(i)).recoveries); assertEquals(numMessages + dlqMessages, ((ConsumerThread) threads.get(i)).counter); } // half of the messages for each consumer should go to the dlq but duplicates will // be suppressed consumeFromDLQ(dlqMessages); } private void consumeFromDLQ(int messageCount) throws Exception { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONSUMER_BROKER_URL); Connection connection = connectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageConsumer dlqConsumer = session.createConsumer(new ActiveMQQueue("ActiveMQ.DLQ")); int count = 0; for (int i = 0; i < messageCount; i++) { if (dlqConsumer.receive(1000) == null) { break; } count++; } assertEquals(messageCount, count); } public void produce(int count) throws Exception { Connection connection = null; try { ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(PRODUCER_BROKER_URL); connection = factory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(destination); producer.setTimeToLive(0); connection.start(); for (int i = 0; i < count; i++) { int id = i + 1; TextMessage message = session.createTextMessage(name.getMethodName() + " Message " + id); message.setIntProperty("MsgNumber", id); producer.send(message); if (id % 500 == 0) { log.info("sent " + id + ", ith " + message); } } } catch (JMSException e) { log.error("unexpected ex on produce", e); exceptions.add(e); } finally { try { if (connection != null) { connection.close(); } } catch (Throwable e) { } } } public class ConsumerThread extends Thread implements MessageListener { public long counter = 0; public long recoveries = 0; private Session session; public ConsumerThread(String threadId) { super(threadId); } @Override public void run() { try { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONSUMER_BROKER_URL); Connection connection = connectionFactory.createConnection(); connection.setExceptionListener(testCase); connection.setClientID(getName()); session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); MessageConsumer consumer = session.createDurableSubscriber(destination, getName()); consumer.setMessageListener(this); connection.start(); started.countDown(); } catch (JMSException exception) { log.error("unexpected ex in consumer run", exception); exceptions.add(exception); } } @Override public void onMessage(Message message) { try { counter++; int messageNumber = message.getIntProperty("MsgNumber"); if (messageNumber % 2 == 0) { session.recover(); recoveries++; } else { message.acknowledge(); } if (counter % 200 == 0) { log.info("recoveries:" + recoveries + ", Received " + counter + ", counter'th " + message); } receivedLatch.countDown(); } catch (Exception e) { log.error("unexpected ex on onMessage", e); exceptions.add(e); } } } @Override public void onException(JMSException exception) { log.info("Unexpected JMSException", exception); exceptions.add(exception); } @Override public void uncaughtException(Thread thread, Throwable exception) { log.info("Unexpected exception from thread " + thread + ", ex: " + exception); exceptions.add(exception); } }