/*
* 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.util;
import c5db.CollectionMatchers;
import org.hamcrest.Matcher;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.States;
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import static c5db.ConcurrencyTestUtil.runAConcurrencyTestSeveralTimes;
import static c5db.ConcurrencyTestUtil.runNTimesAndWaitForAllToComplete;
import static c5db.FutureMatchers.resultsIn;
import static c5db.FutureMatchers.resultsInException;
import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.core.IsEqual.equalTo;
public class WrappingKeySerializingExecutorTest {
@Rule
public JUnitRuleMockery context = new JUnitRuleMockery();
private final ExecutorService fixedThreadExecutor = Executors.newFixedThreadPool(3);
private final ExecutorService executorService = context.mock(ExecutorService.class);
private static int numTasks = 20;
@SuppressWarnings("unchecked")
private final CheckedSupplier<Integer, Exception> task = context.mock(CheckedSupplier.class);
@After
public void shutdownExecutorService() {
fixedThreadExecutor.shutdownNow();
}
@Test
public void runsTasksSubmittedToItAndReturnsTheirResult() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(sameThreadExecutor());
context.checking(new Expectations() {{
oneOf(task).get();
will(returnValue(3));
}});
assertThat(keySerializingExecutor.submit("key", task), resultsIn(equalTo(3)));
}
@Test
public void returnsFuturesSetWithTheExceptionsThrownBySubmittedTasks() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(sameThreadExecutor());
context.checking(new Expectations() {{
oneOf(task).get();
will(throwException(new ArithmeticException("Expected as part of test")));
}});
assertThat(keySerializingExecutor.submit("key", task), resultsInException(ArithmeticException.class));
}
@Test
public void submitsTasksOnceEachToTheSuppliedExecutorService() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(executorService);
context.checking(new Expectations() {{
allowSubmitOrExecuteOnce(context, executorService);
}});
keySerializingExecutor.submit("key", task);
}
@Test(timeout = 1000)
public void executesTasksAllHavingTheSameKeyInSeries() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(fixedThreadExecutor);
List<Integer> log =
submitSeveralTasksAndBeginLoggingTheirInvocations(keySerializingExecutor, "key");
waitForTasksToFinish(keySerializingExecutor, "key");
assertThat(log, containsRecordOfEveryTask());
assertThat(log, isInTheOrderTheTasksWereSubmitted());
}
@Test(timeout = 1000)
public void executesTasksForDifferentKeysEachSeparatelyInSeries() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(fixedThreadExecutor);
List<Integer> log1 =
submitSeveralTasksAndBeginLoggingTheirInvocations(keySerializingExecutor, "key1");
List<Integer> log2 =
submitSeveralTasksAndBeginLoggingTheirInvocations(keySerializingExecutor, "key2");
waitForTasksToFinish(keySerializingExecutor, "key1");
waitForTasksToFinish(keySerializingExecutor, "key2");
assertThat(log1, containsRecordOfEveryTask());
assertThat(log1, isInTheOrderTheTasksWereSubmitted());
assertThat(log2, containsRecordOfEveryTask());
assertThat(log2, isInTheOrderTheTasksWereSubmitted());
}
@Test(expected = RejectedExecutionException.class)
public void throwsAnExceptionIfATaskIsSubmittedAfterShutdownIsCalled() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(fixedThreadExecutor);
keySerializingExecutor.shutdownAndAwaitTermination(1, TimeUnit.SECONDS);
keySerializingExecutor.submit("key", () -> null);
}
@Test
public void onShutdownCompletesAllTasksThatHadBeenSubmittedPriorToShutdown() throws Exception {
KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(fixedThreadExecutor);
List<Integer> log =
submitSeveralTasksAndBeginLoggingTheirInvocations(keySerializingExecutor, "key");
keySerializingExecutor.shutdownAndAwaitTermination(1, TimeUnit.SECONDS);
assertThat(log, containsRecordOfEveryTask());
}
@Test(timeout = 5000)
public void acceptsSubmissionsFromMultipleThreadsConcurrentlyWithEachThreadADifferentKey() throws Exception {
final int numThreads = 20;
final int numAttempts = 150;
runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::executeAMultikeySubmissionConcurrencyStressTest);
}
@Test(timeout = 5000)
public void acceptsSubmissionsFromMultipleThreadsConcurrentlyWithinOneKeyWithExecutionOrderUndetermined()
throws Exception {
final int numThreads = 50;
final int numAttempts = 300;
runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::executeASingleKeyConcurrencyStressTest);
}
@Test(timeout = 5000)
public void shutsDownIdempotently() throws Exception {
final int numThreads = 10;
final int numAttempts = 300;
runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::executeAShutdownIdempotencyStressTest);
}
@Test(timeout = 5000)
public void shutsDownAtomicallyWithRespectToSubmitAttempts() throws Exception {
final int numThreads = 5;
final int numAttempts = 100;
runAConcurrencyTestSeveralTimes(numThreads, numAttempts, this::executeAShutdownAtomicityStressTest);
}
private static List<Integer> submitSeveralTasksAndBeginLoggingTheirInvocations(
KeySerializingExecutor keySerializingExecutor, String key) {
List<Integer> log = new ArrayList<>(numTasks * 2);
for (int i = 0; i < numTasks; i++) {
keySerializingExecutor.submit(key, getSupplierWhichLogsItsNumberTwice(i, log));
}
return log;
}
private static CheckedSupplier<Integer, Exception> getSupplierWhichLogsItsNumberTwice(
int instanceNumber, List<Integer> log) {
return () -> {
log.add(instanceNumber);
Thread.yield();
log.add(instanceNumber);
return 0;
};
}
private static void waitForTasksToFinish(KeySerializingExecutor keySerializingExecutor, String key)
throws Exception {
keySerializingExecutor.submit(key, () -> 0).get();
}
public static void allowSubmitOrExecuteOnce(Mockery context, ExecutorService executorService) {
final States submitted = context.states("submitted").startsAs("no");
context.checking(new Expectations() {{
allowSubmitAndThen(context, executorService, submitted.is("yes"));
doNowAllowSubmitOnce(context, executorService, submitted.is("yes"));
}});
}
@SuppressWarnings("unchecked")
private static void allowSubmitAndThen(Mockery context,
ExecutorService executorService,
org.jmock.internal.State state) {
context.checking(new Expectations() {{
allowing(executorService).submit(with.<Callable>is(any(Callable.class)));
then(state);
allowing(executorService).submit(with.is(any(Runnable.class)), with.is(any(Object.class)));
then(state);
allowing(executorService).submit(with.<Runnable>is(any(Runnable.class)));
then(state);
allowing(executorService).execute(with.is(any(Runnable.class)));
then(state);
}});
}
@SuppressWarnings("unchecked")
private static void doNowAllowSubmitOnce(Mockery context,
ExecutorService executorService,
org.jmock.internal.State state) {
context.checking(new Expectations() {{
never(executorService).submit(with.<Callable>is(any(Callable.class)));
when(state);
never(executorService).submit(with.is(any(Runnable.class)), with.is(any(Object.class)));
when(state);
never(executorService).submit(with.<Runnable>is(any(Runnable.class)));
when(state);
never(executorService).execute(with.is(any(Runnable.class)));
when(state);
}});
}
private void executeAMultikeySubmissionConcurrencyStressTest(int numberOfSubmissions, ExecutorService taskSubmitter)
throws Exception {
final KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(
Executors.newSingleThreadExecutor());
runSeveralSimultaneousSeriesOfTasksAndWaitForAllToComplete(
numberOfSubmissions, taskSubmitter, keySerializingExecutor);
keySerializingExecutor.shutdownAndAwaitTermination(2, TimeUnit.SECONDS);
}
private void runSeveralSimultaneousSeriesOfTasksAndWaitForAllToComplete(
int numSimultaneous,
ExecutorService executorThatSubmitsTasks,
KeySerializingExecutor executorThatRunsTasks) throws Exception {
runNTimesAndWaitForAllToComplete(numSimultaneous, executorThatSubmitsTasks,
(int invocationIndex) -> runSeriesOfTasksForOneKey(executorThatRunsTasks, keyNumber(invocationIndex))
);
}
private String keyNumber(int i) {
return "key" + String.valueOf(i);
}
private void runSeriesOfTasksForOneKey(KeySerializingExecutor keySerializingExecutor,
String key) throws Exception {
setNumberOfTasks(2);
List<Integer> log = submitSeveralTasksAndBeginLoggingTheirInvocations(keySerializingExecutor, key);
waitForTasksToFinish(keySerializingExecutor, key);
assertThat(log, containsRecordOfEveryTask());
assertThat(log, isInTheOrderTheTasksWereSubmitted());
}
private static void setNumberOfTasks(int n) {
numTasks = n;
}
private void executeASingleKeyConcurrencyStressTest(int numCalls, ExecutorService executor)
throws Exception {
final KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(
Executors.newSingleThreadExecutor());
final List<Integer> taskResults = Collections.synchronizedList(new ArrayList<>(numCalls));
// The keySerializingExecutor can make no guarantee about the order in which tasks will
// be completed if added with the same key from multiple threads; only that they will all be completed.
runNTimesAndWaitForAllToComplete(numCalls, executor,
() -> {
keySerializingExecutor.submit("key", () -> {
taskResults.add(0);
return 0;
});
});
keySerializingExecutor.shutdownAndAwaitTermination(1, TimeUnit.SECONDS);
assertThat(taskResults, hasSize(numCalls));
}
private void executeAShutdownIdempotencyStressTest(int numShutdownCalls,
ExecutorService shutdownCallingService) throws Exception {
final KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(
Executors.newSingleThreadExecutor());
keySerializingExecutor.submit("key", () -> null).get();
runNTimesAndWaitForAllToComplete(numShutdownCalls, shutdownCallingService,
() -> keySerializingExecutor.shutdownAndAwaitTermination(1, TimeUnit.SECONDS));
}
private void executeAShutdownAtomicityStressTest(int numberOfSubmissions,
ExecutorService executor) throws Exception {
final KeySerializingExecutor keySerializingExecutor = new WrappingKeySerializingExecutor(
Executors.newSingleThreadExecutor());
// Simply call shutdown interspersed with other submit calls and ensure there are no errors
// However, it is nondeterministic which, if any, of the submits will go through.
runNTimesAndWaitForAllToComplete(numberOfSubmissions, executor,
(int invocationIndex) -> {
if (invocationIndex == numberOfSubmissions / 2) {
keySerializingExecutor.shutdownAndAwaitTermination(1, TimeUnit.SECONDS);
} else {
try {
keySerializingExecutor.submit(keyNumber(invocationIndex), () -> null);
} catch (RejectedExecutionException ignore) {
}
}
}
);
}
private static Matcher<Collection<?>> containsRecordOfEveryTask() {
return hasSize(numTasks * 2);
}
private static <T extends Comparable<T>> Matcher<List<T>> isInTheOrderTheTasksWereSubmitted() {
return CollectionMatchers.isNondecreasing();
}
}