/* * 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.util.KeySerializingExecutor; import c5db.util.WrappingKeySerializingExecutor; import org.junit.Test; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import static c5db.ConcurrencyTestUtil.runAConcurrencyTestSeveralTimes; import static c5db.ConcurrencyTestUtil.runNTimesAndWaitForAllToComplete; import static c5db.FutureMatchers.resultsIn; import static c5db.log.LogTestUtil.someConsecutiveEntries; import static c5db.log.QuorumDelegatingLogUnitTest.ArrayPersistenceService; import static java.util.concurrent.Executors.newFixedThreadPool; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public class QuorumDelegatingLogConcurrencyTest { private static final int LOG_WORKER_THREADS = 8; private static final int ENTRIES_PER_QUORUM_PER_TEST = 1; @Test(timeout = 5000) public void isThreadSafeWithRespectToLoggingFromMultipleQuorumsWithEachQuorumItsOwnThread() throws Exception { final int numThreads = 150; final int numAttempts = 5; runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::runMultipleQuorumThreadSafetyTest); } @Test(timeout = 5000) public void isThreadSafeWithRespectToCallingCloseFromMultipleThreads() throws Exception { final int numThreads = 100; final int numAttempts = 10; runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::runCloseThreadSafetyTest); } private void runMultipleQuorumThreadSafetyTest(int numQuorums, ExecutorService executor) throws Exception { try (OLog log = createLog()) { runForNQuorums(numQuorums, executor, (quorumId) -> { log.openAsync(quorumId) .get(); log.logEntries(logEntriesForQuorum(quorumId), quorumId) .get(); }); assertThatEverythingWasLoggedCorrectly(log); } } private void runCloseThreadSafetyTest(int numQuorums, ExecutorService executor) throws Exception { try (OLog log = createLog()) { runForNQuorums(numQuorums, executor, (quorumId) -> { log.openAsync(quorumId) .get(); log.logEntries(logEntriesForQuorum(quorumId), quorumId) .get(); }); runNTimesAndWaitForAllToComplete(numQuorums * 2, executor, log::close); } } private void runForNQuorums(int numQuorums, ExecutorService executor, QuorumRunner runner) throws Exception { runNTimesAndWaitForAllToComplete(numQuorums, executor, (int invocationIndex) -> { String quorumId = getQuorumNameForIndex(invocationIndex); runner.run(quorumId); }); } private void assertThatEverythingWasLoggedCorrectly(OLog log) { for (String quorumId : allQuorums()) { assertThat(log.getLogEntries(1, 1 + ENTRIES_PER_QUORUM_PER_TEST, quorumId), resultsIn(equalTo(logEntriesForQuorum(quorumId)))); assertThat(log.getLogTerm(1, quorumId), is(equalTo(getTermAtSeq(1, quorumId)))); } } private OLog createLog() { KeySerializingExecutor executor = new WrappingKeySerializingExecutor(newFixedThreadPool(LOG_WORKER_THREADS)); // Run test in memory for speed. The concurrency properties of the code will still be tested. return new QuorumDelegatingLog( new ArrayPersistenceService(), executor, NavigableMapOLogEntryOracle::new, InMemoryPersistenceNavigator::new); } private String getQuorumNameForIndex(int index) { return "quorum" + String.valueOf(index); } private final Map<String, List<OLogEntry>> logEntryListsMemoized = new ConcurrentHashMap<>(); private List<OLogEntry> logEntriesForQuorum(String quorumId) { logEntryListsMemoized.putIfAbsent(quorumId, someConsecutiveEntries(1, 1 + ENTRIES_PER_QUORUM_PER_TEST)); return logEntryListsMemoized.get(quorumId); } private long getTermAtSeq(long seqNum, String quorumId) { for (OLogEntry entry : logEntryListsMemoized.get(quorumId)) { if (entry.getSeqNum() == seqNum) { return entry.getElectionTerm(); } } throw new IndexOutOfBoundsException("getTermAtSeq"); } private Set<String> allQuorums() { return logEntryListsMemoized.keySet(); } private interface QuorumRunner { void run(String quorumId) throws Exception; } }