/* * 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.interfaces.replication.QuorumConfiguration; import c5db.util.WrappingKeySerializingExecutor; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; import static c5db.FutureMatchers.resultsIn; import static c5db.FutureMatchers.resultsInException; import static c5db.log.LogMatchers.aListOfEntriesWithConsecutiveSeqNums; import static c5db.log.LogTestUtil.emptyEntryList; import static c5db.log.LogTestUtil.makeSingleEntryList; import static c5db.log.LogTestUtil.someConsecutiveEntries; import static c5db.log.OLogEntryOracle.QuorumConfigurationWithSeqNum; import static c5db.log.ReplicatorLogGenericTestUtil.seqNum; import static c5db.log.ReplicatorLogGenericTestUtil.someData; import static c5db.log.ReplicatorLogGenericTestUtil.term; import static c5db.log.SequentialLog.LogEntryNotFound; import static c5db.replication.ReplicatorTestUtil.makeConfigurationEntry; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class QuorumDelegatingLogTest { private static Path testDirectory; private LogFileService logFileService; private OLog log; private final String quorumId = "quorumId"; @BeforeClass public static void setTestDirectory() { if (testDirectory == null) { testDirectory = (new C5CommonTestUtil()).getDataTestDir("olog"); } } @Before public final void setUp() throws Exception { logFileService = new LogFileService(testDirectory); logFileService.clearAllLogs(); log = new QuorumDelegatingLog( logFileService, new WrappingKeySerializingExecutor(MoreExecutors.sameThreadExecutor()), NavigableMapOLogEntryOracle::new, InMemoryPersistenceNavigator::new); log.openAsync(quorumId).get(); } @After public final void tearDown() throws Exception { log.close(); logFileService.clearAllLogs(); } @Test(timeout = 1000) public void throwsExceptionFromGetLogEntriesMethodWhenTheLogIsEmpty() throws Exception { assertThat(log.getLogEntries(1, 2, quorumId), resultsInException(LogEntryNotFound.class)); } @Test public void retrievesListsOfEntriesItHasLogged() throws Exception { List<OLogEntry> entries = someConsecutiveEntries(1, 5); log.logEntries(entries, quorumId); assertThat(log.getLogEntries(1, 5, quorumId), resultsIn(equalTo(entries))); } @Test(expected = IllegalArgumentException.class) public void throwsAnExceptionWhenGettingEntriesIfEndIsLessThanStart() throws Exception { log.logEntries(someConsecutiveEntries(1, 5), quorumId); log.getLogEntries(3, 2, quorumId); } @Test public void returnsAnEmptyListWhenGettingEntriesIfStartEqualsEnd() throws Exception { log.logEntries(someConsecutiveEntries(1, 5), quorumId); assertThat(log.getLogEntries(3, 3, quorumId), resultsIn(equalTo(emptyEntryList()))); } @Test public void logsAndRetrievesDifferentQuorumsWithTheSameSequenceNumbers() throws Exception { String quorumA = "A"; String quorumB = "B"; log.openAsync(quorumA); log.openAsync(quorumB); List<OLogEntry> entriesA = someConsecutiveEntries(1, 5); List<OLogEntry> entriesB = someConsecutiveEntries(1, 5); log.logEntries(entriesA, quorumA); log.logEntries(entriesB, quorumB); assertThat(log.getLogEntries(1, 5, quorumA), resultsIn(equalTo(entriesA))); assertThat(log.getLogEntries(1, 5, quorumB), resultsIn(equalTo(entriesB))); } @Test public void retrievesEntriesFromTheMiddleOfTheLog() throws Exception { List<OLogEntry> entries = someConsecutiveEntries(1, 10); log.logEntries(entries, quorumId); assertThat(log.getLogEntries(4, 6, quorumId), resultsIn(equalTo(subListWithSeqNums(entries, 4, 6)))); } @Test public void truncatesEntriesFromTheEndOfTheLogAndMaintainsTheCorrectSequence() throws Exception { log.logEntries(someConsecutiveEntries(1, 5), quorumId); log.truncateLog(3, quorumId); log.logEntries(someConsecutiveEntries(3, 5), quorumId); assertThat(log.getLogEntries(1, 5, quorumId), resultsIn(aListOfEntriesWithConsecutiveSeqNums(1, 5))); } @Test(expected = RuntimeException.class) public void throwsAnExceptionIfAskedToLogEntriesWithASequenceGap() throws Exception { log.logEntries(someConsecutiveEntries(1, 3), quorumId); log.logEntries(someConsecutiveEntries(4, 5), quorumId); } @Test(expected = RuntimeException.class) public void throwsAnExceptionIfAskedToLogEntriesWithoutAscendingSequenceNumber() throws Exception { log.logEntries(someConsecutiveEntries(1, 2), quorumId); log.logEntries(someConsecutiveEntries(1, 2), quorumId); } @Test(timeout = 1000) public void returnsAFutureWithAnExceptionIfAskedToRetrieveEntriesAndAtLeastOneIsNotInTheLog() throws Exception { log.logEntries(someConsecutiveEntries(1, 5), quorumId); log.truncateLog(3, quorumId); assertThat(log.getLogEntries(2, 4, quorumId), resultsInException(LogEntryNotFound.class)); } @Test public void returnsTheExpectedNextSequenceNumber() { assertThat(log.getNextSeqNum(quorumId), equalTo(1L)); log.logEntries(someConsecutiveEntries(1, 4), quorumId); assertThat(log.getNextSeqNum(quorumId), equalTo(4L)); log.truncateLog(seqNum(2), quorumId); assertThat(log.getNextSeqNum(quorumId), equalTo(2L)); } @Test public void storesAndRetrievesElectionTermForEntriesItHasLogged() { log.logEntries(makeSingleEntryList(nextSeqNum(), term(1), someData()), quorumId); log.logEntries(makeSingleEntryList(nextSeqNum(), term(2), someData()), quorumId); assertThat(log.getLastTerm(quorumId), is(equalTo(term(2)))); assertThat(log.getLogTerm(lastSeqNum(), quorumId), is(equalTo(term(2)))); assertThat(log.getLogTerm(lastSeqNum() - 1, quorumId), is(equalTo(term(1)))); } @Test public void retrievesCorrectElectionTermAfterATruncation() { log.logEntries(makeSingleEntryList(nextSeqNum(), term(1), someData()), quorumId); log.logEntries(makeSingleEntryList(nextSeqNum(), term(2), someData()), quorumId); log.truncateLog(lastSeqNum(), quorumId); log.logEntries(makeSingleEntryList(lastSeqNum(), term(3), someData()), quorumId); assertThat(log.getLastTerm(quorumId), is(equalTo(term(3)))); assertThat(log.getLogTerm(lastSeqNum(), quorumId), is(equalTo(term(3)))); } @Test public void retrievesTheLastQuorumConfigurationAndItsSequenceNumber() { QuorumConfiguration firstConfig = QuorumConfiguration.of(Sets.newHashSet(1L, 2L, 3L)); QuorumConfiguration secondConfig = firstConfig.getTransitionalConfiguration(Sets.newHashSet(4L, 5L, 6L)); log.logEntries(singleConfigurationEntryList(firstConfig, seqNum(1)), quorumId); assertThat(log.getLastQuorumConfig(quorumId), equalTo(new QuorumConfigurationWithSeqNum(firstConfig, seqNum(1)))); log.logEntries(singleConfigurationEntryList(secondConfig, seqNum(2)), quorumId); assertThat(log.getLastQuorumConfig(quorumId), equalTo(new QuorumConfigurationWithSeqNum(secondConfig, seqNum(2)))); log.truncateLog(seqNum(2), quorumId); assertThat(log.getLastQuorumConfig(quorumId), equalTo(new QuorumConfigurationWithSeqNum(firstConfig, seqNum(1)))); } @Test public void returnsResultsFromGetLogEntriesThatTakeIntoAccountMutationsRequestedEarlier() throws Exception { log.logEntries(someConsecutiveEntries(1, 10), quorumId); log.truncateLog(5, quorumId); List<OLogEntry> replacementEntries = someConsecutiveEntries(5, 10); log.logEntries(replacementEntries, quorumId); assertThat(log.getLogEntries(5, 10, quorumId), resultsIn(equalTo(replacementEntries))); } @Test(timeout = 3000) public void rollsTheLogWhileAcceptingLogRequestsAndEnsuresThatAllRequestedEntriesEndUpInTheNewLog() throws Exception { log.logEntries(someConsecutiveEntries(1, 11), quorumId); log.roll(quorumId); log.logEntries(someConsecutiveEntries(11, 21), quorumId); assertThat(log.getLogEntries(11, 21, quorumId), resultsIn(aListOfEntriesWithConsecutiveSeqNums(11, 21))); } @Test public void truncatesARolledFileIfRequestedToTruncateToAPointBeforeTheBeginningOfTheCurrentFile() throws Exception { log.logEntries(someConsecutiveEntries(1, 11), quorumId); log.roll(quorumId); log.truncateLog(seqNum(6), quorumId); log.logEntries(someConsecutiveEntries(6, 21), quorumId); assertThat(log.getLogEntries(6, 21, quorumId), resultsIn(aListOfEntriesWithConsecutiveSeqNums(6, 21))); } @Test public void fulfillsGetRequestsThatSpanMultipleLogFiles() throws Exception { log.logEntries(someConsecutiveEntries(1, 6), quorumId); log.roll(quorumId); log.logEntries(someConsecutiveEntries(6, 11), quorumId); log.roll(quorumId); log.logEntries(someConsecutiveEntries(11, 16), quorumId); assertThat(log.getLogEntries(3, 15, quorumId), resultsIn(aListOfEntriesWithConsecutiveSeqNums(3, 15))); } /** * Private methods */ private long testSequenceNumber = 0; private long nextSeqNum() { testSequenceNumber++; return testSequenceNumber; } private long lastSeqNum() { return testSequenceNumber; } private static List<OLogEntry> singleConfigurationEntryList(QuorumConfiguration config, long seqNum) { return Lists.newArrayList( OLogEntry.fromProtostuff( makeConfigurationEntry(seqNum, term(1), config))); } private static List<OLogEntry> subListWithSeqNums(List<OLogEntry> entryList, long start, long end) { return entryList.stream() .filter((entry) -> start <= entry.getSeqNum() && entry.getSeqNum() < end) .collect(Collectors.toList()); } }