/** * Copyright 2013 Benjamin Lerer * * 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. */ package io.horizondb.db.commitlog; import io.horizondb.db.Configuration; import io.horizondb.db.StorageEngine; import io.horizondb.io.Buffer; import io.horizondb.io.buffers.Buffers; import io.horizondb.io.files.FileUtils; import io.horizondb.test.AssertFiles; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.Path; import org.easymock.EasyMock; import org.junit.After; import org.junit.Before; import org.junit.Test; import static io.horizondb.db.commitlog.CommitLogSegment.LOG_OVERHEAD_SIZE; import static io.horizondb.io.files.FileUtils.ONE_KB; import static io.horizondb.test.AssertFiles.assertFileContainsAt; import static io.horizondb.test.AssertFiles.assertFileDoesNotExists; import static io.horizondb.test.AssertFiles.assertFileExists; import static io.horizondb.test.AssertFiles.assertFileSize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class CommitLogSegmentTest { /** * The test directory. */ private Path testDirectory; /** * The configuration used during the tests. */ private Configuration configuration; @Before public void setUp() throws IOException { this.testDirectory = Files.createTempDirectory("test"); this.configuration = Configuration.newBuilder() .commitLogDirectory(this.testDirectory) .commitLogSegmentSize(8 * ONE_KB) .build(); } @After public void tearDown() throws IOException { FileUtils.forceDelete(this.testDirectory); this.testDirectory = null; this.configuration = null; } @Test public void testFreshSegment() throws Exception { long expectedId = IdFactory.nextId() + 1; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { segment.flush(); Path expectedFile = this.testDirectory.resolve("CommitLog-" + expectedId + ".log"); assertFileExists(expectedFile); assertFileSize(8 * ONE_KB, expectedFile); } } @Test public void testRecycle() throws Exception { long expectedId = IdFactory.nextId() + 1; CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration); segment.flush(); Path expectedFile = this.testDirectory.resolve("CommitLog-" + expectedId + ".log"); assertFileExists(expectedFile); assertFileSize(8 * ONE_KB, expectedFile); try (CommitLogSegment recycledSegment = CommitLogSegment.recycleSegment(segment)) { recycledSegment.flush(); assertFileDoesNotExists(expectedFile); expectedFile = this.testDirectory.resolve("CommitLog-" + (expectedId + 1) + ".log"); assertFileExists(expectedFile); assertFileSize(8 * ONE_KB, expectedFile); } } @Test public void testIsCommitLogSegment() { Path path = this.testDirectory.resolve("CommitLog-1456.log"); assertTrue(CommitLogSegment.isCommitLogSegment(path)); path = this.testDirectory.resolve("commitlog-1456.log"); assertFalse(CommitLogSegment.isCommitLogSegment(path)); path = this.testDirectory.resolve("commitlog-.log"); assertFalse(CommitLogSegment.isCommitLogSegment(path)); } @Test public void testWriteWithEmptyBuffer() throws Exception { long expectedId = IdFactory.nextId() + 1; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { segment.write(Buffers.EMPTY_BUFFER); Path expectedFile = this.testDirectory.resolve("CommitLog-" + expectedId + ".log"); AssertFiles.assertFileContainsAt(0, new byte[] { 0, 0, 0, 0 }, expectedFile); } } @Test public void testWrite() throws Exception { long expectedId = IdFactory.nextId() + 1; int position = 0; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { byte[] byteArray = new byte[] { 1, 123, 12, 37 }; Buffer buffer = Buffers.wrap(byteArray); position += (buffer.readableBytes() + LOG_OVERHEAD_SIZE); assertEquals(new ReplayPosition(expectedId, position), segment.write(buffer)); segment.flush(); Path expectedFile = this.testDirectory.resolve("CommitLog-" + expectedId + ".log"); assertFileContainsAt(0, new byte[] { 4, 0, 0, 0 }, expectedFile); assertFileContainsAt(12, byteArray, expectedFile); assertFileContainsAt(24, new byte[] { 0, 0, 0, 0 }, expectedFile); byteArray = new byte[] { 6, 21, 17, 9 }; buffer = Buffers.wrap(byteArray); position += (buffer.readableBytes() + LOG_OVERHEAD_SIZE); assertEquals(new ReplayPosition(expectedId, position), segment.write(buffer)); segment.flush(); assertFileContainsAt(24, new byte[] { 4, 0, 0, 0 }, expectedFile); assertFileContainsAt(36, byteArray, expectedFile); assertFileContainsAt(48, new byte[] { 0, 0, 0, 0 }, expectedFile); } } @Test public void testReplayWithNoData() throws Exception { StorageEngine databaseEngine = EasyMock.createMock(StorageEngine.class); EasyMock.replay(databaseEngine); Path path; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { path = segment.getPath(); } try (CommitLogSegment segment = CommitLogSegment.loadFromFile(this.configuration, path)) { segment.replay(databaseEngine); } EasyMock.verify(databaseEngine); } @Test public void testReplay() throws Exception { long segmentId = IdFactory.nextId() + 1; int position = 0; Buffer firstBuffer = Buffers.wrap(new byte[] { 1, 123, 12, 37 }); position += (firstBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition firstPosition = new ReplayPosition(segmentId, position); Buffer secondBuffer = Buffers.wrap(new byte[] { -121, 5, 0, 30, 14, 56 }); position += (secondBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition secondPosition = new ReplayPosition(segmentId, position); Buffer thirdBuffer = Buffers.wrap(new byte[] { 4, 85, 0 }); position += (thirdBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition thirdPosition = new ReplayPosition(segmentId, position); StorageEngine databaseEngine = EasyMock.createMock(StorageEngine.class); databaseEngine.replay(firstPosition, firstBuffer.duplicate()); databaseEngine.replay(secondPosition, secondBuffer.duplicate()); databaseEngine.replay(thirdPosition, thirdBuffer.duplicate()); EasyMock.replay(databaseEngine); Path path; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { path = segment.getPath(); assertEquals(firstPosition, segment.write(firstBuffer)); assertEquals(secondPosition, segment.write(secondBuffer)); assertEquals(thirdPosition, segment.write(thirdBuffer)); segment.flush(); } try (CommitLogSegment segment = CommitLogSegment.loadFromFile(this.configuration, path)) { segment.replay(databaseEngine); } EasyMock.verify(databaseEngine); } /** * Test the replay when the file has been truncated. * * @throws Exception if a problem occurs. */ @Test public void testReplayWithTruncatedFile() throws Exception { long segmentId = IdFactory.nextId() + 1; int position = 0; Buffer firstBuffer = Buffers.wrap(new byte[] { 1, 123, 12, 37 }); position += (firstBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition firstPosition = new ReplayPosition(segmentId, position); Buffer secondBuffer = Buffers.wrap(new byte[] { -121, 5, 0, 30, 14, 56 }); position += (secondBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition secondPosition = new ReplayPosition(segmentId, position); Buffer thirdBuffer = Buffers.wrap(new byte[] { 4, 85, 0 }); position += (thirdBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition thirdPosition = new ReplayPosition(segmentId, position); long tuncationPoint = position - (CommitLogSegment.CHECKSUM_SIZE + 2); StorageEngine databaseEngine = EasyMock.createMock(StorageEngine.class); databaseEngine.replay(firstPosition, firstBuffer.duplicate()); databaseEngine.replay(secondPosition, secondBuffer.duplicate()); EasyMock.replay(databaseEngine); Path path; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { path = segment.getPath(); assertEquals(firstPosition, segment.write(firstBuffer)); assertEquals(secondPosition, segment.write(secondBuffer)); assertEquals(thirdPosition, segment.write(thirdBuffer)); segment.flush(); } try (RandomAccessFile file = FileUtils.openRandomAccessFile(path)) { FileUtils.extendsOrTruncate(file, tuncationPoint); } try (CommitLogSegment segment = CommitLogSegment.loadFromFile(this.configuration, path)) { segment.replay(databaseEngine); } EasyMock.verify(databaseEngine); } /** * Test the replay when the data or the CRC of the data of the entry has not been written to the disk properly. * * @throws Exception if a problem occurs. */ @Test public void testReplayWithCorruptedData() throws Exception { long segmentId = IdFactory.nextId() + 1; int position = 0; Buffer firstBuffer = Buffers.wrap(new byte[] { 1, 123, 12, 37 }); position += (firstBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition firstPosition = new ReplayPosition(segmentId, position); Buffer secondBuffer = Buffers.wrap(new byte[] { -121, 5, 0, 30, 14, 56 }); position += (secondBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition secondPosition = new ReplayPosition(segmentId, position); Buffer thirdBuffer = Buffers.wrap(new byte[] { 4, 85, 0 }); position += (thirdBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition thirdPosition = new ReplayPosition(segmentId, position); StorageEngine databaseEngine = EasyMock.createMock(StorageEngine.class); databaseEngine.replay(firstPosition, firstBuffer.duplicate()); databaseEngine.replay(secondPosition, secondBuffer.duplicate()); EasyMock.replay(databaseEngine); Path path; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { path = segment.getPath(); assertEquals(firstPosition, segment.write(firstBuffer)); assertEquals(secondPosition, segment.write(secondBuffer)); assertEquals(thirdPosition, segment.write(thirdBuffer)); segment.flush(); } try (RandomAccessFile file = FileUtils.openRandomAccessFile(path)) { file.seek(position - CommitLogSegment.CHECKSUM_SIZE); file.writeLong(1); } try (CommitLogSegment segment = CommitLogSegment.loadFromFile(this.configuration, path)) { segment.replay(databaseEngine); } EasyMock.verify(databaseEngine); } /** * Test the replay when the length or the CRC of the length of the entry has not been written to the disk properly. * * @throws Exception if a problem occurs. */ @Test public void testReplayWithCorruptedLenght() throws Exception { long segmentId = IdFactory.nextId() + 1; int position = 0; Buffer firstBuffer = Buffers.wrap(new byte[] { 1, 123, 12, 37 }); position += (firstBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition firstPosition = new ReplayPosition(segmentId, position); Buffer secondBuffer = Buffers.wrap(new byte[] { -121, 5, 0, 30, 14, 56 }); position += (secondBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition secondPosition = new ReplayPosition(segmentId, position); Buffer thirdBuffer = Buffers.wrap(new byte[] { 4, 85, 0 }); position += (thirdBuffer.readableBytes() + LOG_OVERHEAD_SIZE); ReplayPosition thirdPosition = new ReplayPosition(segmentId, position); StorageEngine databaseEngine = EasyMock.createMock(StorageEngine.class); databaseEngine.replay(firstPosition, firstBuffer.duplicate()); databaseEngine.replay(secondPosition, secondBuffer.duplicate()); EasyMock.replay(databaseEngine); Path path; try (CommitLogSegment segment = CommitLogSegment.freshSegment(this.configuration)) { path = segment.getPath(); assertEquals(firstPosition, segment.write(firstBuffer)); assertEquals(secondPosition, segment.write(secondBuffer)); assertEquals(thirdPosition, segment.write(thirdBuffer)); segment.flush(); } try (RandomAccessFile file = FileUtils.openRandomAccessFile(path)) { file.seek(secondPosition.getPosition()); file.writeInt(1); } try (CommitLogSegment segment = CommitLogSegment.loadFromFile(this.configuration, path)) { segment.replay(databaseEngine); } EasyMock.verify(databaseEngine); } }