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.DtxTestHelper.writeCompleteTransactions;
import static io.eguan.dtx.journal.JournalRotationManager.RotationEvent.RotationStage.ROTATE_SUCCESS;
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.journal.JournalFileUtils;
import io.eguan.dtx.journal.JournalRotationManager;
import io.eguan.dtx.journal.WritableTxJournal;
import io.eguan.dtx.journal.JournalRotationManager.RotationEvent;
import io.eguan.dtx.journal.JournalRotationManager.RotationListener;
import io.eguan.proto.dtx.DistTxWrapper.TxNode;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.NavigableMap;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
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 the rotation methods of the {@link WritableTxJournal} class.
*
* Consider running these tests using the {@link TmpToRamJUnitRunner}, as they last very long due to large amounts of
* transactions being written synchronously to disk.
*
* @author oodrive
* @author pwehrle
*
*/
public final class TestJournalRotation {
/**
* A {@link RotationListener} counting events with {@link RotationEvent.RotationStage#ROTATE_SUCCESS}.
*
*
*/
static final class RotationSuccessCounter implements RotationListener {
private final AtomicInteger rotationCounter = new AtomicInteger();
@Override
public void rotationEventOccured(final RotationEvent rotevt) {
if (ROTATE_SUCCESS.equals(rotevt.getStage())) {
rotationCounter.incrementAndGet();
}
}
/**
* Gets the current successful rotation event count.
*
* @return a positive or zero integer
*/
final int getCount() {
return rotationCounter.get();
}
}
private static final Logger LOGGER = LoggerFactory.getLogger(TestJournalRotation.class);
private static final int NB_TEST_ROTATIONS = 5;
private static final int NB_CONCURRENT_JOURNALS = 5;
private static final int ROTATION_WAIT_DELAY_RETRIES = 10;
private static final int ROTATION_WAIT_DELAY_MS = 1000;
private static final int ROTATOR_THREADCOUNT = 3;
private static final Set<TxNode> PARTICIPANTS = DtxTestHelper.newRandomParticipantsSet();
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 boolean tmpToRamActive = false;
private long rotationThresholdBytes = 0;
private JournalRotationManager journalRotMgr;
private File tmpJournalDir;
/**
* Sets up common fixture.
*
* @throws InitializationError
* if setting up temporary storage 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
tmpToRamActive = Files.exists(RAMDISK_PATH);
try {
if (tmpToRamActive) {
this.tmpJournalDir = Files.createTempDirectory(RAMDISK_PATH, TestJournalRotation.class.getSimpleName())
.toFile();
rotationThresholdBytes = ROTATION_THRESHOLD_RAMDISK;
}
else {
this.tmpJournalDir = Files.createTempDirectory(TestJournalRotation.class.getSimpleName()).toFile();
rotationThresholdBytes = ROTATION_THRESHOLD_HDD;
}
}
catch (final IOException e) {
throw new InitializationError(e);
}
this.journalRotMgr = new JournalRotationManager(ROTATOR_THREADCOUNT);
journalRotMgr.start();
}
/**
* Tears down common fixture.
*
* @throws InitializationError
* if deleting temporary data fails
*/
@After
public final void tearDown() throws InitializationError {
journalRotMgr.stop();
try {
io.eguan.utils.Files.deleteRecursive(tmpJournalDir.toPath());
}
catch (final IOException e) {
throw new InitializationError(e);
}
}
/**
* Tests {@link JournalRotationManager#submitRotation(WritableTxJournal)}'s failure on a stopped instance.
*/
@Test(expected = IllegalStateException.class)
public final void testRotationFailStoppedMgr() {
LOGGER.info("Executing");
journalRotMgr.stop();
assertFalse(journalRotMgr.isStarted());
journalRotMgr.submitRotation(new WritableTxJournal(tmpJournalDir, null, rotationThresholdBytes, journalRotMgr));
}
/**
* Tests a single, automatically triggered rotation.
*
* @throws IllegalStateException
* if the journal is not started, not part of this test
* @throws IOException
* if writing to the journal fails, not part of this test
* @throws InterruptedException
* if the test is interrupted while waiting, not part of this test
*/
@Test
public final void testSingleImplicitRotation() throws IllegalStateException, IOException, InterruptedException {
LOGGER.info("Executing");
final WritableTxJournal targetJrnl = new WritableTxJournal(tmpJournalDir, null, rotationThresholdBytes,
journalRotMgr);
assertNotNull(targetJrnl);
targetJrnl.start();
assertTrue(targetJrnl.isStarted());
final String journalFilename = targetJrnl.getJournalFilename();
final RotationSuccessCounter rotationCounter = new RotationSuccessCounter();
final File journalFile = new File(journalFilename);
assertTrue(journalFile.exists());
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(targetJrnl, 1, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
int retryCounter = 0;
while (1 > rotationCounter.getCount() && retryCounter < ROTATION_WAIT_DELAY_RETRIES) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (1 > rotationCounter.getCount()) {
// write two complete transactions
writeCompleteTransactions(targetJrnl, 2, null, PARTICIPANTS);
}
retryCounter++;
}
final NavigableMap<Integer, File> backupFileList = JournalFileUtils.getInverseBackupMap(tmpJournalDir,
journalFile.getName());
assertFalse(backupFileList.isEmpty());
assertTrue(backupFileList.firstKey().intValue() >= 1);
assertTrue(1 <= rotationCounter.getCount());
journalRotMgr.removeRotationEventListener(rotationCounter);
}
/**
* Tests multiple ({@value #NB_TEST_ROTATIONS}), automatically triggered rotations.
*
* @throws IllegalStateException
* if the journal is not started, not part of this test
* @throws IOException
* if writing to the journal fails, not part of this test
* @throws InterruptedException
* if the test is interrupted while waiting, not part of this test
*/
@Test
public final void testMultipleImplicitRotations() throws IllegalStateException, IOException, InterruptedException {
LOGGER.info("Executing");
final WritableTxJournal targetJrnl = new WritableTxJournal(tmpJournalDir, DEFAULT_JOURNAL_FILE_PREFIX,
rotationThresholdBytes, journalRotMgr);
assertNotNull(targetJrnl);
targetJrnl.start();
assertTrue(targetJrnl.isStarted());
final RotationSuccessCounter rotationCounter = new RotationSuccessCounter();
final String journalFilename = targetJrnl.getJournalFilename();
journalRotMgr.addRotationEventListener(rotationCounter, journalFilename);
final File journalFile = new File(journalFilename);
assertTrue(journalFile.exists());
writeCompleteTransactions(targetJrnl, 2, null, PARTICIPANTS);
for (int j = 1; j <= NB_TEST_ROTATIONS; j++) {
// 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(targetJrnl, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
int retryCounter = 0;
while (j > rotationCounter.getCount() && retryCounter < ROTATION_WAIT_DELAY_RETRIES) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (j > rotationCounter.getCount()) {
// write two complete transactions
writeCompleteTransactions(targetJrnl, 2, null, PARTICIPANTS);
}
retryCounter++;
}
final NavigableMap<Integer, File> backupFileList = JournalFileUtils.getInverseBackupMap(tmpJournalDir,
journalFile.getName());
assertFalse(backupFileList.isEmpty());
LOGGER.debug("Checking for rotation count; highestBackup=" + backupFileList.firstKey().intValue()
+ ", expected=" + j);
assertTrue(backupFileList.firstKey().intValue() >= j);
assertTrue(j <= rotationCounter.getCount());
// verifying the non-existence of the i+1th backup file does not provide reliable results and is therefore
// omitted pending a better solution
}
journalRotMgr.removeRotationEventListener(rotationCounter);
}
/**
* Tests rotation operations performed by one {@link JournalRotationManager} with {@value #NB_CONCURRENT_JOURNALS}
* threads simultaneously writing into their own journal.
*
* @throws IllegalStateException
* if the journal is not started, not part of this test
* @throws IOException
* if writing to any of the journals fails, not part of this test
* @throws InterruptedException
* if the test is interrupted while waiting, not part of this test
*/
@Test
public final void testMultipleConcurrentImplicitRotations() throws IllegalStateException, IOException,
InterruptedException {
LOGGER.info("Executing");
final ExecutorService executor = Executors.newFixedThreadPool(NB_CONCURRENT_JOURNALS, new ThreadFactory() {
private int serial;
@Override
public Thread newThread(final Runnable r) {
serial++;
final Thread result = new Thread(r);
result.setDaemon(true);
result.setName(TestJournalRotation.class.getSimpleName() + " - " + serial);
return result;
}
});
final ArrayList<WritableTxJournal> journalList = new ArrayList<WritableTxJournal>();
final HashMap<String, AtomicInteger> rotationCounters = new HashMap<String, AtomicInteger>();
final ArrayList<Callable<Void>> callableList = new ArrayList<Callable<Void>>();
for (int i = 0; i < NB_CONCURRENT_JOURNALS; i++) {
final WritableTxJournal targetJournal = new WritableTxJournal(tmpJournalDir, DEFAULT_JOURNAL_FILE_PREFIX
+ i, rotationThresholdBytes, journalRotMgr);
journalList.add(targetJournal);
final String journalFilename = targetJournal.getJournalFilename();
rotationCounters.put(journalFilename, new AtomicInteger());
targetJournal.start();
final JournalTestWriter journalWriter = new JournalTestWriter(targetJournal, rotationThresholdBytes,
rotationCounters.get(journalFilename), journalRotMgr);
callableList.add(journalWriter);
}
final RotationListener rotListener = new RotationListener() {
@Override
public void rotationEventOccured(final RotationEvent rotevt) {
if (RotationEvent.RotationStage.ROTATE_SUCCESS.equals(rotevt.getStage())) {
rotationCounters.get(rotevt.getFilename()).incrementAndGet();
}
}
};
journalRotMgr.addRotationEventListener(rotListener,
rotationCounters.keySet().toArray(new String[NB_CONCURRENT_JOURNALS]));
executor.invokeAll(callableList);
// shutdown to try and complete running rotations
journalRotMgr.stop();
journalRotMgr.removeRotationEventListener(rotListener);
for (final String currFile : rotationCounters.keySet()) {
final AtomicInteger currCounter = rotationCounters.get(currFile);
assertTrue(NB_TEST_ROTATIONS <= currCounter.get());
}
executor.shutdown();
}
/**
* Autonomous task writing to a given {@link WritableTxJournal}.
*
*
*/
private static final class JournalTestWriter implements Callable<Void> {
private final WritableTxJournal targetJournal;
private final long rotationThreshold;
private final AtomicInteger rotationCounter;
JournalTestWriter(final WritableTxJournal target, final long rotationThreshold,
final AtomicInteger rotationCounter, final JournalRotationManager journalRotMgr) {
this.targetJournal = target;
this.rotationThreshold = rotationThreshold;
this.rotationCounter = rotationCounter;
}
@Override
public Void call() throws Exception {
for (int j = 1; j <= NB_TEST_ROTATIONS; j++) {
final File journalFile = new File(targetJournal.getJournalFilename());
// write just enough transactions to get over the rotation threshold
long lengthBefore = journalFile.length();
while ((journalFile.length() < rotationThreshold) && (lengthBefore <= journalFile.length())) {
// write two complete transactions
writeCompleteTransactions(targetJournal, 2, null, PARTICIPANTS);
lengthBefore = journalFile.length();
}
// waits until the rotation counter increases
int retryCounter = 0;
while (j > rotationCounter.get() && retryCounter < ROTATION_WAIT_DELAY_RETRIES) {
Thread.sleep(ROTATION_WAIT_DELAY_MS);
if (j > rotationCounter.get()) {
// write two complete transactions to trigger missing rotations
writeCompleteTransactions(targetJournal, 2, null, PARTICIPANTS);
}
retryCounter++;
}
}
return null;
}
}
}