/*
* 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.interfaces.replication.QuorumConfiguration;
import c5db.interfaces.replication.ReplicatorLog;
import c5db.replication.generated.LogEntry;
import com.google.common.collect.Lists;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static c5db.FutureActions.returnFutureWithValue;
import static c5db.log.OLogEntryOracle.QuorumConfigurationWithSeqNum;
import static c5db.replication.ReplicatorTestUtil.makeConfigurationEntry;
import static c5db.replication.ReplicatorTestUtil.makeProtostuffEntry;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
public class MooringTest {
@Rule
public JUnitRuleMockery context = new JUnitRuleMockery();
private final OLog oLog = context.mock(OLog.class);
private final String quorumId = "quorumId";
private ReplicatorLog log;
@Before
public void accessesOLogToObtainTheLastTermAndIndexWhenItIsConstructed() throws Exception {
context.checking(new Expectations() {{
oneOf(oLog).openAsync(quorumId);
will(returnFutureWithValue(null));
allowing(oLog).getLastTerm(quorumId);
will(returnValue(0L));
allowing(oLog).getNextSeqNum(quorumId);
will(returnValue(1L));
oneOf(oLog).getLastQuorumConfig(quorumId);
will(returnValue(zeroConfiguration()));
}});
log = new Mooring(oLog, quorumId);
}
@Test
public void returnsZeroFromGetLastTermWhenLogIsEmpty() {
ignoringLog();
assertThat(log.getLastTerm(), is(equalTo(0L)));
}
@Test
public void returnsZeroFromGetLastIndexWhenLogIsEmpty() {
ignoringLog();
assertThat(log.getLastIndex(), is(equalTo(0L)));
}
@Test(expected = Exception.class)
public void doesNotAcceptARequestToLogAnEmptyEntryList() {
log.logEntries(new ArrayList<>());
}
@Test
public void canReturnTheTermAndIndexOfTheLastEntryLogged() {
expectLoggingNTimes(1);
log.logEntries(
singleEntryList(index(12), term(34), someData()));
assertThat(log.getLastIndex(), is(equalTo(12L)));
assertThat(log.getLastTerm(), is(equalTo(34L)));
}
@Test
public void delegatesLogAndTruncationRequestsToOLog() {
long index = 12;
expectLoggingNTimes(1);
expectTruncationNTimes(1);
oLogGetTermWillReturn(0);
context.checking(new Expectations() {{
oneOf(oLog).getLastQuorumConfig(quorumId);
will(returnValue(zeroConfiguration()));
}});
log.logEntries(
singleEntryList(index, term(34), someData()));
log.truncateLog(index);
}
@Test
public void canReturnTheTermAndIndexOfAnEntryAfterPerformingATruncation() {
long termOfFirstEntry = 34;
long indexOfFirstEntry = 12;
expectLoggingNTimes(1);
expectTruncationNTimes(1);
oLogGetTermWillReturn(termOfFirstEntry);
context.checking(new Expectations() {{
oneOf(oLog).getLastQuorumConfig(quorumId);
will(returnValue(zeroConfiguration()));
}});
log.logEntries(
Lists.newArrayList(
makeProtostuffEntry(indexOfFirstEntry, termOfFirstEntry, someData()),
makeProtostuffEntry(indexOfFirstEntry + 1, term(35), someData())));
log.truncateLog(indexOfFirstEntry + 1);
assertThat(log.getLastIndex(), is(equalTo(indexOfFirstEntry)));
assertThat(log.getLastTerm(), is(equalTo(termOfFirstEntry)));
}
@Test(expected = IllegalArgumentException.class)
public void throwsAnExceptionIfAskedToTruncateToAnIndexOfZero() {
log.truncateLog(0);
}
@Test
public void storesAndRetrievesTheLastQuorumConfigurationLogged() {
final long term = 7;
final QuorumConfiguration config = aQuorumConfiguration();
expectLoggingNTimes(1);
log.logEntries(
Lists.newArrayList(
makeProtostuffEntry(index(2), term, someData()),
makeConfigurationEntry(index(3), term, config),
makeProtostuffEntry(index(4), term, someData())
));
assertThat(log.getLastConfiguration(), is(equalTo(config)));
assertThat(log.getLastConfigurationIndex(), is(equalTo(3L)));
}
@Test
public void retrievesTheEmptyQuorumConfigurationWhenTheLogIsEmpty() {
assertThat(log.getLastConfiguration(), is(equalTo(QuorumConfiguration.EMPTY)));
assertThat(log.getLastConfigurationIndex(), is(equalTo(0L)));
}
@Test
public void retrievesAnEarlierQuorumConfigurationWhenALaterOneIsTruncated() {
final long term = 7;
final QuorumConfiguration firstConfig = aQuorumConfiguration();
final long firstConfigSeqNum = 777;
final QuorumConfiguration secondConfig = firstConfig.getCompletedConfiguration();
final long secondConfigSeqNum = firstConfigSeqNum + 1;
expectLoggingNTimes(1);
expectTruncationNTimes(1);
allowOLogGetTerm();
log.logEntries(
Lists.newArrayList(
makeConfigurationEntry(firstConfigSeqNum, term, firstConfig),
makeConfigurationEntry(secondConfigSeqNum, term, secondConfig)
));
assertThat(log.getLastConfiguration(), is(equalTo(secondConfig)));
assertThat(log.getLastConfigurationIndex(), is(equalTo(secondConfigSeqNum)));
context.checking(new Expectations() {{
oneOf(oLog).getLastQuorumConfig(quorumId);
will(returnValue(new QuorumConfigurationWithSeqNum(firstConfig, firstConfigSeqNum)));
}});
log.truncateLog(secondConfigSeqNum);
assertThat(log.getLastConfiguration(), is(equalTo(firstConfig)));
assertThat(log.getLastConfigurationIndex(), is(equalTo(firstConfigSeqNum)));
}
private long index(long i) {
return i;
}
private long term(long i) {
return i;
}
private String someData() {
return "test data";
}
private QuorumConfiguration aQuorumConfiguration() {
return QuorumConfiguration
.of(Lists.newArrayList(1L, 2L, 3L))
.getTransitionalConfiguration(Lists.newArrayList(4L, 5L, 6L));
}
private List<LogEntry> singleEntryList(long index, long term, String stringData) {
return Lists.newArrayList(makeProtostuffEntry(index, term, stringData));
}
@SuppressWarnings("unchecked")
private void expectLoggingNTimes(int n) {
context.checking(new Expectations() {{
exactly(n).of(oLog).logEntries(with.is(any(List.class)), with(any(String.class)));
}});
}
private void expectTruncationNTimes(int n) {
context.checking(new Expectations() {{
exactly(n).of(oLog).truncateLog(with(any(Long.class)), with(any(String.class)));
}});
}
private void oLogGetTermWillReturn(long expectedTerm) {
context.checking(new Expectations() {{
exactly(1).of(oLog).getLogTerm(with(any(Long.class)), with(any(String.class)));
will(returnValue(expectedTerm));
}});
}
private void allowOLogGetTerm() {
context.checking(new Expectations() {{
allowing(oLog).getLogTerm(with(any(Long.class)), with(any(String.class)));
}});
}
private void ignoringLog() {
context.checking(new Expectations() {{
ignoring(oLog);
}});
}
private QuorumConfigurationWithSeqNum zeroConfiguration() {
return new QuorumConfigurationWithSeqNum(QuorumConfiguration.EMPTY, 0);
}
}