/** * 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 java.io.IOException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import javax.jms.Connection; import javax.jms.DeliveryMode; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.MessageConsumer; import javax.jms.MessageProducer; import javax.jms.Session; import javax.jms.TextMessage; import junit.framework.TestCase; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.broker.TransportConnection; import org.apache.activemq.store.jdbc.DataSourceServiceSupport; import org.apache.activemq.store.jdbc.JDBCPersistenceAdapter; import org.apache.activemq.store.jdbc.LeaseDatabaseLocker; import org.apache.activemq.store.jdbc.TransactionContext; import org.apache.activemq.util.IOHelper; import org.apache.activemq.util.LeaseLockerIOExceptionHandler; import org.apache.derby.jdbc.EmbeddedDataSource; import org.apache.log4j.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Test to demostrate a message trapped in the JDBC store and not * delivered to consumer * * The test throws issues the commit to the DB but throws * an exception back to the broker. This scenario could happen when a network * cable is disconnected - message is committed to DB but broker does not know. * * */ public class TrapMessageInJDBCStoreTest extends TestCase { private static final String MY_TEST_Q = "MY_TEST_Q"; private static final Logger LOG = LoggerFactory .getLogger(TrapMessageInJDBCStoreTest.class); private String transportUrl = "tcp://127.0.0.1:0"; private BrokerService broker; private TestTransactionContext testTransactionContext; private TestJDBCPersistenceAdapter jdbc; private java.sql.Connection checkOnStoreConnection; protected BrokerService createBroker(boolean withJMX) throws Exception { BrokerService broker = new BrokerService(); broker.setUseJmx(withJMX); EmbeddedDataSource embeddedDataSource = (EmbeddedDataSource) DataSourceServiceSupport.createDataSource(IOHelper.getDefaultDataDirectory()); checkOnStoreConnection = embeddedDataSource.getConnection(); //wire in a TestTransactionContext (wrapper to TransactionContext) that has an executeBatch() // method that can be configured to throw a SQL exception on demand jdbc = new TestJDBCPersistenceAdapter(); jdbc.setDataSource(embeddedDataSource); jdbc.setCleanupPeriod(0); testTransactionContext = new TestTransactionContext(jdbc); jdbc.setLockKeepAlivePeriod(1000l); LeaseDatabaseLocker leaseDatabaseLocker = new LeaseDatabaseLocker(); leaseDatabaseLocker.setLockAcquireSleepInterval(2000l); jdbc.setLocker(leaseDatabaseLocker); broker.setPersistenceAdapter(jdbc); broker.setIoExceptionHandler(new LeaseLockerIOExceptionHandler()); transportUrl = broker.addConnector(transportUrl).getPublishableConnectString(); return broker; } /** * * sends 3 messages to the queue. When the second message is being committed to the JDBCStore, $ * it throws a dummy SQL exception - the message has been committed to the embedded DB before the exception * is thrown * * Excepted correct outcome: receive 3 messages and the DB should contain no messages * * @throws Exception */ public void testDBCommitException() throws Exception { org.apache.log4j.Logger serviceLogger = org.apache.log4j.Logger.getLogger(TransportConnection.class.getName() + ".Service"); serviceLogger.setLevel (Level.TRACE); broker = this.createBroker(false); broker.deleteAllMessages(); broker.start(); broker.waitUntilStarted(); try { LOG.info("***Broker started..."); // failover but timeout in 5 seconds so the test does not hang String failoverTransportURL = "failover:(" + transportUrl + ")?timeout=5000"; sendMessage(MY_TEST_Q, failoverTransportURL); //check db contents ArrayList<Long> dbSeq = dbMessageCount(checkOnStoreConnection); LOG.info("*** after send: db contains message seq " + dbSeq); List<TextMessage> consumedMessages = consumeMessages(MY_TEST_Q, failoverTransportURL); assertEquals("number of consumed messages", 3, consumedMessages.size()); //check db contents dbSeq = dbMessageCount(checkOnStoreConnection); LOG.info("*** after consume - db contains message seq " + dbSeq); assertEquals("number of messages in DB after test", 0, dbSeq.size()); } finally { try { checkOnStoreConnection.close(); } catch (Exception ignored) {} broker.stop(); broker.waitUntilStopped(); } } public List<TextMessage> consumeMessages(String queue, String transportURL) throws JMSException { Connection connection = null; LOG.debug("*** consumeMessages() called ..."); try { ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory( transportURL); connection = factory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination = session.createQueue(queue); ArrayList<TextMessage> consumedMessages = new ArrayList<TextMessage>(); MessageConsumer messageConsumer = session.createConsumer(destination); while(true){ TextMessage textMessage= (TextMessage) messageConsumer.receive(4000); LOG.debug("*** consumed Messages :"+textMessage); if(textMessage==null){ return consumedMessages; } consumedMessages.add(textMessage); } } finally { if (connection != null) { connection.close(); } } } public void sendMessage(String queue, String transportURL) throws Exception { Connection connection = null; try { ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory( transportURL); connection = factory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination = session.createQueue(queue); MessageProducer producer = session.createProducer(destination); producer.setDeliveryMode(DeliveryMode.PERSISTENT); TextMessage m = session.createTextMessage("1"); LOG.debug("*** send message 1 to broker..."); producer.send(m); // trigger SQL exception in transactionContext LOG.debug("*** send message 2 to broker"); m.setText("2"); producer.send(m); //check db contents ArrayList<Long> dbSeq = dbMessageCount(checkOnStoreConnection); LOG.info("*** after send 2 - db contains message seq " + dbSeq); assertEquals("number of messages in DB after send 2",2,dbSeq.size()); LOG.debug("*** send message 3 to broker"); m.setText("3"); producer.send(m); LOG.debug("*** Finished sending messages to broker"); } finally { if (connection != null) { connection.close(); } } } /** * query the DB to see what messages are left in the store * @return * @throws SQLException * @throws IOException * @param checkOnStoreConnection */ private ArrayList<Long> dbMessageCount(java.sql.Connection checkOnStoreConnection) throws SQLException, IOException { PreparedStatement statement = checkOnStoreConnection.prepareStatement("SELECT MSGID_SEQ FROM ACTIVEMQ_MSGS"); try{ ResultSet result = statement.executeQuery(); ArrayList<Long> dbSeq = new ArrayList<Long>(); while (result.next()){ dbSeq.add(result.getLong(1)); } return dbSeq; }finally{ statement.close(); } } /* * Mock classes used for testing */ public class TestJDBCPersistenceAdapter extends JDBCPersistenceAdapter { public TransactionContext getTransactionContext() throws IOException { return testTransactionContext; } } public class TestTransactionContext extends TransactionContext { private int count; public TestTransactionContext( JDBCPersistenceAdapter jdbcPersistenceAdapter) throws IOException { super(jdbcPersistenceAdapter); } public void executeBatch() throws SQLException { super.executeBatch(); count++; LOG.debug("ExecuteBatchOverride: count:" + count, new RuntimeException("executeBatch")); // throw on second add message if (count == 16){ throw new SQLException("TEST SQL EXCEPTION from executeBatch after super.execution: count:" + count); } } } }