/* * 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.log.SequentialEntryCodec; import c5db.interfaces.replication.QuorumConfiguration; import c5db.util.CheckedSupplier; import c5db.util.KeySerializingExecutor; import c5db.util.WrappingKeySerializingExecutor; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.MoreExecutors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jmock.Expectations; import org.jmock.integration.junit4.JUnitRuleMockery; import org.jmock.lib.concurrent.Synchroniser; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static c5db.FutureMatchers.resultsInException; import static c5db.log.LogPersistenceService.BytePersistence; import static c5db.log.LogPersistenceService.PersistenceNavigator; import static c5db.log.LogPersistenceService.PersistenceNavigatorFactory; import static c5db.log.LogTestUtil.makeEntry; import static c5db.log.LogTestUtil.makeSingleEntryList; import static c5db.log.LogTestUtil.someConsecutiveEntries; import static c5db.log.OLog.QuorumNotOpen; import static c5db.log.OLogEntryOracle.OLogEntryOracleFactory; 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 org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; @SuppressWarnings("unchecked") public class QuorumDelegatingLogUnitTest { @Rule public JUnitRuleMockery context = new JUnitRuleMockery() {{ setThreadingPolicy(new Synchroniser()); }}; private final KeySerializingExecutor serializingExecutor = new WrappingKeySerializingExecutor(MoreExecutors.sameThreadExecutor()); private final OLogEntryOracleFactory OLogEntryOracleFactory = context.mock(OLogEntryOracleFactory.class); private final OLogEntryOracle oLogEntryOracle = context.mock(OLogEntryOracle.class); private final PersistenceNavigatorFactory navigatorFactory = context.mock(PersistenceNavigatorFactory.class); private final PersistenceNavigator persistenceNavigator = context.mock(PersistenceNavigator.class); private final ArrayPersistenceService persistenceService = new ArrayPersistenceService(); private final QuorumDelegatingLog oLog = new QuorumDelegatingLog( persistenceService, serializingExecutor, OLogEntryOracleFactory, navigatorFactory); @Before public void setUpMockedFactories() throws Exception { context.checking(new Expectations() {{ allowing(navigatorFactory).create(with(any(BytePersistence.class)), with.<SequentialEntryCodec<?>>is(any(SequentialEntryCodec.class)), with(any(Long.class))); will(returnValue(persistenceNavigator)); allowing(OLogEntryOracleFactory).create(); will(returnValue(oLogEntryOracle)); atMost(1).of(oLogEntryOracle).notifyLogging(with(any(OLogEntry.class))); allowing(oLogEntryOracle).getGreatestSeqNum(); allowing(persistenceNavigator).getStreamAtFirstEntry(); will(returnValue(aZeroLengthInputStream())); }}); } @After public void closeLog() throws Exception { oLog.close(); } @Test(expected = QuorumNotOpen.class) public void throwsAnExceptionIfAttemptingToLogToAQuorumBeforeOpeningIt() throws Exception { oLog.logEntries(arbitraryEntries(), "quorum"); } @Test(expected = Exception.class) public void throwsAnExceptionIfAttemptingToOpenAQuorumAfterClosingTheLog() throws Exception { oLog.close(); oLog.openAsync("quorum"); } @Test(timeout = 3000) public void getsOneNewPersistenceObjectPerQuorumWhenLogEntriesIsCalled() throws Exception { String quorumA = "quorumA"; String quorumB = "quorumB"; context.checking(new Expectations() {{ allowing(oLogEntryOracle).notifyLogging(with(any(OLogEntry.class))); allowing(persistenceNavigator).notifyLogging(with(any(Long.class)), with(any(Long.class))); allowing(persistenceNavigator).addToIndex(with(any(Long.class)), with(any(Long.class))); }}); oLog.openAsync(quorumA).get(); oLog.openAsync(quorumB).get(); oLog.logEntries(arbitraryEntries(), quorumA); oLog.logEntries(arbitraryEntries(), quorumB); } @Test(timeout = 3000) public void passesLoggedEntriesToItsOLogEntryOracleObject() throws Exception { final String quorumId = "quorum"; final OLogEntry entry = makeEntry(seqNum(1), term(1), someData()); context.checking(new Expectations() {{ ignoring(persistenceNavigator); oneOf(oLogEntryOracle).notifyLogging(entry); }}); oLog.openAsync(quorumId).get(); oLog.logEntries(Lists.newArrayList(entry), quorumId); } @Test(timeout = 3000) public void createsANewLogWhenRollIsCalledAndWritesSubsequentEntriesToTheNewLog() throws Exception { final String quorumId = "quorum"; context.checking(new Expectations() {{ ignoring(persistenceNavigator); allowing(oLogEntryOracle).notifyLogging(with(any(OLogEntry.class))); allowing(oLogEntryOracle).getLastTerm(); allowing(oLogEntryOracle).getLastQuorumConfig(); will(returnValue(new QuorumConfigurationWithSeqNum(QuorumConfiguration.EMPTY, 0))); }}); oLog.openAsync(quorumId).get(); oLog.logEntries(someConsecutiveEntries(1, 11), quorumId); oLog.roll(quorumId).get(); assertThat(logPersistenceObjectsForQuorum(quorumId), hasSize(2)); deleteFirstLog(quorumId); oLog.logEntries(someConsecutiveEntries(11, 21), quorumId).get(); } @Test(timeout = 3000) public void returnsAnExceptionIfAttemptingToTruncateBackToANonExistentLog() throws Exception { final String quorumId = "quorum"; context.checking(new Expectations() {{ ignoring(persistenceNavigator); allowing(oLogEntryOracle).notifyLogging(with(any(OLogEntry.class))); allowing(oLogEntryOracle).notifyTruncation(with(any(Long.class))); allowing(oLogEntryOracle).getLastTerm(); allowing(oLogEntryOracle).getLastQuorumConfig(); will(returnValue(new QuorumConfigurationWithSeqNum(QuorumConfiguration.EMPTY, 0))); }}); oLog.openAsync(quorumId).get(); oLog.logEntries(someConsecutiveEntries(1, 11), quorumId); oLog.roll(quorumId).get(); deleteFirstLog(quorumId); assertThat(oLog.truncateLog(seqNum(5), quorumId), resultsInException(IOException.class)); } private static List<OLogEntry> arbitraryEntries() { return makeSingleEntryList(seqNum(1), term(1), "x"); } private InputStream aZeroLengthInputStream() { return new InputStream() { @Override public int read() throws IOException { return -1; } }; } private Collection<BytePersistence> logPersistenceObjectsForQuorum(String quorumId) { return new ArrayList<>(persistenceService.quorumMap.get(quorumId)); } private void deleteFirstLog(String quorumId) throws Exception { // TODO for now, closing the persistence, which is actually in-memory and not really persisted // TODO to any medium, is used to simulate the underlying persistence having been deleted. persistenceService.firstLog(quorumId).close(); } /** * In-memory LogPersistenceService to simplify these tests, rather than use mocks that return mocks. */ static class ArrayPersistenceService implements LogPersistenceService<ByteArrayPersistence> { private final Map<String, Deque<ByteArrayPersistence>> quorumMap = new ConcurrentHashMap<>(); @Nullable @Override public ByteArrayPersistence getCurrent(String quorumId) throws IOException { quorumMap.putIfAbsent(quorumId, new LinkedList<>()); return quorumMap.get(quorumId).peek(); } @NotNull @Override public ByteArrayPersistence create(String quorumId) throws IOException { return new ByteArrayPersistence(); } @Override public void append(String quorumId, @NotNull ByteArrayPersistence persistence) throws IOException { quorumMap.putIfAbsent(quorumId, new LinkedList<>()); quorumMap.get(quorumId).push(persistence); } @Override public void truncate(String quorumId) throws IOException { quorumMap.get(quorumId).pop(); } @Override public ImmutableList<CheckedSupplier<ByteArrayPersistence, IOException>> getList(String quorumId) throws IOException { List<CheckedSupplier<ByteArrayPersistence, IOException>> persistenceSupplierList = new ArrayList<>(); for (ByteArrayPersistence persistence : quorumMap.get(quorumId)) { persistenceSupplierList.add( () -> persistence); } return ImmutableList.copyOf(persistenceSupplierList); } public BytePersistence firstLog(String quorumId) { return quorumMap.get(quorumId).peekLast(); } } }