/* * 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.artemis.tests.integration.journal; import java.io.File; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import org.apache.activemq.artemis.ArtemisConstants; import org.apache.activemq.artemis.core.io.IOCriticalErrorListener; import org.apache.activemq.artemis.core.io.SequentialFile; import org.apache.activemq.artemis.core.io.SequentialFileFactory; import org.apache.activemq.artemis.core.io.aio.AIOSequentialFileFactory; import org.apache.activemq.artemis.core.io.mapped.MappedSequentialFileFactory; import org.apache.activemq.artemis.core.io.nio.NIOSequentialFileFactory; import org.apache.activemq.artemis.core.journal.LoaderCallback; import org.apache.activemq.artemis.core.journal.PreparedTransactionInfo; import org.apache.activemq.artemis.core.journal.RecordInfo; import org.apache.activemq.artemis.core.journal.impl.JournalImpl; import org.apache.activemq.artemis.jlibaio.LibaioContext; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.tests.util.SpawnedVMSupport; import org.junit.Assert; import org.junit.Test; /** * This test spawns a remote VM, as we want to "crash" the VM right after the journal is filled with data */ public class ValidateTransactionHealthTest extends ActiveMQTestBase { private static final int OK = 10; @Test public void testAIO() throws Exception { internalTest("aio", getTestDir(), 10000, 100, true, true, 1); } @Test public void testAIOHugeTransaction() throws Exception { internalTest("aio", getTestDir(), 10000, 10000, true, true, 1); } @Test public void testAIOMultiThread() throws Exception { internalTest("aio", getTestDir(), 1000, 100, true, true, 10); } @Test public void testAIONonTransactionalNoExternalProcess() throws Exception { internalTest("aio", getTestDir(), 1000, 0, true, false, 10); } @Test public void testNIO() throws Exception { internalTest("nio", getTestDir(), 10000, 100, true, true, 1); } @Test public void testNIOHugeTransaction() throws Exception { internalTest("nio", getTestDir(), 10000, 10000, true, true, 1); } @Test public void testNIOMultiThread() throws Exception { internalTest("nio", getTestDir(), 1000, 100, true, true, 10); } @Test public void testNIONonTransactional() throws Exception { internalTest("nio", getTestDir(), 10000, 0, true, true, 1); } @Test public void testNIO2() throws Exception { internalTest("nio2", getTestDir(), 10000, 100, true, true, 1); } @Test public void testNIO2HugeTransaction() throws Exception { internalTest("nio2", getTestDir(), 10000, 10000, true, true, 1); } @Test public void testNIO2MultiThread() throws Exception { internalTest("nio2", getTestDir(), 1000, 100, true, true, 10); } @Test public void testNIO2NonTransactional() throws Exception { internalTest("nio2", getTestDir(), 10000, 0, true, true, 1); } @Test public void testMMap() throws Exception { internalTest("mmap", getTestDir(), 10000, 100, true, true, 1); } @Test public void testMMAPHugeTransaction() throws Exception { internalTest("mmap", getTestDir(), 10000, 10000, true, true, 1); } @Test public void testMMAPOMultiThread() throws Exception { internalTest("mmap", getTestDir(), 1000, 100, true, true, 10); } @Test public void testMMAPNonTransactional() throws Exception { internalTest("mmap", getTestDir(), 10000, 0, true, true, 1); } // Package protected --------------------------------------------- private void internalTest(final String type, final String journalDir, final long numberOfRecords, final int transactionSize, final boolean append, final boolean externalProcess, final int numberOfThreads) throws Exception { try { if (type.equals("aio") && !LibaioContext.isLoaded()) { // Using System.out as this output will go towards junit report System.out.println("AIO not found, test being ignored on this platform"); return; } // This property could be set to false for debug purposes. if (append) { if (externalProcess) { Process process = SpawnedVMSupport.spawnVM(ValidateTransactionHealthTest.class.getCanonicalName(), type, journalDir, Long.toString(numberOfRecords), Integer.toString(transactionSize), Integer.toString(numberOfThreads)); process.waitFor(); Assert.assertEquals(ValidateTransactionHealthTest.OK, process.exitValue()); } else { JournalImpl journal = ValidateTransactionHealthTest.appendData(type, journalDir, numberOfRecords, transactionSize, numberOfThreads); journal.stop(); } } reload(type, journalDir, numberOfRecords, numberOfThreads); } finally { File file = new File(journalDir); deleteDirectory(file); } } private void reload(final String type, final String journalDir, final long numberOfRecords, final int numberOfThreads) throws Exception { JournalImpl journal = ValidateTransactionHealthTest.createJournal(type, journalDir); journal.start(); try { Loader loadTest = new Loader(numberOfRecords); journal.load(loadTest); Assert.assertEquals(numberOfRecords * numberOfThreads, loadTest.numberOfAdds); Assert.assertEquals(0, loadTest.numberOfPreparedTransactions); Assert.assertEquals(0, loadTest.numberOfUpdates); Assert.assertEquals(0, loadTest.numberOfDeletes); if (loadTest.ex != null) { throw loadTest.ex; } } finally { journal.stop(); } } // Inner classes ------------------------------------------------- static class Loader implements LoaderCallback { int numberOfPreparedTransactions = 0; int numberOfAdds = 0; int numberOfDeletes = 0; int numberOfUpdates = 0; long expectedRecords = 0; Exception ex = null; long lastID = 0; Loader(final long expectedRecords) { this.expectedRecords = expectedRecords; } @Override public void addPreparedTransaction(final PreparedTransactionInfo preparedTransaction) { numberOfPreparedTransactions++; } @Override public void addRecord(final RecordInfo info) { if (info.id == lastID) { System.out.println("id = " + info.id + " last id = " + lastID); } ByteBuffer buffer = ByteBuffer.wrap(info.data); long recordValue = buffer.getLong(); if (recordValue != info.id) { ex = new Exception("Content not as expected (" + recordValue + " != " + info.id + ")"); } lastID = info.id; numberOfAdds++; } @Override public void deleteRecord(final long id) { numberOfDeletes++; } @Override public void updateRecord(final RecordInfo info) { numberOfUpdates++; } @Override public void failedTransaction(final long transactionID, final List<RecordInfo> records, final List<RecordInfo> recordsToDelete) { } } // Remote part of the test ================================================================= public static void main(final String[] args) throws Exception { if (args.length != 5) { System.err.println("Use: java -cp <classpath> " + ValidateTransactionHealthTest.class.getCanonicalName() + " aio|nio|mmap <journalDirectory> <NumberOfElements> <TransactionSize> <NumberOfThreads>"); System.exit(-1); } System.out.println("Running"); String journalType = args[0]; String journalDir = args[1]; long numberOfElements = Long.parseLong(args[2]); int transactionSize = Integer.parseInt(args[3]); int numberOfThreads = Integer.parseInt(args[4]); try { ValidateTransactionHealthTest.appendData(journalType, journalDir, numberOfElements, transactionSize, numberOfThreads); // We don't stop the journal on the case of an external process... // The test is making sure that committed data can be reloaded fine... // i.e. commits are sync on disk as stated on the transaction. // The journal shouldn't leave any state impeding reloading the server } catch (Exception e) { e.printStackTrace(System.out); System.exit(-1); } // Simulating a crash on the system right after the data was committed. Runtime.getRuntime().halt(ValidateTransactionHealthTest.OK); } public static JournalImpl appendData(final String journalType, final String journalDir, final long numberOfElements, final int transactionSize, final int numberOfThreads) throws Exception { final JournalImpl journal = ValidateTransactionHealthTest.createJournal(journalType, journalDir); journal.start(); journal.load(new LoaderCallback() { @Override public void addPreparedTransaction(final PreparedTransactionInfo preparedTransaction) { } @Override public void addRecord(final RecordInfo info) { } @Override public void deleteRecord(final long id) { } @Override public void updateRecord(final RecordInfo info) { } @Override public void failedTransaction(final long transactionID, final List<RecordInfo> records, final List<RecordInfo> recordsToDelete) { } }); LocalThread[] threads = new LocalThread[numberOfThreads]; final AtomicLong sequenceTransaction = new AtomicLong(); for (int i = 0; i < numberOfThreads; i++) { threads[i] = new LocalThread(journal, numberOfElements, transactionSize, sequenceTransaction); threads[i].start(); } Exception e = null; for (LocalThread t : threads) { t.join(); if (t.e != null) { e = t.e; } } if (e != null) { throw e; } journal.flush(); return journal; } public static JournalImpl createJournal(final String journalType, final String journalDir) { JournalImpl journal = new JournalImpl(10485760, 2, 2, 0, 0, ValidateTransactionHealthTest.getFactory(journalType, journalDir, 10485760), "journaltst", "tst", 500); return journal; } public static SequentialFileFactory getFactory(final String factoryType, final String directory, int fileSize) { if (factoryType.equals("aio")) { return new AIOSequentialFileFactory(new File(directory), ArtemisConstants.DEFAULT_JOURNAL_BUFFER_SIZE_AIO, ArtemisConstants.DEFAULT_JOURNAL_BUFFER_TIMEOUT_AIO, 10, false); } else if (factoryType.equals("nio2")) { return new NIOSequentialFileFactory(new File(directory), true, 1); } else if (factoryType.equals("mmap")) { return new MappedSequentialFileFactory(new File(directory), new IOCriticalErrorListener() { @Override public void onIOException(Throwable code, String message, SequentialFile file) { code.printStackTrace(); } }, true).chunkBytes(fileSize).overlapBytes(0); } else { return new NIOSequentialFileFactory(new File(directory), false, 1); } } static class LocalThread extends Thread { final JournalImpl journal; final long numberOfElements; final int transactionSize; final AtomicLong nextID; Exception e; LocalThread(final JournalImpl journal, final long numberOfElements, final int transactionSize, final AtomicLong nextID) { super(); this.journal = journal; this.numberOfElements = numberOfElements; this.transactionSize = transactionSize; this.nextID = nextID; } @Override public void run() { try { int transactionCounter = 0; long transactionId = nextID.incrementAndGet(); for (long i = 0; i < numberOfElements; i++) { long id = nextID.incrementAndGet(); ByteBuffer buffer = ByteBuffer.allocate(512 * 3); buffer.putLong(id); if (transactionSize != 0) { journal.appendAddRecordTransactional(transactionId, id, (byte) 99, buffer.array()); if (++transactionCounter == transactionSize) { System.out.println("Commit transaction " + transactionId); journal.appendCommitRecord(transactionId, true); transactionCounter = 0; transactionId = nextID.incrementAndGet(); } } else { journal.appendAddRecord(id, (byte) 99, buffer.array(), false); } } if (transactionCounter != 0) { journal.appendCommitRecord(transactionId, true); } if (transactionSize == 0) { journal.debugWait(); } } catch (Exception e) { this.e = e; } } } }