/* * Bitronix Transaction Manager * * Copyright (c) 2011, Juergen Kellerer. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package bitronix.tm.journal.nio; import bitronix.tm.TransactionManagerServices; import bitronix.tm.journal.Journal; import bitronix.tm.journal.JournalRecord; import bitronix.tm.journal.nio.util.SequencedBlockingQueue; import bitronix.tm.utils.Uid; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import javax.transaction.Status; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.reflect.Method; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; import static bitronix.tm.journal.nio.NioJournalFile.FIXED_HEADER_SIZE; import static bitronix.tm.utils.UidGenerator.generateUid; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; /** * Nio journal specific functional tests. * * @author juergen kellerer, 2011-04-30 */ public class NioJournalFunctionalTest extends AbstractJournalFunctionalTest { @Override protected JournalRecord getLogRecord(int status, int recordLength, int headerLength, long time, int sequenceNumber, int crc32, Uid gtrid, Set uniqueNames, int endRecord) { return new NioJournalRecord(status, recordLength, time, sequenceNumber, false, gtrid, new HashSet<String>(uniqueNames), true); } @Override protected Journal getJournal() { return new NioJournal(); } @Before public void setUp() throws Exception { File file = NioJournal.getJournalFilePath(); assertTrue(!file.isFile() || file.delete()); } @Test public void testCannotOpenTheSameFileTwice() throws Exception { // TODO: We need a different approach to lock files (e.g. a class with a main method that can be forked) // TODO: The test code below works only on windows. if (!System.getProperty("os.name", "unknown").toLowerCase().contains("windows")) return; final File file = NioJournal.getJournalFilePath(); journal.open(); journal.open(); // is allowed as 2nd call to open(), re-opens the journal. // test accessing a opened journal in write mode. try { RandomAccessFile rw = new RandomAccessFile(file, "rw"); try { rw.getChannel().lock(); } finally { rw.close(); } fail("write should fail on opened journal."); } catch (OverlappingFileLockException expected) { } journal.close(); // test open fails if another process has the journal locked. RandomAccessFile rw = new RandomAccessFile(file, "rw"); try { FileLock lock = rw.getChannel().lock(); try { journal.open(); fail("open should fail on locked file."); } catch (OverlappingFileLockException expected) { } finally { lock.release(); } } finally { rw.close(); } } @Test public void testExceptions() throws Exception { try { journal.force(); fail("expected IOException"); } catch (IOException ex) { assertEquals("The journal is not yet opened or was already closed.", ex.getMessage()); } try { journal.log(0, null, null); fail("expected IOException"); } catch (IOException ex) { assertEquals("The journal is not yet opened or was already closed.", ex.getMessage()); } try { journal.collectDanglingRecords(); fail("expected IOException"); } catch (IOException ex) { assertEquals("The journal is not yet opened or was already closed.", ex.getMessage()); } } @Test public void testFSYNCCanBeSetFromCentralConfiguration() throws Exception { TransactionManagerServices.getConfiguration().setForcedWriteEnabled(true); assertFalse(new NioJournal().isSkipForce()); TransactionManagerServices.getConfiguration().setForcedWriteEnabled(false); assertTrue(new NioJournal().isSkipForce()); } @Test public void testJournalGrowsIfOpenTransactionsExceedCapacity() throws Exception { HashSet<String> uniqueNames = new HashSet<String>(Arrays.asList("1")); int rawRecordSize = calculateRawRecordSize(generateUid(), uniqueNames); journal.open(); File journalFilePath = NioJournal.getJournalFilePath(); long fileSize = journalFilePath.length(); int iterations = (int) ((fileSize - FIXED_HEADER_SIZE) / rawRecordSize) + 1; HashSet<Uid> ids = new HashSet<Uid>(iterations); for (int i = 0; i < iterations; i++) { Uid id = generateUid(); journal.log(Status.STATUS_COMMITTING, id, uniqueNames); ids.add(id); } journal.close(); assertTrue("journal was not grown after opening " + iterations + " transactions.", fileSize < journalFilePath.length()); journal.open(); assertEquals("not all transactions were stored.", ids, journal.collectDanglingRecords().keySet()); } @Test public void testForceFailsIfOpenTransactionsExceedCapacityAndGrowIsDisabled() throws Exception { final File file = NioJournal.getJournalFilePath(); NioJournalWritingThread thread = null; final int journalSize = 64 * 1024; final NioJournalFile journalFile = new NioJournalFile(file, journalSize); try { NioJournalFile mockFile = mock(NioJournalFile.class, new Answer<Object>() { final Method growMethod = NioJournalFile.class.getMethod("growJournal", long.class); public Object answer(InvocationOnMock invocation) throws Throwable { if (growMethod.equals(invocation.getMethod())) throw new IOException("disk is full."); return invocation.getMethod().invoke(journalFile, invocation.getArguments()); } }); SequencedBlockingQueue<NioJournalFileRecord> recordsQueue = new SequencedBlockingQueue<NioJournalFileRecord>(); NioForceSynchronizer synchronizer = new NioForceSynchronizer(recordsQueue); NioTrackedTransactions trackedTransactions = new NioTrackedTransactions(); thread = NioJournalWritingThread.newRunningInstance(trackedTransactions, mockFile, synchronizer, recordsQueue); HashSet<String> uniqueNames = new HashSet<String>(Arrays.asList("1")); int recordsThatFitIn = (int) Math.floor((float) (journalSize - FIXED_HEADER_SIZE) / (float) calculateRawRecordSize(generateUid(), uniqueNames)); int checks = 0; for (int i = 0; i <= recordsThatFitIn; i++) { NioJournalFileRecord fileRecord = mockFile.createEmptyRecord(); NioJournalRecord record = new NioJournalRecord(Status.STATUS_COMMITTING, generateUid(), uniqueNames); trackedTransactions.track(record); record.encodeTo(fileRecord.createEmptyPayload(record.getRecordLength()), false); recordsQueue.putElement(fileRecord); if (i == recordsThatFitIn - 1) { assertTrue("not all expected elements were written.", synchronizer.waitOnEnlisted()); checks++; } else if (i == recordsThatFitIn) { assertFalse("the non-storable record was not reported as failure.", synchronizer.waitOnEnlisted()); checks++; } } assertEquals("not all checks executed.", 2, checks); } finally { try { if (thread != null) { thread.shutdown(); } } finally { journalFile.close(); } } } @Test public void testJournalIsPositionedCorrectlyAfterOpen() throws Exception { final Uid gtrid = generateUid(); final Set<String> uniqueNames = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList("1"))); final float rawRecordSize = calculateRawRecordSize(gtrid, uniqueNames), readBufferSize = NioJournalFileIterable.INITIAL_READ_BUFFER_SIZE; int[] recordCounts = { 1, 2, (int) Math.floor(readBufferSize / rawRecordSize) / 2, (int) Math.floor(readBufferSize / rawRecordSize) / 2 * 25, (int) Math.floor(readBufferSize / rawRecordSize), (int) Math.ceil(readBufferSize / rawRecordSize), (int) Math.floor(readBufferSize * 3 / rawRecordSize), (int) Math.ceil(readBufferSize * 3 / rawRecordSize), (int) Math.floor(readBufferSize * 25 / rawRecordSize), (int) Math.ceil(readBufferSize * 25 / rawRecordSize)}; for (int records : recordCounts) { setUp(); doTestPositionIsCorrect(records, gtrid, uniqueNames); shutdownJournal(); } } private void doTestPositionIsCorrect(int iterations, Uid gtrid, Set<String> uniqueNames) throws IOException { int rawRecordSize = calculateRawRecordSize(gtrid, uniqueNames); journal.open(); assertEquals(FIXED_HEADER_SIZE, ((NioJournal) journal).journalFile.getPosition()); for (int i = 0; i < iterations; i++) journal.log(Status.STATUS_ACTIVE, gtrid, uniqueNames); journal.close(); assertFalse(((NioJournal) journal).isOpen()); journal.open(); assertEquals("Iterations:" + iterations, FIXED_HEADER_SIZE + (iterations * rawRecordSize), ((NioJournal) journal).journalFile.getPosition()); } private int calculateRawRecordSize(Uid gtrid, Set<String> uniqueNames) { return NioJournalFileRecord.RECORD_HEADER_SIZE + NioJournalFileRecord.RECORD_TRAILER_SIZE + new NioJournalRecord(Status.STATUS_ACTIVE, gtrid, uniqueNames).getRecordLength(); } }