/* * 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.unit.core.journal.impl; import java.io.File; import java.io.FilenameFilter; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.apache.activemq.artemis.cli.commands.tools.journal.DecodeJournal; import org.apache.activemq.artemis.cli.commands.tools.journal.EncodeJournal; import org.apache.activemq.artemis.core.io.SequentialFileFactory; import org.apache.activemq.artemis.core.journal.EncodingSupport; import org.apache.activemq.artemis.core.journal.PreparedTransactionInfo; import org.apache.activemq.artemis.core.journal.RecordInfo; import org.apache.activemq.artemis.core.journal.TestableJournal; import org.apache.activemq.artemis.core.journal.impl.JournalImpl; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.ReusableLatch; import org.jboss.logging.Logger; import org.junit.After; import org.junit.Assert; import org.junit.Before; public abstract class JournalImplTestBase extends ActiveMQTestBase { private static final Logger logger = Logger.getLogger(JournalImplTestBase.class); protected List<RecordInfo> records = new LinkedList<>(); protected TestableJournal journal; protected int recordLength = 1024; protected Map<Long, TransactionHolder> transactions = new LinkedHashMap<>(); protected int maxAIO; protected int minFiles; protected int poolSize; protected int fileSize; protected boolean sync; protected String filePrefix = "amq"; protected String fileExtension = "amq"; protected SequentialFileFactory fileFactory; private final ReusableLatch latchDone = new ReusableLatch(0); private final ReusableLatch latchWait = new ReusableLatch(0); private Thread compactThread; @Override @Before public void setUp() throws Exception { super.setUp(); resetFileFactory(); fileFactory.start(); transactions.clear(); records.clear(); } @Override @After public void tearDown() throws Exception { stopComponent(journal); if (fileFactory != null) { fileFactory.stop(); } fileFactory = null; journal = null; super.tearDown(); } protected void resetFileFactory() throws Exception { if (fileFactory != null) { fileFactory.stop(); } fileFactory = getFileFactory(); } protected void checkAndReclaimFiles() throws Exception { journal.debugWait(); boolean originalAutoReclaim = journal.isAutoReclaim(); journal.setAutoReclaim(true); journal.checkReclaimStatus(); journal.setAutoReclaim(originalAutoReclaim); journal.debugWait(); } protected abstract SequentialFileFactory getFileFactory() throws Exception; // Private // --------------------------------------------------------------------------------- protected void setup(final int minFreeFiles, final int fileSize, final boolean sync, final int maxAIO) { this.minFiles = minFreeFiles; this.poolSize = minFreeFiles; this.fileSize = fileSize; this.sync = sync; this.maxAIO = maxAIO; } protected void setup(final int minFreeFiles, final int poolSize, final int fileSize, final boolean sync, final int maxAIO) { minFiles = minFreeFiles; this.poolSize = poolSize; this.fileSize = fileSize; this.sync = sync; this.maxAIO = maxAIO; } protected void setup(final int minFreeFiles, final int fileSize, final boolean sync) { minFiles = minFreeFiles; poolSize = minFreeFiles; this.fileSize = fileSize; this.sync = sync; maxAIO = 50; } public void createJournal() throws Exception { journal = new JournalImpl(fileSize, minFiles, poolSize, 0, 0, fileFactory, filePrefix, fileExtension, maxAIO) { @Override public void onCompactDone() { latchDone.countDown(); try { latchWait.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }; journal.setAutoReclaim(false); addActiveMQComponent(journal); } // It will start compacting, but it will let the thread in wait mode at onCompactDone, so we can validate command // executions protected void startCompact() throws Exception { latchDone.setCount(1); latchWait.setCount(1); this.compactThread = new Thread() { @Override public void run() { try { journal.testCompact(); } catch (Throwable e) { e.printStackTrace(); } } }; this.compactThread.start(); latchDone.await(); } protected void finishCompact() throws Exception { latchWait.countDown(); compactThread.join(); } protected void startJournal() throws Exception { journal.start(); } protected void stopJournal() throws Exception { stopJournal(true); } protected void stopJournal(final boolean reclaim) throws Exception { // We do a reclaim in here if (reclaim) { checkAndReclaimFiles(); } journal.stop(); } /** * @throws Exception */ protected void exportImportJournal() throws Exception { System.out.println("Exporting to " + getTestDir() + "/output.log"); EncodeJournal.exportJournal(getTestDir(), this.filePrefix, this.fileExtension, this.minFiles, this.fileSize, getTestDir() + "/output.log"); File dir = new File(getTestDir()); FilenameFilter fnf = new FilenameFilter() { @Override public boolean accept(final File file, final String name) { return name.endsWith("." + fileExtension); } }; System.out.println("file = " + dir); File[] files = dir.listFiles(fnf); for (File file : files) { System.out.println("Deleting " + file); file.delete(); } DecodeJournal.importJournal(getTestDir(), filePrefix, fileExtension, minFiles, fileSize, getTestDir() + "/output.log"); } protected void loadAndCheck() throws Exception { loadAndCheck(false); } protected void loadAndCheck(final boolean printDebugJournal) throws Exception { List<RecordInfo> committedRecords = new ArrayList<>(); List<PreparedTransactionInfo> preparedTransactions = new ArrayList<>(); journal.load(committedRecords, preparedTransactions, null); checkRecordsEquivalent(records, committedRecords); if (printDebugJournal) { printJournalLists(records, committedRecords); } // check prepared transactions List<PreparedTransactionInfo> prepared = new ArrayList<>(); for (Map.Entry<Long, TransactionHolder> entry : transactions.entrySet()) { if (entry.getValue().prepared) { PreparedTransactionInfo info = new PreparedTransactionInfo(entry.getKey(), null); info.getRecords().addAll(entry.getValue().records); info.getRecordsToDelete().addAll(entry.getValue().deletes); prepared.add(info); } } checkTransactionsEquivalent(prepared, preparedTransactions); } protected void load() throws Exception { journal.load(null, null, null); } protected void beforeJournalOperation() throws Exception { } protected void add(final long... arguments) throws Exception { addWithSize(recordLength, arguments); } protected void addWithSize(final int size, final long... arguments) throws Exception { for (long element : arguments) { byte[] record = generateRecord(size); beforeJournalOperation(); journal.appendAddRecord(element, (byte) 0, record, sync); records.add(new RecordInfo(element, (byte) 0, record, false, (short) 0)); } journal.debugWait(); } protected void update(final long... arguments) throws Exception { for (long element : arguments) { byte[] updateRecord = generateRecord(recordLength); beforeJournalOperation(); journal.appendUpdateRecord(element, (byte) 0, updateRecord, sync); records.add(new RecordInfo(element, (byte) 0, updateRecord, true, (short) 0)); } journal.debugWait(); } protected void delete(final long... arguments) throws Exception { for (long element : arguments) { beforeJournalOperation(); journal.appendDeleteRecord(element, sync); removeRecordsForID(element); } journal.debugWait(); } protected void addTx(final long txID, final long... arguments) throws Exception { TransactionHolder tx = getTransaction(txID); for (long element : arguments) { // SIZE_BYTE + SIZE_LONG + SIZE_LONG + SIZE_INT + record.length + // SIZE_BYTE byte[] record = generateRecord(recordLength - (JournalImpl.SIZE_ADD_RECORD_TX + 1)); beforeJournalOperation(); journal.appendAddRecordTransactional(txID, element, (byte) 0, record); tx.records.add(new RecordInfo(element, (byte) 0, record, false, (short) 0)); } journal.debugWait(); } protected void updateTx(final long txID, final long... arguments) throws Exception { TransactionHolder tx = getTransaction(txID); for (long element : arguments) { byte[] updateRecord = generateRecord(recordLength - (JournalImpl.SIZE_ADD_RECORD_TX + 1)); beforeJournalOperation(); journal.appendUpdateRecordTransactional(txID, element, (byte) 0, updateRecord); tx.records.add(new RecordInfo(element, (byte) 0, updateRecord, true, (short) 0)); } journal.debugWait(); } protected void deleteTx(final long txID, final long... arguments) throws Exception { TransactionHolder tx = getTransaction(txID); for (long element : arguments) { beforeJournalOperation(); journal.appendDeleteRecordTransactional(txID, element); tx.deletes.add(new RecordInfo(element, (byte) 0, null, true, (short) 0)); } journal.debugWait(); } protected void prepare(final long txID, final EncodingSupport xid) throws Exception { TransactionHolder tx = transactions.get(txID); if (tx == null) { tx = new TransactionHolder(); transactions.put(txID, tx); } if (tx.prepared) { throw new IllegalStateException("Transaction is already prepared"); } beforeJournalOperation(); journal.appendPrepareRecord(txID, xid, sync); tx.prepared = true; journal.debugWait(); } protected void commit(final long txID) throws Exception { TransactionHolder tx = transactions.remove(txID); if (tx == null) { throw new IllegalStateException("Cannot find tx " + txID); } beforeJournalOperation(); journal.appendCommitRecord(txID, sync); records.addAll(tx.records); for (RecordInfo l : tx.deletes) { removeRecordsForID(l.id); } journal.debugWait(); } protected void rollback(final long txID) throws Exception { TransactionHolder tx = transactions.remove(txID); if (tx == null) { throw new IllegalStateException("Cannot find tx " + txID); } beforeJournalOperation(); journal.appendRollbackRecord(txID, sync); journal.debugWait(); } protected void removeRecordsForID(final long id) { for (ListIterator<RecordInfo> iter = records.listIterator(); iter.hasNext(); ) { RecordInfo info = iter.next(); if (info.id == id) { iter.remove(); } } } protected TransactionHolder getTransaction(final long txID) { TransactionHolder tx = transactions.get(txID); if (tx == null) { tx = new TransactionHolder(); transactions.put(txID, tx); } return tx; } protected void checkTransactionsEquivalent(final List<PreparedTransactionInfo> expected, final List<PreparedTransactionInfo> actual) { Assert.assertEquals("Lists not same length", expected.size(), actual.size()); Iterator<PreparedTransactionInfo> iterExpected = expected.iterator(); Iterator<PreparedTransactionInfo> iterActual = actual.iterator(); while (iterExpected.hasNext()) { PreparedTransactionInfo rexpected = iterExpected.next(); PreparedTransactionInfo ractual = iterActual.next(); Assert.assertEquals("ids not same", rexpected.getId(), ractual.getId()); checkRecordsEquivalent(rexpected.getRecords(), ractual.getRecords()); Assert.assertEquals("deletes size not same", rexpected.getRecordsToDelete().size(), ractual.getRecordsToDelete().size()); Iterator<RecordInfo> iterDeletesExpected = rexpected.getRecordsToDelete().iterator(); Iterator<RecordInfo> iterDeletesActual = ractual.getRecordsToDelete().iterator(); while (iterDeletesExpected.hasNext()) { long lexpected = iterDeletesExpected.next().id; long lactual = iterDeletesActual.next().id; Assert.assertEquals("Delete ids not same", lexpected, lactual); } } } protected void checkRecordsEquivalent(final List<RecordInfo> expected, final List<RecordInfo> actual) { if (expected.size() != actual.size()) { printJournalLists(expected, actual); } Assert.assertEquals("Lists not same length", expected.size(), actual.size()); Iterator<RecordInfo> iterExpected = expected.iterator(); Iterator<RecordInfo> iterActual = actual.iterator(); while (iterExpected.hasNext()) { RecordInfo rexpected = iterExpected.next(); RecordInfo ractual = iterActual.next(); if (rexpected.id != ractual.id || rexpected.isUpdate != ractual.isUpdate) { printJournalLists(expected, actual); } Assert.assertEquals("ids not same", rexpected.id, ractual.id); Assert.assertEquals("type not same", rexpected.isUpdate, ractual.isUpdate); ActiveMQTestBase.assertEqualsByteArrays(rexpected.data, ractual.data); } } /** * @param expected * @param actual */ protected void printJournalLists(final List<RecordInfo> expected, final List<RecordInfo> actual) { HashSet<RecordInfo> expectedSet = new HashSet<>(); expectedSet.addAll(expected); Assert.assertEquals("There are duplicated on the expected list", expectedSet.size(), expected.size()); HashSet<RecordInfo> actualSet = new HashSet<>(); actualSet.addAll(actual); expectedSet.removeAll(actualSet); for (RecordInfo info: expectedSet) { logger.warn("The following record is missing:: " + info); } Assert.assertEquals("There are duplicates on the actual list", actualSet.size(), actualSet.size()); RecordInfo[] expectedArray = expected.toArray(new RecordInfo[expected.size()]); RecordInfo[] actualArray = actual.toArray(new RecordInfo[actual.size()]); Assert.assertArrayEquals(expectedArray, actualArray); } protected byte[] generateRecord(final int length) { byte[] record = new byte[length]; for (int i = 0; i < length; i++) { // record[i] = RandomUtil.randomByte(); record[i] = ActiveMQTestBase.getSamplebyte(i); } return record; } protected String debugJournal() throws Exception { return "***************************************************\n" + ((JournalImpl) journal).debug() + "***************************************************\n"; } static final class TransactionHolder { List<RecordInfo> records = new ArrayList<>(); List<RecordInfo> deletes = new ArrayList<>(); boolean prepared; } }