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.JOURNAL_FILE_EXTENSION;
import static io.eguan.dtx.DtxTestHelper.writeCompleteTransactions;
import static io.eguan.dtx.journal.JournalRotationManager.RotationEvent.RotationStage.PRE_ROTATE;
import static io.eguan.dtx.journal.JournalRotationManager.RotationEvent.RotationStage.ROTATE_FAILURE;
import static io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode.COMMIT;
import static io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode.ROLLBACK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import io.eguan.dtx.DtxTestHelper;
import io.eguan.dtx.journal.JournalFileUtils;
import io.eguan.dtx.journal.JournalRecord;
import io.eguan.dtx.journal.JournalRotationManager;
import io.eguan.dtx.journal.ReadOnlyTxJournal;
import io.eguan.dtx.journal.WritableTxJournal;
import io.eguan.dtx.journal.JournalRotationManager.RotationEvent;
import io.eguan.dtx.journal.JournalRotationManager.RotationListener;
import io.eguan.dtx.journal.TestJournalRotation.RotationSuccessCounter;
import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry;
import io.eguan.proto.dtx.DistTxWrapper.TxJournalEntry.TxOpCode;
import io.eguan.proto.dtx.DistTxWrapper.TxNode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runners.model.InitializationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests for the {@link ReadOnlyTxJournal} class and methods.
*
* @author oodrive
* @author pwehrle
*
*/
public class TestReadOnlyTxJournal {
private static final Logger LOGGER = LoggerFactory.getLogger(TestReadOnlyTxJournal.class);
private static final Path RAMDISK_PATH = FileSystems.getDefault().getPath("/run/shm");
private static final long ROTATION_THRESHOLD_RAMDISK = 2097152L;
private static final long ROTATION_THRESHOLD_HDD = 6144L;
private static final int NB_TEST_ROTATIONS = 5;
private static final int ROTATOR_THREADCOUNT = 3;
private static final int ROTATION_WAIT_DELAY_MS = 1000;
private static final int EXCHANGE_TIMEOUT_S = 5;
private static final int TASK_TIMEOUT_S = 20;
private static final int EXECUTOR_SHUTDOWN_WAIT_S = 10;
private static final int ROTATIONLESS_TX_NUMBER = 20;
private static final Set<TxNode> PARTICIPANTS = DtxTestHelper.newRandomParticipantsSet();
private Path tmpJournalDir;
private long rotationThresholdBytes;
private JournalRotationManager journalRotMgr;
private WritableTxJournal writableJournal;
private Path unreadableFile;
private Path missingFile;
/**
* Sets up common fixture. TODO: refactor to share with other tests.
*
* @throws InitializationError
* if setting up temporary files fails
*/
@Before
public final void setUp() throws InitializationError {
// redirect temporary file directory to ramdisk, if possible
// TODO: this should be factorized as a custom runner or abstract superclass, but those solutions don't work
// with maven and/or coverage tools
try {
if (Files.exists(RAMDISK_PATH)) {
this.tmpJournalDir = Files.createTempDirectory(RAMDISK_PATH, TestJournalRotation.class.getSimpleName());
rotationThresholdBytes = ROTATION_THRESHOLD_RAMDISK;
}
else {
this.tmpJournalDir = Files.createTempDirectory(TestJournalRotation.class.getSimpleName());
rotationThresholdBytes = ROTATION_THRESHOLD_HDD;
}
this.journalRotMgr = new JournalRotationManager(ROTATOR_THREADCOUNT);
journalRotMgr.start();
this.writableJournal = new WritableTxJournal(tmpJournalDir.toFile(),
TestReadOnlyTxJournal.class.getSimpleName(), rotationThresholdBytes, journalRotMgr);
this.writableJournal.start();
this.unreadableFile = Files.createTempFile(tmpJournalDir, DEFAULT_JOURNAL_FILE_PREFIX,
JOURNAL_FILE_EXTENSION,
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("---------")));
this.missingFile = Files.createTempFile(tmpJournalDir, DEFAULT_JOURNAL_FILE_PREFIX, JOURNAL_FILE_EXTENSION);
Files.delete(missingFile);
}
catch (final IOException e) {
throw new InitializationError(e);
}
}
/**
* Tears down common fixture.
*
* @throws InitializationError
* if deleting temporary data fails
*/
@After
public final void tearDown() throws InitializationError {
try {
writableJournal.stop();
journalRotMgr.stop();
Files.setPosixFilePermissions(unreadableFile, PosixFilePermissions.fromString("rwxr-x---"));
io.eguan.utils.Files.deleteRecursive(tmpJournalDir);
}
catch (final IOException e) {
throw new InitializationError(e);
}
}
/**
* Tests failure of the construction due to a missing journal file.
*
* @throws IllegalArgumentException
* if the journal file cannot be found, expected for this test
*/
@Test(expected = IllegalArgumentException.class)
public final void testCreateReadOnlyJournalFailFileNotFound() throws IllegalArgumentException,
IllegalStateException, IOException {
LOGGER.info("Executing");
new ReadOnlyTxJournal(this.missingFile.toFile(), journalRotMgr);
}
/**
* Tests failure of the construction due to an unreadable journal file.
*
* @throws IllegalArgumentException
* if the journal file cannot be read, expected for this test
*/
@Test(expected = IllegalArgumentException.class)
public final void testCreateReadOnlyJournalFailFileNotReadable() throws IllegalArgumentException,
IllegalStateException, IOException {
LOGGER.info("Executing");
new ReadOnlyTxJournal(this.unreadableFile.toFile(), journalRotMgr);
}
/**
* Tests reading a transaction journal, trying to remove each element and verifying all attempts fail.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
*/
@Test
public final void testIteratorRemoveFailure() throws IllegalStateException, IOException {
LOGGER.info("Executing");
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(new File(writableJournal.getJournalFilename()),
journalRotMgr);
long currentTxId = DtxTestHelper.nextTxId() + 1;
final long lastTxId = writeCompleteTransactions(writableJournal, ROTATIONLESS_TX_NUMBER, null, PARTICIPANTS);
int exceptionCounter = 0;
for (final Iterator<JournalRecord> jrnlIter = targetRoJournal.iterator(); jrnlIter.hasNext();) {
final JournalRecord currRecord = jrnlIter.next();
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
assertEquals(currentTxId, currEntry.getTxId());
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
currentTxId++;
}
try {
jrnlIter.remove();
}
catch (final UnsupportedOperationException e) {
exceptionCounter++;
}
}
assertEquals(lastTxId + 1, currentTxId);
assertEquals(ROTATIONLESS_TX_NUMBER * 2, exceptionCounter);
}
/**
* Tests failure to read a transaction journal with invalid data.
*
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws NoSuchElementException
* if trying to iterate over invalid journal data fails, expected for this test
*
*/
@Test(expected = NoSuchElementException.class)
public final void testIteratorFailBadContent() throws IOException, NoSuchElementException {
LOGGER.info("Executing");
final File journalFile = new File(writableJournal.getJournalFilename());
// write bad data
try (FileOutputStream output = new FileOutputStream(journalFile)) {
output.write(DEFAULT_JOURNAL_FILE_PREFIX.getBytes());
}
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(journalFile, journalRotMgr);
targetRoJournal.iterator().next();
}
/**
* Tests failure to read a transaction journal that has been deleted.
*
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws NoSuchElementException
* if trying to iterate over a deleted journal file fails, expected for this test
*
*/
@Test(expected = NoSuchElementException.class)
public final void testIteratorFailFileDeleted() throws IOException, NoSuchElementException {
LOGGER.info("Executing");
final File journalFile = Files.createTempFile(tmpJournalDir, DEFAULT_JOURNAL_FILE_PREFIX,
JOURNAL_FILE_EXTENSION).toFile();
writeCompleteTransactions(writableJournal, ROTATIONLESS_TX_NUMBER, null, PARTICIPANTS);
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(journalFile, journalRotMgr);
final Iterator<JournalRecord> jrnlIter = targetRoJournal.iterator();
assertTrue(journalFile.delete());
jrnlIter.next();
}
/**
* Tests failure to iterate on a transaction journal beyond its limit.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws NoSuchElementException
* if trying to iterate beyond the limit of a journal file fails, expected for this test
*/
@Test(expected = NoSuchElementException.class)
public final void testIteratorFailBeyondLimit() throws IllegalStateException, IOException, NoSuchElementException {
LOGGER.info("Executing");
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(new File(writableJournal.getJournalFilename()),
journalRotMgr);
long currentTxId = DtxTestHelper.nextTxId() + 1;
final long lastTxId = writeCompleteTransactions(writableJournal, ROTATIONLESS_TX_NUMBER, null, PARTICIPANTS);
final Iterator<JournalRecord> jrnlIter = targetRoJournal.iterator();
for (; jrnlIter.hasNext();) {
final JournalRecord currRecord = jrnlIter.next();
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
assertEquals(currentTxId, currEntry.getTxId());
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
currentTxId++;
}
}
assertEquals(lastTxId + 1, currentTxId);
jrnlIter.next();
}
/**
* Tests reading a transaction journal that hasn't been rotated through a {@link ReadOnlyTxJournal}.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
*/
@Test
public final void testReadingWithoutRotation() throws IllegalStateException, IOException {
LOGGER.info("Executing");
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(new File(writableJournal.getJournalFilename()),
journalRotMgr);
long currentTxId = DtxTestHelper.nextTxId() + 1;
final long lastTxId = writeCompleteTransactions(writableJournal, ROTATIONLESS_TX_NUMBER, null, PARTICIPANTS);
for (final JournalRecord currRecord : targetRoJournal) {
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
assertEquals(currentTxId, currEntry.getTxId());
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
currentTxId++;
}
}
assertEquals(lastTxId + 1, currentTxId);
}
/**
* Tests reading a transaction journal while simulating a failed rotation between reading records.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws InterruptedException
* if rotation event handling is interrupted, not part of this test
*/
@Test
public final void testReadingWithSimulatedFailedRotations() throws IllegalStateException, IOException,
InterruptedException {
LOGGER.info("Executing");
final String journalFilename = writableJournal.getJournalFilename();
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(new File(journalFilename), journalRotMgr);
long currentTxId = DtxTestHelper.nextTxId() + 1;
final long lastTxId = writeCompleteTransactions(writableJournal, ROTATIONLESS_TX_NUMBER, null, PARTICIPANTS);
final Iterator<JournalRecord> jrnlIter = targetRoJournal.iterator();
final RotationListener jrnlRotListener = (RotationListener) jrnlIter;
for (; jrnlIter.hasNext();) {
final JournalRecord currRecord = jrnlIter.next();
jrnlRotListener.rotationEventOccured(new RotationEvent(journalFilename, PRE_ROTATE));
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
assertEquals(currentTxId, currEntry.getTxId());
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
currentTxId++;
}
jrnlRotListener.rotationEventOccured(new RotationEvent(journalFilename, ROTATE_FAILURE));
}
assertEquals(lastTxId + 1, currentTxId);
}
/**
* Tests reading a complete journal after it has been rotated once.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws InterruptedException
* if the thread is interrupted while waiting, not part of this test
*/
@Test
public final void testReadingAfterRotation() throws IllegalStateException, IOException, InterruptedException {
LOGGER.info("Executing");
final String journalFilename = writableJournal.getJournalFilename();
final File journalFile = new File(journalFilename);
assertTrue(journalFile.exists());
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(journalFile, journalRotMgr);
long currentTxId = DtxTestHelper.nextTxId() + 1;
assertTrue(journalFile.exists());
final RotationSuccessCounter rotationCounter = new RotationSuccessCounter();
journalRotMgr.addRotationEventListener(rotationCounter, journalFilename);
long lastTxId = currentTxId;
// write just enough transactions to get over the rotation threshold
long lengthBefore = journalFile.length();
while ((journalFile.length() < rotationThresholdBytes) && (lengthBefore <= journalFile.length())) {
// write one complete transaction
lastTxId = writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
while (1 > rotationCounter.getCount()) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (1 > rotationCounter.getCount()) {
// write two complete transactions
lastTxId = writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
}
}
journalRotMgr.removeRotationEventListener(rotationCounter);
for (final JournalRecord currRecord : targetRoJournal) {
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
assertEquals(currentTxId, currEntry.getTxId());
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
currentTxId++;
}
}
assertEquals(lastTxId + 1, currentTxId);
}
/**
* Tests failure to read a journal after it has been rotated once.
*
* @throws IllegalStateException
* if the journal cannot be written to, not part of this test
* @throws IOException
* if writing or parsing the contents of the journal fail, not part of this test
* @throws InterruptedException
* if the thread is interrupted while waiting, not part of this test
* @throws NoSuchElementException
* if trying to iterate after backup files have been deleted fails, expected for this test
*/
@Test(expected = NoSuchElementException.class)
public final void testReadingAfterRotationFailureMissingFiles() throws IllegalStateException, IOException,
InterruptedException, NoSuchElementException {
LOGGER.info("Executing");
final String journalFilename = writableJournal.getJournalFilename();
final File journalFile = new File(journalFilename);
assertTrue(journalFile.exists());
final ReadOnlyTxJournal targetRoJournal = new ReadOnlyTxJournal(journalFile, journalRotMgr);
assertTrue(journalFile.exists());
final RotationSuccessCounter rotationCounter = new RotationSuccessCounter();
journalRotMgr.addRotationEventListener(rotationCounter, journalFilename);
// write just enough transactions to get over the rotation threshold
long lengthBefore = journalFile.length();
while ((journalFile.length() < rotationThresholdBytes) && (lengthBefore <= journalFile.length())) {
// write one complete transaction
writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
while (1 > rotationCounter.getCount()) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (1 > rotationCounter.getCount()) {
// write two complete transactions
writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
}
}
journalRotMgr.removeRotationEventListener(rotationCounter);
// construct the iterator
final Iterator<JournalRecord> jrnlIter = targetRoJournal.iterator();
final NavigableMap<Integer, File> backupMap = JournalFileUtils.getInverseBackupMap(tmpJournalDir.toFile(),
FileSystems.getDefault().getPath(writableJournal.getJournalFilename()).getFileName().toString());
for (final File currBackupFile : backupMap.values()) {
assertTrue(currBackupFile.delete());
}
// should throw an exception as the underlying journal has been compromised
jrnlIter.next();
}
/**
* Tests reading a journal while it is being written to and consequently rotated.
*
* This tests reading by a reader thread that initially waits for a single writer to complete writing and rotating
* up to a target, but continues reading during the next of the writer's iterations.
*
* @throws InterruptedException
* if any of the threads are interrupted, not part of this test
* @throws ExecutionException
* if any exception is thrown by either reader or writer threads, not part of this test
* @throws TimeoutException
* if either reader or writer thread times out, not part of this test
*/
@Test
public final void testReadingWhileRotating() throws InterruptedException, ExecutionException, TimeoutException {
LOGGER.info("Executing");
final ExecutorService executor = Executors.newFixedThreadPool(2);
final String journalFilename = writableJournal.getJournalFilename();
final File journalFile = new File(journalFilename);
final ReadOnlyTxJournal targetRoJournal = writableJournal.newReadOnlyTxJournal();
final long currentTxId = DtxTestHelper.nextTxId() + 1;
assertTrue(journalFile.exists());
final RotationSuccessCounter rotationCounter = new RotationSuccessCounter();
journalRotMgr.addRotationEventListener(rotationCounter, journalFilename);
final Exchanger<Long> lastTxIdExch = new java.util.concurrent.Exchanger<Long>();
final Callable<Long> writer = new Callable<Long>() {
@Override
public Long call() throws Exception {
long result = currentTxId;
Long lastReadId = Long.valueOf(result);
for (int i = 1; i <= NB_TEST_ROTATIONS; i++) {
// write just enough transactions to get over the rotation threshold
long lengthBefore = journalFile.length();
while ((journalFile.length() < rotationThresholdBytes) && (lengthBefore <= journalFile.length())) {
// write two complete transactions
result = writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
while (i > rotationCounter.getCount()) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (i > rotationCounter.getCount()) {
// write two complete transactions
result = writeCompleteTransactions(writableJournal, 2, null, PARTICIPANTS);
}
}
final Long targetId = Long.valueOf(result);
LOGGER.debug("Sending new target ID: " + targetId);
try {
final Long lastFromReader = lastTxIdExch.exchange(targetId, EXCHANGE_TIMEOUT_S,
TimeUnit.SECONDS);
assertTrue("Reader has read beyond last transmitted value; ID read before last rotation="
+ lastReadId + ", last read ID=" + lastFromReader,
(lastFromReader.compareTo(lastReadId) >= 0));
lastReadId = lastFromReader;
LOGGER.debug("Writer received last read txID: " + lastReadId);
}
catch (final TimeoutException te) {
LOGGER.warn("Timed out waiting for reader thread");
break;
}
}
return Long.valueOf(result);
}
};
final Callable<Long> reader = new Callable<Long>() {
@Override
public Long call() throws Exception {
long result = currentTxId;
Long targetTxId = Long.valueOf(result);
for (int i = 1; i <= NB_TEST_ROTATIONS; i++) {
LOGGER.debug("Reader waiting for new target txID");
targetTxId = lastTxIdExch.exchange(targetTxId, EXCHANGE_TIMEOUT_S, TimeUnit.SECONDS);
LOGGER.debug("Reader received target txID: " + targetTxId);
for (final JournalRecord currRecord : targetRoJournal) {
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
final long readTxId = currEntry.getTxId();
if (readTxId < result) {
continue;
}
assertTrue(result <= readTxId);
final TxOpCode currOp = currEntry.getOp();
if (COMMIT.equals(currOp) || ROLLBACK.equals(currOp)) {
result = currEntry.getTxId();
}
}
assertTrue("Read transaction value is greater or equal than target; target=" + targetTxId
+ ", read=" + result, targetTxId.compareTo(Long.valueOf(result)) <= 0);
}
return Long.valueOf(result);
}
};
final Future<Long> readerFut = executor.submit(reader);
final Future<Long> writerFut = executor.submit(writer);
final Long lastReadTxId = readerFut.get(EXCHANGE_TIMEOUT_S * NB_TEST_ROTATIONS, TimeUnit.SECONDS);
LOGGER.debug("Reader returned; lastReadTxId=" + lastReadTxId);
final Long lastWrittenTxId = writerFut.get(EXCHANGE_TIMEOUT_S * NB_TEST_ROTATIONS, TimeUnit.SECONDS);
LOGGER.debug("Writer returned; lastWrittenTxId=" + lastWrittenTxId);
journalRotMgr.removeRotationEventListener(rotationCounter);
assertEquals(lastReadTxId, lastWrittenTxId);
executor.shutdown();
executor.awaitTermination(EXECUTOR_SHUTDOWN_WAIT_S, TimeUnit.SECONDS);
}
/**
* Tests continuous reading of journals by separate threads while they are being written to and rotated.
*
* @throws IllegalStateException
* if a journal start fails, not part of this test
* @throws IOException
* if disk I/O fails, not part of this test
* @throws InterruptedException
* if executor shutdown fails, not part of this test
* @throws ExecutionException
* if a reader thread fails, not part of this test
* @throws TimeoutException
* if a reader thread times out, not part of this test
*/
@Test
public final void testMultipleJournalWritesAndReads() throws IllegalStateException, IOException,
InterruptedException, ExecutionException, TimeoutException {
LOGGER.info("Executing");
final ExecutorService executor = Executors.newFixedThreadPool(ROTATOR_THREADCOUNT);
final ArrayList<WritableTxJournal> journalList = new ArrayList<WritableTxJournal>(ROTATOR_THREADCOUNT);
final HashMap<String, Future<Long>> readTaskList = new HashMap<String, Future<Long>>(ROTATOR_THREADCOUNT);
final CountDownLatch writeDoneLatch = new CountDownLatch(1);
final HashMap<String, AtomicInteger> rotationCounters = new HashMap<String, AtomicInteger>();
for (int i = 0; i < ROTATOR_THREADCOUNT; i++) {
final WritableTxJournal newJournal;
if (i == 0) {
newJournal = writableJournal;
}
else {
newJournal = new WritableTxJournal(tmpJournalDir.toFile(), TestReadOnlyTxJournal.class.getSimpleName()
+ "-" + i, rotationThresholdBytes, journalRotMgr);
newJournal.start();
}
journalList.add(newJournal);
rotationCounters.put(newJournal.getJournalFilename(), new AtomicInteger());
readTaskList.put(newJournal.getJournalFilename(), executor.submit(new Callable<Long>() {
@Override
public Long call() throws Exception {
long result = 0;
while (writeDoneLatch.getCount() > 0) {
for (final JournalRecord currRecord : newJournal.newReadOnlyTxJournal()) {
final TxJournalEntry currEntry = TxJournalEntry.parseFrom(currRecord.getEntry());
result = currEntry.getTxId();
}
}
return Long.valueOf(result);
}
}));
}
journalRotMgr.addRotationEventListener(new RotationListener() {
@Override
public void rotationEventOccured(final RotationEvent rotevt) throws InterruptedException {
if (RotationEvent.RotationStage.ROTATE_SUCCESS.equals(rotevt.getStage())) {
rotationCounters.get(rotevt.getFilename()).incrementAndGet();
}
}
}, rotationCounters.keySet().toArray(new String[ROTATOR_THREADCOUNT]));
for (int i = 0; i < NB_TEST_ROTATIONS; i++) {
for (final WritableTxJournal currJournal : journalList) {
final String journalFilename = currJournal.getJournalFilename();
final File journalFile = new File(journalFilename);
// write just enough transactions to get over the rotation threshold
long lengthBefore = journalFile.length();
while ((journalFile.length() < rotationThresholdBytes) && (lengthBefore <= journalFile.length())) {
// write two complete transactions
writeCompleteTransactions(currJournal, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
final AtomicInteger rotationCounter = rotationCounters.get(journalFilename);
while (i > rotationCounter.get()) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (i > rotationCounter.get()) {
// write two complete transactions
writeCompleteTransactions(currJournal, 2, null, PARTICIPANTS);
}
}
}
}
writeDoneLatch.countDown();
for (final WritableTxJournal currJournal : journalList) {
assertEquals(Long.valueOf(currJournal.getLastFinishedTxId()),
readTaskList.get(currJournal.getJournalFilename()).get(TASK_TIMEOUT_S, TimeUnit.SECONDS));
currJournal.stop();
}
executor.shutdown();
executor.awaitTermination(EXECUTOR_SHUTDOWN_WAIT_S, TimeUnit.SECONDS);
}
}