/** * 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; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.MessageProducer; import javax.jms.ObjectMessage; import javax.jms.Session; import javax.jms.TextMessage; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import org.apache.activemq.broker.BrokerFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.test.JmsResourceProvider; import org.apache.activemq.test.TestSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * */ public abstract class JmsTransactionTestSupport extends TestSupport implements MessageListener { private static final Logger LOG = LoggerFactory.getLogger(JmsTransactionTestSupport.class); private static final int MESSAGE_COUNT = 5; private static final String MESSAGE_TEXT = "message"; protected ConnectionFactory connectionFactory; protected Connection connection; protected Session session; protected MessageConsumer consumer; protected MessageProducer producer; protected JmsResourceProvider resourceProvider; protected Destination destination; protected int batchCount = 10; protected int batchSize = 20; protected BrokerService broker; // for message listener test private final List<Message> unackMessages = new ArrayList<>(MESSAGE_COUNT); private final List<Message> ackMessages = new ArrayList<>(MESSAGE_COUNT); private boolean resendPhase; public JmsTransactionTestSupport() { super(); } public JmsTransactionTestSupport(String name) { super(name); } /* * (non-Javadoc) * * @see junit.framework.TestCase#setUp() */ @Override protected void setUp() throws Exception { broker = createBroker(); broker.start(); broker.waitUntilStarted(); resourceProvider = getJmsResourceProvider(); topic = resourceProvider.isTopic(); // We will be using transacted sessions. setSessionTransacted(); connectionFactory = newConnectionFactory(); reconnect(); } protected void setSessionTransacted() { resourceProvider.setTransacted(true); } protected ConnectionFactory newConnectionFactory() throws Exception { return resourceProvider.createConnectionFactory(); } protected void beginTx() throws Exception { //no-op for local tx } protected void commitTx() throws Exception { session.commit(); } protected void rollbackTx() throws Exception { session.rollback(); } /** */ protected BrokerService createBroker() throws Exception, URISyntaxException { return BrokerFactory.createBroker(new URI("broker://()/localhost?persistent=false")); } /* * (non-Javadoc) * * @see junit.framework.TestCase#tearDown() */ @Override protected void tearDown() throws Exception { LOG.info("Closing down connection"); try { session.close(); session = null; connection.close(); connection = null; } catch (Exception e) { LOG.info("Caught exception while closing resources."); } try { broker.stop(); broker.waitUntilStopped(); broker = null; } catch (Exception e) { LOG.info("Caught exception while shutting down the Broker", e); } LOG.info("Connection closed."); } protected abstract JmsResourceProvider getJmsResourceProvider(); /** * Sends a batch of messages and validates that the messages are received. * * @throws Exception */ public void testSendReceiveTransactedBatches() throws Exception { TextMessage message = session.createTextMessage("Batch Message"); for (int j = 0; j < batchCount; j++) { LOG.info("Producing bacth " + j + " of " + batchSize + " messages"); beginTx(); for (int i = 0; i < batchSize; i++) { producer.send(message); } messageSent(); commitTx(); LOG.info("Consuming bacth " + j + " of " + batchSize + " messages"); beginTx(); for (int i = 0; i < batchSize; i++) { message = (TextMessage) consumer.receive(1000 * 5); assertNotNull("Received only " + i + " messages in batch " + j, message); assertEquals("Batch Message", message.getText()); } commitTx(); } } protected void messageSent() throws Exception { } /** * Sends a batch of messages and validates that the rollbacked message was * not consumed. * * @throws Exception */ public void testSendRollback() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // sends a message beginTx(); producer.send(outbound[0]); commitTx(); // sends a message that gets rollbacked beginTx(); producer.send(session.createTextMessage("I'm going to get rolled back.")); rollbackTx(); // sends a message beginTx(); producer.send(outbound[1]); commitTx(); // receives the first message beginTx(); ArrayList<Message> messages = new ArrayList<>(); LOG.info("About to consume message 1"); Message message = consumer.receive(1000); messages.add(message); LOG.info("Received: " + message); // receives the second message LOG.info("About to consume message 2"); message = consumer.receive(4000); messages.add(message); LOG.info("Received: " + message); // validates that the rollbacked was not consumed commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Rollback did not work.", outbound, inbound); } /** * spec section 3.6 acking a message with automation acks has no effect. * @throws Exception */ public void testAckMessageInTx() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message")}; // sends a message beginTx(); producer.send(outbound[0]); outbound[0].acknowledge(); commitTx(); outbound[0].acknowledge(); // receives the first message beginTx(); ArrayList<Message> messages = new ArrayList<>(); LOG.info("About to consume message 1"); Message message = consumer.receive(1000); messages.add(message); LOG.info("Received: " + message); // validates that the rollbacked was not consumed commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Message not delivered.", outbound, inbound); } /** * Sends a batch of messages and validates that the message sent before * session close is not consumed. * * This test only works with local transactions, not xa. * @throws Exception */ public void testSendSessionClose() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // sends a message beginTx(); producer.send(outbound[0]); commitTx(); // sends a message that gets rollbacked beginTx(); producer.send(session.createTextMessage("I'm going to get rolled back.")); consumer.close(); reconnectSession(); // sends a message producer.send(outbound[1]); commitTx(); // receives the first message ArrayList<Message> messages = new ArrayList<>(); LOG.info("About to consume message 1"); beginTx(); Message message = consumer.receive(1000); messages.add(message); LOG.info("Received: " + message); // receives the second message LOG.info("About to consume message 2"); message = consumer.receive(4000); messages.add(message); LOG.info("Received: " + message); // validates that the rollbacked was not consumed commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Rollback did not work.", outbound, inbound); } /** * Sends a batch of messages and validates that the message sent before * session close is not consumed. * * @throws Exception */ public void testSendSessionAndConnectionClose() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // sends a message beginTx(); producer.send(outbound[0]); commitTx(); // sends a message that gets rollbacked beginTx(); producer.send(session.createTextMessage("I'm going to get rolled back.")); consumer.close(); session.close(); reconnect(); // sends a message beginTx(); producer.send(outbound[1]); commitTx(); // receives the first message ArrayList<Message> messages = new ArrayList<>(); LOG.info("About to consume message 1"); beginTx(); Message message = consumer.receive(1000); messages.add(message); LOG.info("Received: " + message); // receives the second message LOG.info("About to consume message 2"); message = consumer.receive(4000); messages.add(message); LOG.info("Received: " + message); // validates that the rollbacked was not consumed commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Rollback did not work.", outbound, inbound); } /** * Sends a batch of messages and validates that the rollbacked message was * redelivered. * * @throws Exception */ public void testReceiveRollback() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // lets consume any outstanding messages from prev test runs beginTx(); while (consumer.receive(1000) != null) { } commitTx(); // sent both messages beginTx(); producer.send(outbound[0]); producer.send(outbound[1]); commitTx(); LOG.info("Sent 0: " + outbound[0]); LOG.info("Sent 1: " + outbound[1]); ArrayList<Message> messages = new ArrayList<>(); beginTx(); Message message = consumer.receive(1000); messages.add(message); assertEquals(outbound[0], message); commitTx(); // rollback so we can get that last message again. beginTx(); message = consumer.receive(1000); assertNotNull(message); assertEquals(outbound[1], message); rollbackTx(); // Consume again.. the prev message should // get redelivered. beginTx(); message = consumer.receive(5000); assertNotNull("Should have re-received the message again!", message); messages.add(message); commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Rollback did not work", outbound, inbound); } /** * Sends a batch of messages and validates that the rollbacked message was * redelivered. * * @throws Exception */ public void testReceiveTwoThenRollback() throws Exception { Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // lets consume any outstanding messages from prev test runs beginTx(); while (consumer.receive(1000) != null) { } commitTx(); // beginTx(); producer.send(outbound[0]); producer.send(outbound[1]); commitTx(); LOG.info("Sent 0: " + outbound[0]); LOG.info("Sent 1: " + outbound[1]); ArrayList<Message> messages = new ArrayList<>(); beginTx(); Message message = consumer.receive(1000); assertEquals(outbound[0], message); message = consumer.receive(1000); assertNotNull(message); assertEquals(outbound[1], message); rollbackTx(); // Consume again.. the prev message should // get redelivered. beginTx(); message = consumer.receive(5000); assertNotNull("Should have re-received the first message again!", message); messages.add(message); assertEquals(outbound[0], message); message = consumer.receive(5000); assertNotNull("Should have re-received the second message again!", message); messages.add(message); assertEquals(outbound[1], message); assertNull(consumer.receiveNoWait()); commitTx(); Message inbound[] = new Message[messages.size()]; messages.toArray(inbound); assertTextMessagesEqual("Rollback did not work", outbound, inbound); } /** * Sends a batch of messages and validates that the rollbacked message was * not consumed. * * @throws Exception */ public void testSendReceiveWithPrefetchOne() throws Exception { setPrefetchToOne(); Message[] outbound = new Message[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message"), session.createTextMessage("Third Message"), session.createTextMessage("Fourth Message")}; beginTx(); for (int i = 0; i < outbound.length; i++) { // sends a message producer.send(outbound[i]); } commitTx(); // receives the first message beginTx(); for (int i = 0; i < outbound.length; i++) { LOG.info("About to consume message 1"); Message message = consumer.receive(1000); assertNotNull(message); LOG.info("Received: " + message); } // validates that the rollbacked was not consumed commitTx(); } /** * Perform the test that validates if the rollbacked message was redelivered * multiple times. * * @throws Exception */ public void testReceiveTwoThenRollbackManyTimes() throws Exception { for (int i = 0; i < 5; i++) { testReceiveTwoThenRollback(); } } /** * Sends a batch of messages and validates that the rollbacked message was * not consumed. This test differs by setting the message prefetch to one. * * @throws Exception */ public void testSendRollbackWithPrefetchOfOne() throws Exception { setPrefetchToOne(); testSendRollback(); } /** * Sends a batch of messages and and validates that the rollbacked message * was redelivered. This test differs by setting the message prefetch to * one. * * @throws Exception */ public void testReceiveRollbackWithPrefetchOfOne() throws Exception { setPrefetchToOne(); testReceiveRollback(); } /** * Tests if the messages can still be received if the consumer is closed * (session is not closed). * * @throws Exception see http://jira.codehaus.org/browse/AMQ-143 */ public void testCloseConsumerBeforeCommit() throws Exception { TextMessage[] outbound = new TextMessage[]{session.createTextMessage("First Message"), session.createTextMessage("Second Message")}; // lets consume any outstanding messages from prev test runs beginTx(); while (consumer.receiveNoWait() != null) { } commitTx(); // sends the messages beginTx(); producer.send(outbound[0]); producer.send(outbound[1]); commitTx(); LOG.info("Sent 0: " + outbound[0]); LOG.info("Sent 1: " + outbound[1]); beginTx(); TextMessage message = (TextMessage) consumer.receive(1000); assertEquals(outbound[0].getText(), message.getText()); // Close the consumer before the commit. This should not cause the // received message // to rollback. consumer.close(); commitTx(); // Create a new consumer consumer = resourceProvider.createConsumer(session, destination); LOG.info("Created consumer: " + consumer); beginTx(); message = (TextMessage) consumer.receive(1000); assertEquals(outbound[1].getText(), message.getText()); commitTx(); } public void testChangeMutableObjectInObjectMessageThenRollback() throws Exception { ArrayList<String> list = new ArrayList<>(); list.add("First"); Message outbound = session.createObjectMessage(list); outbound.setStringProperty("foo", "abc"); beginTx(); producer.send(outbound); commitTx(); LOG.info("About to consume message 1"); beginTx(); Message message = consumer.receive(5000); List<String> body = assertReceivedObjectMessageWithListBody(message); // now lets try mutate it try { message.setStringProperty("foo", "def"); fail("Cannot change properties of the object!"); } catch (JMSException e) { LOG.info("Caught expected exception: " + e, e); } body.clear(); body.add("This should never be seen!"); rollbackTx(); beginTx(); message = consumer.receive(5000); List<String> secondBody = assertReceivedObjectMessageWithListBody(message); assertNotSame("Second call should return a different body", secondBody, body); commitTx(); } @SuppressWarnings("unchecked") protected List<String> assertReceivedObjectMessageWithListBody(Message message) throws JMSException { assertNotNull("Should have received a message!", message); assertEquals("foo header", "abc", message.getStringProperty("foo")); assertTrue("Should be an object message but was: " + message, message instanceof ObjectMessage); ObjectMessage objectMessage = (ObjectMessage) message; List<String> body = (List<String>) objectMessage.getObject(); LOG.info("Received body: " + body); assertEquals("Size of list should be 1", 1, body.size()); assertEquals("element 0 of list", "First", body.get(0)); return body; } /** * Recreates the connection. * * @throws javax.jms.JMSException */ protected void reconnect() throws Exception { if (connection != null) { // Close the prev connection. connection.close(); } session = null; connection = resourceProvider.createConnection(connectionFactory); reconnectSession(); connection.start(); } /** * Recreates the connection. * * @throws javax.jms.JMSException */ protected void reconnectSession() throws JMSException { if (session != null) { session.close(); } session = resourceProvider.createSession(connection); destination = resourceProvider.createDestination(session, getSubject()); producer = resourceProvider.createProducer(session, destination); consumer = resourceProvider.createConsumer(session, destination); } /** * Sets the prefeftch policy to one. */ protected void setPrefetchToOne() { ActiveMQPrefetchPolicy prefetchPolicy = getPrefetchPolicy(); prefetchPolicy.setQueuePrefetch(1); prefetchPolicy.setTopicPrefetch(1); prefetchPolicy.setDurableTopicPrefetch(1); prefetchPolicy.setOptimizeDurableTopicPrefetch(1); } protected ActiveMQPrefetchPolicy getPrefetchPolicy() { return ((ActiveMQConnection) connection).getPrefetchPolicy(); } //This test won't work with xa tx so no beginTx() has been added. public void testMessageListener() throws Exception { // send messages for (int i = 0; i < MESSAGE_COUNT; i++) { producer.send(session.createTextMessage(MESSAGE_TEXT + i)); } commitTx(); consumer.setMessageListener(this); // wait receive waitReceiveUnack(); assertEquals(unackMessages.size(), MESSAGE_COUNT); // resend phase waitReceiveAck(); assertEquals(ackMessages.size(), MESSAGE_COUNT); // should no longer re-receive consumer.setMessageListener(null); assertNull(consumer.receive(500)); reconnect(); } @Override public void onMessage(Message message) { if (!resendPhase) { unackMessages.add(message); if (unackMessages.size() == MESSAGE_COUNT) { try { rollbackTx(); resendPhase = true; } catch (Exception e) { e.printStackTrace(); } } } else { ackMessages.add(message); if (ackMessages.size() == MESSAGE_COUNT) { try { commitTx(); } catch (Exception e) { e.printStackTrace(); } } } } private void waitReceiveUnack() throws Exception { for (int i = 0; i < 100 && !resendPhase; i++) { Thread.sleep(100); } assertTrue(resendPhase); } private void waitReceiveAck() throws Exception { for (int i = 0; i < 100 && ackMessages.size() < MESSAGE_COUNT; i++) { Thread.sleep(100); } assertFalse(ackMessages.size() < MESSAGE_COUNT); } }