/* * Copyright 2014 WANdisco * * WANdisco licenses this file to you 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 c5db.log; import c5db.C5CommonTestUtil; import c5db.MiscMatchers; import c5db.interfaces.replication.QuorumConfiguration; import c5db.log.generated.OLogHeader; import c5db.replication.generated.QuorumConfigurationMessage; import c5db.util.CheckedSupplier; import com.google.common.collect.Iterables; import com.google.common.primitives.Longs; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.file.Path; import java.util.List; import static c5db.log.EntryEncodingUtil.decodeAndCheckCrc; import static c5db.log.EntryEncodingUtil.encodeWithLengthAndCrc; import static c5db.log.LogMatchers.equalToHeader; import static c5db.log.LogPersistenceService.BytePersistence; import static c5db.log.ReplicatorLogGenericTestUtil.term; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; public class LogFileServiceTest { private static final String QUORUM_ID = "q"; private final Path testDirectory = (new C5CommonTestUtil()).getDataTestDir("log-file-service-test"); private LogFileService logFileService; @Before public void createTestObject() throws Exception { logFileService = new LogFileService(testDirectory); } @Test(expected = IOException.class) public void throwsAnIOExceptionIfAttemptingToAppendToTheFileAfterClosingIt() throws Exception { try (FilePersistence persistence = logFileService.create(QUORUM_ID)) { persistence.close(); persistence.append(serializedHeader(anOLogHeader())); } } @Test public void returnsNullWhenCallingGetCurrentWhenThereAreNoLogs() throws Exception { assertThat(logFileService.getCurrent(QUORUM_ID), nullValue()); } @Test public void returnsTheLatestAppendedPersistenceForAGivenQuorum() throws Exception { final OLogHeader header = anOLogHeader(); havingAppendedAPersistenceContainingHeader(header); try (BytePersistence persistence = logFileService.getCurrent(QUORUM_ID)) { assertThat(deserializedHeader(persistence), is(equalToHeader(header))); } } @Test public void returnsANewEmptyPersistenceOnEachCallToCreate() throws Exception { havingAppendedAPersistenceContainingHeader(anOLogHeader()); try (BytePersistence aSecondPersistence = logFileService.create(QUORUM_ID)) { assertThat(aSecondPersistence, isEmpty()); } } @Test public void appendsANewPersistenceSoThatItWillBeReturnedByFutureCallsToGetCurrent() throws Exception { final OLogHeader secondHeader; final long anArbitrarySeqNum = 1210; final long aGreaterArbitrarySeqNum = 1815; havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(anArbitrarySeqNum)); havingAppendedAPersistenceContainingHeader(secondHeader = anOLogHeaderWithSeqNum(aGreaterArbitrarySeqNum)); try (BytePersistence primaryPersistence = logFileService.getCurrent(QUORUM_ID)) { assertThat(deserializedHeader(primaryPersistence), is(equalToHeader(secondHeader))); } } @Test public void anExistingPersistenceRemainsUsableAfterBeingAppended() throws Exception { final OLogHeader header = anOLogHeader(); try (FilePersistence persistence = logFileService.create(QUORUM_ID)) { persistence.append(serializedHeader(header)); logFileService.append(QUORUM_ID, persistence); assertThat(deserializedHeader(persistence), is(equalToHeader(header))); } } @Test public void removesTheMostRecentAppendedDataStoreWhenTruncateIsCalled() throws Exception { final OLogHeader firstHeader; final long anArbitrarySeqNum = 1210; final long aGreaterArbitrarySeqNum = 1815; havingAppendedAPersistenceContainingHeader(firstHeader = anOLogHeaderWithSeqNum(anArbitrarySeqNum)); havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(aGreaterArbitrarySeqNum)); logFileService.truncate(QUORUM_ID); try (FilePersistence persistence = logFileService.getCurrent(QUORUM_ID)) { assertThat(deserializedHeader(persistence), is(equalToHeader(firstHeader))); } } @Test public void listsDataStoresInOrderOfMostRecentToLeastRecent() throws Exception { havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(1)); havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(2)); havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(3)); assertThat(logFileService.getList(QUORUM_ID), is(aListOfPersistencesWithSeqNums(3, 2, 1))); } @Test public void canArchiveAllButTheMostRecentFile() throws Exception { havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(1)); havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(2)); havingAppendedAPersistenceContainingHeader(anOLogHeaderWithSeqNum(3)); logFileService.archiveAllButCurrent(QUORUM_ID); assertThat(logFileService.getList(QUORUM_ID), is(aListOfPersistencesWithSeqNums(3))); } private void havingAppendedAPersistenceContainingHeader(OLogHeader header) throws Exception { try (FilePersistence persistenceToReplacePrimary = logFileService.create(QUORUM_ID)) { persistenceToReplacePrimary.append(serializedHeader(header)); logFileService.append(QUORUM_ID, persistenceToReplacePrimary); } } private static OLogHeader anOLogHeader() { return anOLogHeaderWithSeqNum(1); } private static OLogHeader anOLogHeaderWithSeqNum(long seqNum) { return new OLogHeader(term(1), seqNum, configurationOf(1, 2, 3)); } private ByteBuffer[] serializedHeader(OLogHeader header) { List<ByteBuffer> buffers = encodeWithLengthAndCrc(OLogHeader.getSchema(), header); return Iterables.toArray(buffers, ByteBuffer.class); } private static OLogHeader deserializedHeader(BytePersistence persistence) throws Exception { try (InputStream input = Channels.newInputStream(persistence.getReader())) { return decodeAndCheckCrc(input, OLogHeader.getSchema()); } } private static QuorumConfigurationMessage configurationOf(long... peerIds) { return QuorumConfiguration.of(Longs.asList(peerIds)).toProtostuff(); } private static Matcher<List<? extends CheckedSupplier<? extends BytePersistence, IOException>>> aListOfPersistencesWithSeqNums( long... seqNumsInHeaders) { return MiscMatchers.simpleMatcherForPredicate((list) -> { try { int seqNumIndex = 0; for (CheckedSupplier<? extends BytePersistence, IOException> persistenceSupplier : list) { BytePersistence persistence = persistenceSupplier.get(); OLogHeader header = deserializedHeader(persistence); if (header.getBaseSeqNum() != seqNumsInHeaders[seqNumIndex]) { return false; } seqNumIndex++; } return seqNumIndex == seqNumsInHeaders.length; } catch (Exception e) { throw new AssertionError(e); } }, (description) -> description.appendText("a List of BytePersistence Suppliers, where the persistence" + " objects begin with headers with sequence numbers ").appendValue(seqNumsInHeaders).appendText(" in order")); } private static Matcher<BytePersistence> isEmpty() { return MiscMatchers.simpleMatcherForPredicate((persistence) -> { try { return persistence.isEmpty(); } catch (IOException e) { throw new AssertionError(e); } }, (description) -> description.appendText("is empty")); } }