/** * 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.store.kahadb; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.broker.region.policy.PolicyEntry; import org.apache.activemq.broker.region.policy.PolicyMap; import org.apache.activemq.command.ActiveMQQueue; import org.apache.activemq.store.kahadb.disk.journal.DataFile; import org.apache.activemq.store.kahadb.disk.journal.Journal; import org.apache.activemq.util.ByteSequence; import org.apache.activemq.util.RecoverableRandomAccessFile; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.Message; import javax.jms.MessageProducer; import javax.jms.Session; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import static org.apache.activemq.store.kahadb.JournalCorruptionEofIndexRecoveryTest.drain; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) public class JournalCorruptionExceptionTest { private static final Logger LOG = LoggerFactory.getLogger(JournalCorruptionExceptionTest.class); private final String KAHADB_DIRECTORY = "target/activemq-data/"; private final String payload = new String(new byte[1024]); private ActiveMQConnectionFactory cf = null; private BrokerService broker = null; private final Destination destination = new ActiveMQQueue("Test"); private String connectionUri; private KahaDBPersistenceAdapter adapter; @Parameterized.Parameter(0) public byte fill = Byte.valueOf("3"); @Parameterized.Parameter(1) public int fillLength = 10; @Parameterized.Parameters(name = "fill=#{0},#{1}") public static Iterable<Object[]> parameters() { // corruption can be valid record type values return Arrays.asList(new Object[][]{ {Byte.valueOf("0"), 6}, {Byte.valueOf("1"), 8}, {Byte.valueOf("-1"), 6}, {Byte.valueOf("0"), 10}, {Byte.valueOf("1"), 10}, {Byte.valueOf("2"), 10}, {Byte.valueOf("-1"), 10} }); } protected void startBroker() throws Exception { doStartBroker(true); } private void doStartBroker(boolean delete) throws Exception { broker = new BrokerService(); broker.setDeleteAllMessagesOnStartup(delete); broker.setPersistent(true); broker.setUseJmx(true); broker.setDataDirectory(KAHADB_DIRECTORY); broker.addConnector("tcp://localhost:0"); PolicyMap policyMap = new PolicyMap(); PolicyEntry defaultEntry = new PolicyEntry(); defaultEntry.setUseCache(false); defaultEntry.setExpireMessagesPeriod(0); policyMap.setDefaultEntry(defaultEntry); broker.setDestinationPolicy(policyMap); configurePersistence(broker); connectionUri = "vm://localhost?create=false"; cf = new ActiveMQConnectionFactory(connectionUri); broker.start(); LOG.info("Starting broker.."); } protected void configurePersistence(BrokerService brokerService) throws Exception { adapter = (KahaDBPersistenceAdapter) brokerService.getPersistenceAdapter(); // ensure there are a bunch of data files but multiple entries in each adapter.setJournalMaxFileLength(1024 * 20); } @After public void tearDown() throws Exception { if (broker != null) { broker.stop(); broker.waitUntilStopped(); } } @Test public void testIOExceptionOnCorruptJournalLocationRead() throws Exception { startBroker(); produceMessagesToConsumeMultipleDataFiles(50); int numFiles = getNumberOfJournalFiles(); assertTrue("more than x files: " + numFiles, numFiles > 4); corruptLocationAtDataFileIndex(3); assertEquals("missing one message", 50, broker.getAdminView().getTotalMessageCount()); assertEquals("Drain", 0, drainQueue(50)); assertTrue("broker stopping", broker.isStopping()); } private void corruptLocationAtDataFileIndex(int id) throws IOException { Collection<DataFile> files = ((KahaDBPersistenceAdapter) broker.getPersistenceAdapter()).getStore().getJournal().getFileMap().values(); DataFile dataFile = (DataFile) files.toArray()[id]; RecoverableRandomAccessFile randomAccessFile = dataFile.openRandomAccessFile(); final ByteSequence header = new ByteSequence(Journal.BATCH_CONTROL_RECORD_HEADER); byte data[] = new byte[1024 * 20]; ByteSequence bs = new ByteSequence(data, 0, randomAccessFile.read(data, 0, data.length)); int offset = bs.indexOf(header, 0); offset += Journal.BATCH_CONTROL_RECORD_SIZE; if (fillLength >= 10) { offset += 4; // location size offset += 1; // location type } LOG.info("Whacking batch record in file:" + id + ", at offset: " + offset + " with fill:" + fill); // whack that record byte[] bla = new byte[fillLength]; Arrays.fill(bla, fill); randomAccessFile.seek(offset); randomAccessFile.write(bla, 0, bla.length); randomAccessFile.getFD().sync(); } private int getNumberOfJournalFiles() throws IOException { Collection<DataFile> files = ((KahaDBPersistenceAdapter) broker.getPersistenceAdapter()).getStore().getJournal().getFileMap().values(); int reality = 0; for (DataFile file : files) { if (file != null) { reality++; } } return reality; } private int produceMessages(Destination destination, int numToSend) throws Exception { int sent = 0; Connection connection = new ActiveMQConnectionFactory(broker.getTransportConnectors().get(0).getConnectUri()).createConnection(); connection.start(); try { Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(destination); for (int i = 0; i < numToSend; i++) { producer.send(createMessage(session, i)); sent++; } } finally { connection.close(); } return sent; } private int produceMessagesToConsumeMultipleDataFiles(int numToSend) throws Exception { return produceMessages(destination, numToSend); } private Message createMessage(Session session, int i) throws Exception { return session.createTextMessage(payload + "::" + i); } private int drainQueue(int max) throws Exception { return drain(cf, destination, max); } }