package io.eguan.dtx.journal; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * Licensed 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. * #L% */ import static io.eguan.dtx.DtxConstants.DEFAULT_JOURNAL_FILE_PREFIX; import static io.eguan.dtx.DtxConstants.DEFAULT_LAST_TX_VALUE; import static io.eguan.dtx.DtxConstants.JOURNAL_FILE_EXTENSION; import static io.eguan.dtx.DtxTestHelper.DEFAULT_TX_MESSAGE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import io.eguan.dtx.DtxTestHelper; import io.eguan.dtx.TestTransactionManagerErrorCases; import io.eguan.dtx.TestTransactionManagerErrorCases.TestOp; import io.eguan.dtx.journal.JournalRecord; import io.eguan.dtx.journal.JournalRotationManager; import io.eguan.dtx.journal.WritableTxJournal; import io.eguan.proto.Common.ProtocolVersion; import io.eguan.proto.dtx.DistTxWrapper; import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry; import io.eguan.proto.dtx.DistTxWrapper.TxMessage; import io.eguan.proto.dtx.DistTxWrapper.TxNode; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runners.model.InitializationError; /** * Tests for the {@link WritableTxJournal} class. * * @author oodrive * @author pwehrle * */ public class TestWritableTxJournal { private static final int TEST_THREAD_COUNT = 10; private static final int TEST_WRITES_PER_THREAD = 5; private static final int TEST_READS_PER_THREAD = 7; private static final int TEST_NB_OF_TEST_ENTRIES = 30; private static final int TEST_NB_OF_START_CYCLES = 6; private static final long TEST_ROTATION_THRESHOLD = 3221225472L; // keep this high to avoid rotation private static final Set<TxNode> PARTICIPANTS = DtxTestHelper.newRandomParticipantsSet(); private static class JournalTestWriter implements Callable<Void> { private final WritableTxJournal target; private final TestOp op; private final int rep; JournalTestWriter(final WritableTxJournal target, final TestTransactionManagerErrorCases.TestOp operation, final int nbOfRepetitions) { this.target = target; this.op = operation; this.rep = nbOfRepetitions; } @Override public Void call() { final TxMessage defTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).build(); final Random rnd = new Random(System.currentTimeMillis()); for (int i = 0; i < rep; i++) { try { switch (op) { case START: target.writeStart(defTx, PARTICIPANTS); break; case COMMIT: target.writeCommit(rnd.nextLong(), PARTICIPANTS); break; case ROLLBACK: target.writeRollback(rnd.nextLong(), -1, PARTICIPANTS); break; default: break; } } catch (IllegalStateException | IOException e) { e.printStackTrace(); } } return null; } } private static class JournalTestReader implements Callable<Void> { private final WritableTxJournal target; private final int rep; JournalTestReader(final WritableTxJournal target, final int nbOfRepetitions) { this.target = target; this.rep = nbOfRepetitions; } @Override public Void call() throws Exception { for (int i = 0; i < rep; i++) { for (final JournalRecord currRecord : target) { TxJournalEntry.parseFrom(currRecord.getEntry()); } } return null; } } private Path tmpFileDir; private Path roFileDir; private JournalRotationManager journalRotMgr; private WritableTxJournal target; /** * Sets up common fixture. * * @throws InitializationError * if creating some temporary directory fails */ @Before public final void setUp() throws InitializationError { try { final String tmpPrefix = TestWritableTxJournal.class.getSimpleName(); tmpFileDir = Files.createTempDirectory(tmpPrefix); roFileDir = Files.createTempDirectory(tmpPrefix, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("--x------"))); assertFalse(Files.isReadable(roFileDir)); } catch (final IOException e) { throw new InitializationError(e); } // this instance remains stopped on purpose so tests will fail if any rotation is submitted during writes journalRotMgr = new JournalRotationManager(0); this.target = new WritableTxJournal(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX, TEST_ROTATION_THRESHOLD, journalRotMgr); } /** * Tears down common fixture. * * @throws InitializationError * if deleting some of the temporary files and directories fails */ @After public final void tearDown() throws InitializationError { final ArrayList<Throwable> exceptionList = new ArrayList<Throwable>(); if (target.isStarted()) { try { target.stop(); } catch (final IOException e) { exceptionList.add(new InitializationError(e)); } } try { io.eguan.utils.Files.deleteRecursive(tmpFileDir); } catch (final IOException e) { exceptionList.add(new InitializationError(e)); } try { Files.setPosixFilePermissions(roFileDir, PosixFilePermissions.fromString("rwx------")); io.eguan.utils.Files.deleteRecursive(roFileDir); } catch (final IOException e) { exceptionList.add(new InitializationError(e)); } if (!exceptionList.isEmpty()) { throw new InitializationError(exceptionList); } } /** * Tests creating and starting a journal with and without an explicit filename prefix. * * @throws IOException * if initializing the journal fails, not part of this test */ @Test public final void testStartFileCreation() throws IOException { final File parentDir = tmpFileDir.toFile(); final WritableTxJournal noPrefixJournal = new WritableTxJournal(parentDir, null, 0, journalRotMgr); noPrefixJournal.start(); assertTrue(new File(parentDir, DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION).exists()); noPrefixJournal.stop(); assertFalse(noPrefixJournal.isStarted()); final WritableTxJournal prefixedJournal = new WritableTxJournal(parentDir, DEFAULT_JOURNAL_FILE_PREFIX, 0, journalRotMgr); prefixedJournal.start(); assertTrue(new File(parentDir, DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION).exists()); prefixedJournal.stop(); assertFalse(prefixedJournal.isStarted()); } /** * Tests (repeatedly) starting and stopping a given journal. * * @throws IOException * if initializing the journal fails, not part of this test */ @Test public final void testStartStopJournal() throws IOException { target.start(); assertTrue(new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION).exists()); // tests idempotence of start() target.start(); assertTrue(target.isStarted()); target.stop(); assertFalse(target.isStarted()); // tests idempotence of stop() target.stop(); assertFalse(target.isStarted()); // starts once more target.start(); assertTrue(target.isStarted()); // and finally stops target.stop(); assertFalse(target.isStarted()); } /** * Tests (repeatedly) starting and stopping a given journal, while checking written and recovered transaction IDs. * * @throws IOException * if initializing the journal fails, not part of this test */ @Test public final void testStartStopJournalRecovery() throws IOException { long lastTxId = DEFAULT_LAST_TX_VALUE; for (int i = 0; i < TEST_NB_OF_START_CYCLES; i++) { target.start(); // checks after (re)start assertEquals(lastTxId, target.getLastFinishedTxId()); lastTxId = DtxTestHelper.writeCompleteTransactions(target, TEST_NB_OF_TEST_ENTRIES, null, PARTICIPANTS); // checks directly after write assertEquals(lastTxId, target.getLastFinishedTxId()); final long nextTxId = DtxTestHelper.nextTxId(); assertTrue(lastTxId < nextTxId); target.writeStart(TxMessage.newBuilder(DEFAULT_TX_MESSAGE).setVersion(ProtocolVersion.VERSION_1).setTxId(nextTxId).build(), PARTICIPANTS); // checks after partial transaction write assertEquals(lastTxId, target.getLastFinishedTxId()); target.writeCommit(nextTxId, PARTICIPANTS); // checks after completing partial transaction write assertEquals(nextTxId, target.getLastFinishedTxId()); lastTxId = nextTxId; target.stop(); } } /** * Tests failure of the {@link WritableTxJournal#start()} method due to a missing target directory. * * @throws IllegalArgumentException * if construction fails, not part of this test * @throws IOException * if initializing the journal fails, not part of this test * @throws IllegalStateException * expected for this test */ @Test(expected = IllegalStateException.class) public final void testStartJournalFailNoDirectory() throws IllegalArgumentException, IOException, IllegalStateException { final Path vanishingDir = Files.createTempDirectory(TestWritableTxJournal.class.getSimpleName()); Files.deleteIfExists(vanishingDir); assertFalse(Files.exists(vanishingDir)); final WritableTxJournal target = new WritableTxJournal(vanishingDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX, 0, journalRotMgr); target.start(); } /** * Tests failure of the {@link WritableTxJournal#start()} method due to a read-only target directory. * * @throws IllegalArgumentException * if construction fails, not part of this test * @throws IOException * if initializing the journal fails, not part of this test * @throws IllegalStateException * expected for this test */ @Test(expected = IllegalStateException.class) public final void testStartJournalFailDirectoryNotWritable() throws IllegalArgumentException, IOException { final WritableTxJournal target = new WritableTxJournal(roFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX, 0, journalRotMgr); target.start(); } /** * Tests failure of the {@link WritableTxJournal#writeStart(TxMessage)} method due to the journal not being started. * * @throws IllegalStateException * expected for this test * @throws IOException * not part of this test */ @Test(expected = IllegalStateException.class) public final void testWriteStartFailNotStarted() throws IllegalStateException, IOException { assertFalse(target.isStarted()); final TxMessage defTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).build(); target.writeStart(defTx, PARTICIPANTS); } /** * Tests failure of the {@link WritableTxJournal#writeCommit(long)} method due to the journal not being started. * * @throws IllegalStateException * expected for this test * @throws IOException * not part of this test */ @Test(expected = IllegalStateException.class) public final void testWriteCommitFailNotStarted() throws IllegalStateException, IOException { assertFalse(target.isStarted()); target.writeCommit(DtxTestHelper.nextTxId(), PARTICIPANTS); } /** * Tests failure of the {@link WritableTxJournal#writeRollback(long, int)} method due to the journal not being * started. * * @throws IllegalStateException * expected for this test * @throws IOException * not part of this test */ @Test(expected = IllegalStateException.class) public final void testWriteRollbackFailNotStarted() throws IllegalStateException, IOException { assertFalse(target.isStarted()); target.writeRollback(DtxTestHelper.nextTxId(), -1, PARTICIPANTS); } /** * Tests the {@link Iterator} provided by {@link WritableTxJournal#iterator()}, especially its failure when reaching * the end of the file. * * @throws IllegalStateException * not part of this test * @throws IOException * not part of this test * @throws NoSuchElementException * expected for this test */ @Test(expected = NoSuchElementException.class) public final void testIteratorFailAtEnd() throws IllegalStateException, IOException, NoSuchElementException { target.start(); final File journalFile = new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES; i++) { final long txId = DtxTestHelper.nextTxId(); final TxMessage defTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).build(); target.writeStart(defTx, PARTICIPANTS); // writes commits and rollbacks for every other transaction if (i % 2 == 0) { target.writeRollback(txId, -1, PARTICIPANTS); } else { target.writeCommit(txId, PARTICIPANTS); } } final Iterator<JournalRecord> journalIter = target.iterator(); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES * 2; i++) { assertTrue(journalIter.hasNext()); assertNotNull(journalIter.next()); } assertFalse(journalIter.hasNext()); journalIter.next(); } /** * Tests the {@link Iterator} provided by {@link WritableTxJournal#iterator()}, in particular its failure upon * calling {@link Iterator#remove()}. * * @throws IllegalStateException * not part of this test * @throws IOException * not part of this test * @throws UnsupportedOperationException * expected for this test */ @Test(expected = UnsupportedOperationException.class) public final void testIteratorFailOnRemove() throws IllegalStateException, IOException, UnsupportedOperationException { final File parentDir = tmpFileDir.toFile(); target.start(); final File journalFile = new File(parentDir, DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); final Iterator<JournalRecord> journalIter = target.iterator(); journalIter.remove(); } /** * Tests the {@link Iterator} provided by {@link WritableTxJournal#iterator()}, especially its correct behavior upon * stopping the underlying journal instance. * * @throws IllegalStateException * not part of this test * @throws IOException * not part of this test * @throws NoSuchElementException * expected for this test */ @Test(expected = NoSuchElementException.class) public final void testIteratorEndIterationUponStop() throws IllegalStateException, IOException { target.start(); final File journalFile = new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES; i++) { // only write commits to save time target.writeCommit(DtxTestHelper.nextTxId(), PARTICIPANTS); } final Iterator<JournalRecord> journalIter = target.iterator(); // iterate up to half of the entries for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES / 2; i++) { assertTrue(journalIter.hasNext()); assertNotNull(journalIter.next()); } target.stop(); // checks for the last read element assertTrue(journalIter.hasNext()); assertNotNull(journalIter.next()); // no further elements should be available assertFalse(journalIter.hasNext()); journalIter.next(); } /** * Tests the {@link Iterator} provided by {@link WritableTxJournal#iterator()} on a stopped journal. * * @throws IllegalStateException * not part of this test * @throws IOException * not part of this test * @throws NoSuchElementException * expected for this test */ @Test(expected = NoSuchElementException.class) public final void testIteratorNoIterationOnStopped() throws IllegalStateException, IOException { target.start(); final File journalFile = new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES; i++) { // only write commits to save time target.writeCommit(DtxTestHelper.nextTxId(), PARTICIPANTS); } target.stop(); final Iterator<JournalRecord> journalIter = target.iterator(); // no elements should be available assertFalse(journalIter.hasNext()); journalIter.next(); } /** * Tests the {@link Iterator} provided by {@link WritableTxJournal#iterator()} on a deleted journal file. * * @throws IllegalStateException * not part of this test * @throws IOException * not part of this test * @throws NoSuchElementException * expected for this test */ @Test(expected = NoSuchElementException.class) public final void testIteratorNoIterationOnDeleted() throws IllegalStateException, IOException { target.start(); final File journalFile = new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES; i++) { // only write commits to save time target.writeCommit(DtxTestHelper.nextTxId(), PARTICIPANTS); } assertTrue(Files.deleteIfExists(journalFile.toPath())); final Iterator<JournalRecord> journalIter = target.iterator(); // no elements should be available assertFalse(journalIter.hasNext()); journalIter.next(); } /** * Tests writing and reading back journal entries sequentially. * * @throws IOException * if initializing the journal fails, not part of this test */ @Test public final void testSingleWritesAndReads() throws IOException { target.start(); final File journalFile = new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION); assertTrue(journalFile.exists()); for (int i = 0; i < TEST_NB_OF_TEST_ENTRIES; i++) { final long txId = DtxTestHelper.nextTxId(); final TxMessage defTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).setVersion(ProtocolVersion.VERSION_1).setTxId(txId).build(); target.writeStart(defTx, PARTICIPANTS); // writes commits and rollbacks for every other transaction if (i % 2 == 0) { target.writeRollback(txId, -1, PARTICIPANTS); } else { target.writeCommit(txId, PARTICIPANTS); } assertTrue(journalFile.length() > i); for (final Iterator<JournalRecord> iter = target.iterator(); iter.hasNext();) { final JournalRecord currRecord = iter.next(); final TxJournalEntry currEntry = DistTxWrapper.TxJournalEntry.parseFrom(currRecord.getEntry()); assertEquals(PARTICIPANTS.size(), currEntry.getTxNodesList().size()); switch (currEntry.getOp()) { case START: final TxMessage currTransaction = currEntry.getTx(); assertTrue(currTransaction.getTxId() <= txId); break; case COMMIT: assertTrue(currEntry.getTxId() <= txId); break; case ROLLBACK: assertTrue(currEntry.getTxId() <= txId); break; default: // nothing } if (!iter.hasNext()) { assertEquals(currEntry.getTxId(), defTx.getTxId()); } } } target.stop(); } /** * Tests concurrent write accesses to a single journal instance. * * @throws IOException * if initializing the journal fails, not part of this test * @throws InterruptedException * if one of the thread is interrupted, not part of this test */ @Test public final void testConcurrentWrites() throws IOException, InterruptedException { target.start(); assertTrue(new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION).exists()); final ExecutorService forkPool = Executors.newFixedThreadPool(TEST_THREAD_COUNT); final ArrayList<Callable<Void>> runList = new ArrayList<Callable<Void>>(); int writesCounter = 0; for (int i = 0; i < TEST_THREAD_COUNT; i++) { runList.add(new JournalTestWriter(target, TestOp.START, TEST_WRITES_PER_THREAD)); writesCounter++; runList.add(new JournalTestWriter(target, TestOp.COMMIT, TEST_WRITES_PER_THREAD)); writesCounter++; runList.add(new JournalTestWriter(target, TestOp.ROLLBACK, TEST_WRITES_PER_THREAD)); writesCounter++; } forkPool.invokeAll(runList); // reads back all records and counts them int counter = 0; for (final Iterator<JournalRecord> iter = target.iterator(); iter.hasNext(); iter.next()) { counter++; } assertEquals(writesCounter * TEST_WRITES_PER_THREAD, counter); target.stop(); } /** * Tests concurrent read and write accesses to a single journal instance. * * @throws IOException * if initializing the journal fails, not part of this test * @throws InterruptedException * if one of the thread is interrupted, not part of this test */ @Test public final void testConcurrentReadsAndWrites() throws IOException, InterruptedException { target.start(); assertTrue(new File(tmpFileDir.toFile(), DEFAULT_JOURNAL_FILE_PREFIX + JOURNAL_FILE_EXTENSION).exists()); final ExecutorService execPool = Executors.newFixedThreadPool(TEST_THREAD_COUNT); final ArrayList<Callable<Void>> runList = new ArrayList<Callable<Void>>(); int writesCounter = 0; for (int i = 0; i < TEST_THREAD_COUNT; i++) { runList.add(new JournalTestWriter(target, TestOp.START, TEST_WRITES_PER_THREAD)); writesCounter++; runList.add(new JournalTestReader(target, TEST_READS_PER_THREAD)); } execPool.invokeAll(runList); int counter = 0; for (final Iterator<JournalRecord> iter = target.iterator(); iter.hasNext(); iter.next()) { counter++; } assertEquals(writesCounter * TEST_WRITES_PER_THREAD, counter); target.stop(); } }