/* * 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.replication; import c5db.C5CommonTestUtil; import c5db.MiscMatchers; import c5db.ReplicatorConstants; import c5db.SimpleModuleInformationProvider; import c5db.discovery.BeaconService; import c5db.interfaces.DiscoveryModule; import c5db.interfaces.LogModule; import c5db.interfaces.ReplicationModule; import c5db.interfaces.replication.GeneralizedReplicator; import c5db.interfaces.replication.ReplicateSubmissionInfo; import c5db.log.LogService; import c5db.log.ReplicatorLogGenericTestUtil; import c5db.util.C5Futures; import c5db.util.ExceptionHandlingBatchExecutor; import c5db.util.FiberSupplier; import c5db.util.JUnitRuleFiberExceptions; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Service; import com.google.common.util.concurrent.SettableFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import org.hamcrest.Matcher; import org.jetlang.fibers.Fiber; import org.jetlang.fibers.PoolFiberFactory; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.stream.Collectors; import static c5db.CollectionMatchers.isStrictlyIncreasing; import static c5db.FutureMatchers.resultsIn; import static c5db.FutureMatchers.returnsAFutureWhoseResult; import static com.google.common.util.concurrent.Futures.allAsList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; public class C5GeneralizedReplicationServiceTest { @Rule public JUnitRuleFiberExceptions jUnitFiberExceptionHandler = new JUnitRuleFiberExceptions(); private static final int NUMBER_OF_PROCESSORS = Runtime.getRuntime().availableProcessors(); private static final int DISCOVERY_PORT = 54333; private final Path baseTestPath = new C5CommonTestUtil().getDataTestDir("general-replicator-test"); private final ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_OF_PROCESSORS); private final PoolFiberFactory fiberFactory = new PoolFiberFactory(executorService); private final Set<Fiber> fibers = new HashSet<>(); private final Fiber mainTestFiber = newFiber(jUnitFiberExceptionHandler); @Before public void setupConfigDirectory() throws Exception { mainTestFiber.start(); } @After public void disposeOfResources() throws Exception { fiberFactory.dispose(); executorService.shutdownNow(); fibers.forEach(Fiber::dispose); } @Test(timeout = 9000) public void logsToASingleQuorumReplicator() throws Exception { List<Long> nodeIds = Lists.newArrayList(1L); try (QuorumOfReplicatorsController controller = newQuorum(nodeIds)) { GeneralizedReplicator replicator = controller.waitUntilAReplicatorIsReady(); ListenableFuture<List<ReplicateSubmissionInfo>> resultListFuture = resultFutureForNReplicateRequests(replicator, 3); assertThat(sequenceNumberListFuture(resultListFuture), resultsInAListOfLongsThat(hasSize(3))); assertThat(sequenceNumberListFuture(resultListFuture), resultsInAListOfLongsThat(isStrictlyIncreasing())); assertThat(resultListFuture, allRequestsWillComplete()); } } @Test(timeout = 9000) public void replicatesAcrossAQuorumComposedOfThreeReplicators() throws Exception { List<Long> nodeIds = Lists.newArrayList(1L, 2L, 3L); try (QuorumOfReplicatorsController controller = newQuorum(nodeIds)) { GeneralizedReplicator replicator = controller.waitUntilAReplicatorIsReady(); ListenableFuture<List<ReplicateSubmissionInfo>> resultListFuture = resultFutureForNReplicateRequests(replicator, 3); assertThat(sequenceNumberListFuture(resultListFuture), resultsInAListOfLongsThat(hasSize(3))); assertThat(sequenceNumberListFuture(resultListFuture), resultsInAListOfLongsThat(isStrictlyIncreasing())); assertThat(resultListFuture, allRequestsWillComplete()); } } private Fiber newFiber(Consumer<Throwable> throwableHandler) { Fiber newFiber = fiberFactory.create(new ExceptionHandlingBatchExecutor(throwableHandler)); fibers.add(newFiber); return newFiber; } private List<ByteBuffer> someData() { return Lists.newArrayList(ReplicatorLogGenericTestUtil.someData()); } private QuorumOfReplicatorsController newQuorum(Collection<Long> nodeIds) throws Exception { return new QuorumOfReplicatorsController(nodeIds, baseTestPath, mainTestFiber, this::newFiber, jUnitFiberExceptionHandler); } private ListenableFuture<List<ReplicateSubmissionInfo>> resultFutureForNReplicateRequests( GeneralizedReplicator replicator, int numberOfReplicateRequests) throws Exception { List<ListenableFuture<ReplicateSubmissionInfo>> replicateFutures = new ArrayList<ListenableFuture<ReplicateSubmissionInfo>>() {{ for (int i = 0; i < numberOfReplicateRequests; i++) { add(replicator.replicate(someData())); } }}; return allAsList(replicateFutures); } private ListenableFuture<List<Long>> sequenceNumberListFuture(ListenableFuture<List<ReplicateSubmissionInfo>> resultListFuture) { return Futures.transform(resultListFuture, (List<ReplicateSubmissionInfo> resultList) -> resultList.stream() .map((result) -> result.sequenceNumber) .collect(Collectors.toList())); } private static Matcher<? super ListenableFuture<List<Long>>> resultsInAListOfLongsThat( Matcher<? super List<Long>> longsMatcher) { return resultsIn(longsMatcher); } private static Matcher<? super ListenableFuture<List<ReplicateSubmissionInfo>>> allRequestsWillComplete() { return returnsAFutureWhoseResult( MiscMatchers.simpleMatcherForPredicate( (List<ReplicateSubmissionInfo> resultList) -> { for (ReplicateSubmissionInfo result : resultList) { if (!resultsIn(equalTo(null)).matches(result.completedFuture)) { return false; } } return true; }, (description) -> description.appendText("a list of ReplicateSubmissionInfo each indicating the " + "requests have completed") )); } /** * Runs a C5GeneralizedReplicationService and handles startup and disposal, * for the purpose of making tests more readable */ private static class SingleReplicatorController implements AutoCloseable { private static final String QUORUM_ID = "quorumId"; public final C5GeneralizedReplicationService service; public final GeneralizedReplicator replicator; private final SimpleModuleInformationProvider moduleInfo; private final ReplicationModule replicationModule; private final LogModule logModule; private final DiscoveryModule nodeInfoModule; public SingleReplicatorController(int port, long nodeId, Collection<Long> peerIds, Path baseTestPath, Fiber testFiber, FiberSupplier fiberSupplier, Consumer<Throwable> exceptionHandler, EventLoopGroup bossGroup, EventLoopGroup workerGroup) throws Exception { moduleInfo = new SimpleModuleInformationProvider(testFiber, exceptionHandler); replicationModule = new ReplicatorService(bossGroup, workerGroup, nodeId, port, moduleInfo, fiberSupplier, new NioQuorumFileReaderWriter(baseTestPath)); logModule = new LogService(baseTestPath, fiberSupplier); nodeInfoModule = new BeaconService(nodeId, DISCOVERY_PORT, workerGroup, moduleInfo, fiberSupplier); startAll(); service = new C5GeneralizedReplicationService(replicationModule, logModule, fiberSupplier); replicator = service.createReplicator(QUORUM_ID, peerIds).get(); } @Override public void close() { service.dispose(); stopAll(); } private void startAll() throws Exception { List<ListenableFuture<Service.State>> startFutures = new ArrayList<>(); startFutures.add(moduleInfo.startModule(logModule)); startFutures.add(moduleInfo.startModule(nodeInfoModule)); startFutures.add(moduleInfo.startModule(replicationModule)); ListenableFuture<List<Service.State>> allFutures = allAsList(startFutures); // Block waiting for everything to start. allFutures.get(); } private void stopAll() { replicationModule.stopAndWait(); nodeInfoModule.stopAndWait(); logModule.stopAndWait(); } } /** * Runs a complete quorum of C5GeneralizedReplicationService and handles startup and disposal, * for the purpose of making tests more readable */ private static class QuorumOfReplicatorsController implements AutoCloseable { private final Collection<Long> peerIds; private final Path baseTestPath; private final Fiber testFiber; private final FiberSupplier fiberSupplier; private final Consumer<Throwable> exceptionHandler; private final EventLoopGroup bossGroup = new NioEventLoopGroup(NUMBER_OF_PROCESSORS / 3); private final EventLoopGroup workerGroup = new NioEventLoopGroup(NUMBER_OF_PROCESSORS / 3); private final Map<Long, SingleReplicatorController> controllers = new HashMap<>(); private final Map<Long, GeneralizedReplicator> replicators = new HashMap<>(); public QuorumOfReplicatorsController(Collection<Long> peerIds, Path baseTestPath, Fiber testFiber, FiberSupplier fiberSupplier, Consumer<Throwable> exceptionHandler) throws Exception { this.peerIds = peerIds; this.baseTestPath = baseTestPath; this.testFiber = testFiber; this.fiberSupplier = fiberSupplier; this.exceptionHandler = exceptionHandler; createControllersForEachPeerId(); } @Override public void close() { controllers.values().forEach(SingleReplicatorController::close); // Initiate shut down but don't wait for termination, for the sake of test speed. bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } public GeneralizedReplicator waitUntilAReplicatorIsReady() throws Exception { SettableFuture<GeneralizedReplicator> readyReplicatorFuture = SettableFuture.create(); for (GeneralizedReplicator replicator : replicators.values()) { final ListenableFuture<Void> isAvailableFuture = replicator.isAvailableFuture(); C5Futures.addCallback(isAvailableFuture, (ignore) -> readyReplicatorFuture.set(replicator), readyReplicatorFuture::setException, testFiber); } return readyReplicatorFuture.get(); } private void createControllersForEachPeerId() throws Exception { int port = ReplicatorConstants.REPLICATOR_PORT_MIN; for (long nodeId : peerIds) { SingleReplicatorController controller = new SingleReplicatorController(port, nodeId, peerIds, baseTestPath, testFiber, fiberSupplier, exceptionHandler, bossGroup, workerGroup); controllers.put(nodeId, controller); port++; replicators.put(nodeId, controller.replicator); } } } }