c5db.replication.InRamTest.java Source code

Java tutorial

Introduction

Here is the source code for c5db.replication.InRamTest.java

Source

/*
 * 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.interfaces.replication.IndexCommitNotice;
import c5db.interfaces.replication.QuorumConfiguration;
import c5db.interfaces.replication.Replicator;
import c5db.interfaces.replication.ReplicatorInstanceEvent;
import c5db.interfaces.replication.ReplicatorLog;
import c5db.interfaces.replication.ReplicatorReceipt;
import c5db.replication.rpc.RpcMessage;
import c5db.replication.rpc.RpcRequest;
import c5db.replication.rpc.RpcWireReply;
import c5db.util.CheckedConsumer;
import c5db.util.ExceptionHandlingBatchExecutor;
import c5db.util.JUnitRuleFiberExceptions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture;
import io.netty.util.CharsetUtil;
import org.hamcrest.Matcher;
import org.jetlang.channels.MemoryChannel;
import org.jetlang.channels.Request;
import org.jetlang.core.BatchExecutor;
import org.jetlang.core.RunnableExecutor;
import org.jetlang.core.RunnableExecutorImpl;
import org.jetlang.fibers.Fiber;
import org.jetlang.fibers.ThreadFiber;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static c5db.AsyncChannelAsserts.ChannelHistoryMonitor;
import static c5db.CollectionMatchers.isIn;
import static c5db.FutureMatchers.resultsIn;
import static c5db.IndexCommitMatcher.aCommitNotice;
import static c5db.RpcMatchers.ReplyMatcher.aPreElectionReply;
import static c5db.RpcMatchers.ReplyMatcher.anAppendReply;
import static c5db.RpcMatchers.RequestMatcher;
import static c5db.RpcMatchers.RequestMatcher.aPreElectionPoll;
import static c5db.RpcMatchers.RequestMatcher.anAppendRequest;
import static c5db.RpcMatchers.containsQuorumConfiguration;
import static c5db.interfaces.replication.Replicator.State.FOLLOWER;
import static c5db.interfaces.replication.ReplicatorInstanceEvent.EventType.ELECTION_TIMEOUT;
import static c5db.replication.ReplicationMatchers.aQuorumChangeCommittedEvent;
import static c5db.replication.ReplicationMatchers.aReplicatorEvent;
import static c5db.replication.ReplicationMatchers.hasCommittedEntriesUpTo;
import static c5db.replication.ReplicationMatchers.leaderElectedEvent;
import static c5db.replication.ReplicationMatchers.theLeader;
import static c5db.replication.ReplicationMatchers.willCommitConfiguration;
import static c5db.replication.ReplicationMatchers.willCommitEntriesUpTo;
import static c5db.replication.ReplicationMatchers.willRespondToAnAppendRequest;
import static c5db.replication.ReplicationMatchers.willSend;
import static c5db.replication.ReplicationMatchers.wonAnElectionWithTerm;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.any;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.core.IsNot.not;

/**
 * A class for tests of the behavior of multiple interacting ReplicatorInstance nodes,
 */
public class InRamTest {
    private static final int ELECTION_TIMEOUT_MILLIS = 50; // election timeout (milliseconds)
    private static final long OFFSET_STAGGERING_MILLIS = 50; // offset between different peers' clocks

    @Rule
    public JUnitRuleFiberExceptions fiberExceptionHandler = new JUnitRuleFiberExceptions();

    private final BatchExecutor batchExecutor = new ExceptionHandlingBatchExecutor(fiberExceptionHandler);
    private final RunnableExecutor runnableExecutor = new RunnableExecutorImpl(batchExecutor);
    private final Fiber fiber = new ThreadFiber(runnableExecutor, "InRamTest-ThreadFiber", true);
    private InRamSim sim;

    private ChannelHistoryMonitor<IndexCommitNotice> commitMonitor;
    private ChannelHistoryMonitor<ReplicatorInstanceEvent> eventMonitor;
    private ChannelHistoryMonitor<RpcMessage> replyMonitor;
    private long lastIndexLogged;

    private final MemoryChannel<Request<RpcRequest, RpcWireReply>> requestLog = new MemoryChannel<>();
    private final ChannelHistoryMonitor<Request<RpcRequest, RpcWireReply>> requestMonitor = new ChannelHistoryMonitor<>(
            requestLog, fiber);

    private final Map<Long, Long> lastCommit = new HashMap<>();

    @Before
    public final void setUpSimulationAndFibers() throws Exception {
        sim = new InRamSim(ELECTION_TIMEOUT_MILLIS, OFFSET_STAGGERING_MILLIS, batchExecutor);

        sim.getRpcChannel().subscribe(fiber, requestLog::publish);
        sim.getCommitNotices().subscribe(fiber, this::updateLastCommit);
        sim.getCommitNotices().subscribe(fiber, System.out::println);
        sim.getEventChannel().subscribe(fiber, System.out::println);
        commitMonitor = new ChannelHistoryMonitor<>(sim.getCommitNotices(), fiber);
        eventMonitor = new ChannelHistoryMonitor<>(sim.getEventChannel(), fiber);
        replyMonitor = new ChannelHistoryMonitor<>(sim.getReplyChannel(), fiber);

        fiber.start();
        sim.start(initialPeerSet());
    }

    @After
    public final void disposeResources() {
        sim.dispose();
        fiber.dispose();
    }

    @Test
    public void aLeaderWillBeElectedInATimelyMannerInANewQuorum() throws Exception {
        waitForALeader(term(1));
    }

    @Test
    public void aNewLeaderWillBeElectedIfAnExistingLeaderDies() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        leader().die();
        waitForANewLeader();
    }

    @Test
    public void ifAKilledLeaderIsRestartedItWillBecomeAFollower() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        LeaderController firstLeader = leader();
        firstLeader.die();

        waitForANewLeader();
        assertThat(leader(), is(not(equalTo(firstLeader))));

        firstLeader.restart();
        assertThat(firstLeader, willRespondToAnAppendRequest(currentTerm()));
    }

    @Test
    public void ifAnElectionOccursWhileAPeerIsOfflineThenThePeerWillRecognizeTheNewLeaderWhenThePeerRestarts()
            throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        LeaderController firstLeader = leader();
        PeerController follower = pickFollower().die();

        leader().log(someData()).waitForCommit(index(1)).die();

        waitForANewLeader();
        assertThat(leader(), not(equalTo(firstLeader)));

        // The second leader logs some entry, then the first node to go offline comes back
        leader().log(someData());
        follower.restart().waitForCommit(index(2));

        leader().log(someData());
        firstLeader.restart();

        allPeers((peer) -> assertThat(peer, willCommitEntriesUpTo(index(3))));
    }

    @Test
    public void aFollowerMaintainsItsCommitIndexWhenItBecomesLeader() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        leader().log(someData());
        allPeers((peer) -> peer.waitForCommit(lastIndexLogged()));

        // Kill the first leader; wait for a second leader to come to power
        leader().die();
        waitForANewLeader();
        leader().log(someData());

        assertThat(leader(), willSend(anAppendRequest().withCommitIndex(equalTo(lastIndexLogged()))));
    }

    @Test
    public void aLeaderSendsDataToAllOtherPeersResultingInAllPeersCommitting() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));
        leader().log(someData());

        allPeers((peer) -> assertThat(peer, willCommitEntriesUpTo(lastIndexLogged())));
    }

    @Test
    public void aFollowerWillStageANewElectionIfItTimesOutWaitingToHearFromTheLeader() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));
        final long firstLeaderTerm = currentTerm();

        PeerController follower = pickFollower();

        follower.willDropIncomingAppendsUntil(leader(), is(not(theLeader())));
        follower.allowToTimeout();

        waitForALeader(firstLeaderTerm + 1);
        assertThat(follower, anyOf(is(theLeader()), willRespondToAnAppendRequest(currentTerm())));
    }

    @Test
    public void ifAFollowerFallsBehindInReceivingAndLoggingEntriesItIsAbleToCatchUp() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        PeerController follower = pickFollower();
        follower.willDropIncomingAppendsUntil(leader(), hasCommittedEntriesUpTo(index(3)));

        leader().log(someData()).log(someData());
        assertThat(follower, willCommitEntriesUpTo(index(3)));
    }

    @Test
    public void aReplicatorReturnsNullIfAskedToChangeQuorumsWhenItIsNotInTheLeaderState() throws Exception {
        final Set<Long> newPeerIds = smallerPeerSetWithOneInCommonWithInitialSet();
        havingElectedALeaderAtOrAfter(term(1));

        assertThat(pickFollower().changeQuorum(newPeerIds), nullValue());
    }

    @Test
    public void aLeaderCanCoordinateAQuorumMembershipChange() throws Exception {
        final Set<Long> newPeerIds = smallerPeerSetWithNoneInCommonWithInitialSet();
        final QuorumConfiguration finalConfig = QuorumConfiguration.of(newPeerIds);

        havingElectedALeaderAtOrAfter(term(1));

        leader().changeQuorum(newPeerIds);
        sim.createAndStartReplicators(newPeerIds);

        waitForANewLeader();
        leader().log(someData());

        peers(newPeerIds).forEach((peer) -> assertThat(peer, willCommitConfiguration(finalConfig)));
        assertThat(newPeerIds, hasItem(equalTo(leader().id)));
    }

    @Test
    public void aSecondQuorumChangeWillOverrideTheFirst() throws Exception {
        final Set<Long> firstPeerSet = smallerPeerSetWithOneInCommonWithInitialSet();
        final Set<Long> secondPeerSet = largerPeerSetWithSomeInCommonWithInitialSet();

        havingElectedALeaderAtOrAfter(term(1));

        leader().changeQuorum(firstPeerSet);
        sim.createAndStartReplicators(firstPeerSet);

        leader().changeQuorum(secondPeerSet);
        sim.createAndStartReplicators(secondPeerSet);

        waitForALeaderWithId(isIn(secondPeerSet));
        leader().log(someData());

        peers(secondPeerSet).forEach(
                (peer) -> assertThat(peer, willCommitConfiguration(QuorumConfiguration.of(secondPeerSet))));
    }

    @Test
    public void theFutureReturnedByAQuorumChangeRequestWillReturnTheReceiptOfTheTransitionalConfigurationEntry()
            throws Exception {
        final Set<Long> newPeerIds = smallerPeerSetWithOneInCommonWithInitialSet();
        final long lastIndexBeforeQuorumChange = 4;

        havingElectedALeaderAtOrAfter(term(1));
        final long electionTerm = currentTerm();

        sim.createAndStartReplicators(newPeerIds);
        leader().logDataUpToIndex(lastIndexBeforeQuorumChange);

        assertThat(leader().changeQuorum(newPeerIds),
                resultsIn(equalTo(new ReplicatorReceipt(electionTerm, lastIndexBeforeQuorumChange + 1))));
    }

    @Test
    public void aQuorumChangeWillGoThroughEvenIfTheLeaderDiesBeforeItCommitsTheTransitionalConfiguration()
            throws Exception {
        // Leader dies before it can commit the transitional configuration, but as long as the next leader
        // has already received the transitional configuration entry, it can complete the view change.

        final Set<Long> newPeerIds = smallerPeerSetWithNoneInCommonWithInitialSet();
        final QuorumConfiguration transitionalConfig = QuorumConfiguration.of(initialPeerSet())
                .getTransitionalConfiguration(newPeerIds);

        havingElectedALeaderAtOrAfter(term(1));
        final long nextLogIndex = leader().log.getLastIndex() + 1;
        leader().changeQuorum(newPeerIds);

        allPeersExceptLeader((peer) -> assertThat(leader(),
                willSend(anAppendRequest().containingQuorumConfig(transitionalConfig).to(peer.id))));

        ignoringPreviousReplies();
        allPeers((peer) -> peer.waitForAppendReply(greaterThanOrEqualTo(currentTerm())));
        assertThat(leader().hasCommittedEntriesUpTo(nextLogIndex), is(false));

        // As of this point, all peers have replicated the transitional config, but the leader has not committed.
        // It would be impossible to commit because the new peers have not come online, and their votes are
        // necessary to commit the transitional configuration.

        leader().die();
        sim.createAndStartReplicators(newPeerIds);
        waitForANewLeader();
        assertThat(leader().currentConfiguration(), equalTo(transitionalConfig));

        // Necessary to log again because the new leader may not commit an entry from a past term (such as
        // the configuration entry) until it has also committed an entry from its current term.
        leader().log(someData());

        peers(newPeerIds)
                .forEach((peer) -> assertThat(peer, willCommitConfiguration(QuorumConfiguration.of(newPeerIds))));
    }

    @Test
    public void afterAQuorumChangeTheNewNodesWillCatchUpToThePreexistingOnes() throws Exception {
        final Set<Long> newPeerIds = largerPeerSetWithSomeInCommonWithInitialSet();
        final long maximumIndex = 5;

        havingElectedALeaderAtOrAfter(term(1));

        leader().logDataUpToIndex(maximumIndex).waitForCommit(maximumIndex);

        sim.createAndStartReplicators(newPeerIds);
        leader().changeQuorum(newPeerIds);

        peers(newPeerIds).forEach((peer) -> {
            assertThat(peer, willCommitEntriesUpTo(maximumIndex));
            assertThat(peer, willCommitConfiguration(QuorumConfiguration.of(newPeerIds)));
        });
    }

    @Test
    public void aQuorumCanMakeProgressEvenIfAFollowerCanSendRequestsButNotReceiveReplies() throws Exception {
        final long maximumIndex = 5;
        havingElectedALeaderAtOrAfter(term(1));

        pickFollower().willDropAllIncomingTraffic().allowToTimeout();

        waitForAnElectionTimeout();

        leader().logDataUpToIndex(maximumIndex);
        assertThat(leader(), willCommitEntriesUpTo(maximumIndex));
    }

    @Test
    public void aQuorumChangeCanCompleteEvenIfARemovedPeerTimesOutDuringIt() throws Exception {
        final Set<Long> newPeerIds = smallerPeerSetWithNoneInCommonWithInitialSet();
        final QuorumConfiguration transitionalConfig = QuorumConfiguration.of(initialPeerSet())
                .getTransitionalConfiguration(newPeerIds);
        final QuorumConfiguration finalConfig = transitionalConfig.getCompletedConfiguration();

        havingElectedALeaderAtOrAfter(term(1));
        final long firstLeaderTerm = currentTerm();
        final long leaderId = currentLeader();

        dropAllAppendsWithThisConfigurationUntilAPreElectionPollTakesPlace(finalConfig);

        leader().changeQuorum(newPeerIds);
        sim.createAndStartReplicators(newPeerIds);

        allPeersExceptLeader((peer) -> assertThat(leader(),
                willSend(anAppendRequest().containingQuorumConfig(transitionalConfig).to(peer.id))));

        peers(newPeerIds).forEach((peer) -> assertThat(peer, willCommitConfiguration(transitionalConfig)));

        peersBeingRemoved(transitionalConfig).forEach(PeerController::allowToTimeout);
        waitForAnElectionTimeout();

        peersBeingRemoved(transitionalConfig).forEach((peer) -> {
            if (peer.id != leaderId) {
                assertThat(peer, willSend(aPreElectionPoll()));
            }
        });

        waitForALeaderWithId(isIn(newPeerIds));
        leader().log(someData());

        peers(newPeerIds).forEach((peer) -> assertThat(peer, willCommitConfiguration(finalConfig)));

        peersBeingRemoved(transitionalConfig)
                .forEach((peer) -> assertThat(peer, not(wonAnElectionWithTerm(greaterThan(firstLeaderTerm)))));
    }

    @Test
    public void aQuorumCanElectANewLeaderEvenWhileReceivingMessagesFromRemovedPeersWhoHaveTimedOut()
            throws Exception {
        final Set<Long> newPeerIds = smallerPeerSetWithNoneInCommonWithInitialSet();
        final QuorumConfiguration transitionalConfig = QuorumConfiguration.of(initialPeerSet())
                .getTransitionalConfiguration(newPeerIds);
        final QuorumConfiguration finalConfig = transitionalConfig.getCompletedConfiguration();

        havingElectedALeaderAtOrAfter(term(1));
        long firstLeaderTerm = currentTerm();

        leader().changeQuorum(newPeerIds);
        sim.createAndStartReplicators(newPeerIds);

        peers(newPeerIds).forEach((peer) -> assertThat(peer, willCommitConfiguration(finalConfig)));

        peersBeingRemoved(transitionalConfig).forEach(PeerController::allowToTimeout);
        waitForAnElectionTimeout();

        waitForALeader(term(firstLeaderTerm + 1));

        leader().die();

        waitForANewLeader();
    }

    @Test
    public void aLateBootstrapCallWillBeDisregarded() throws Exception {
        havingElectedALeaderAtOrAfter(term(1));

        leader().logDataUpToIndex(2);
        allPeers((peer) -> assertThat(peer, willCommitEntriesUpTo(lastIndexLogged())));

        // Bootstrap calls to both leader and a non-leader -- both will be no-ops
        pickNonLeader().instance.bootstrapQuorum(smallerPeerSetWithNoneInCommonWithInitialSet());
        leader().instance.bootstrapQuorum(smallerPeerSetWithNoneInCommonWithInitialSet());

        // Verify that quorum is still in a working state
        assertThat(sim.getLog(leader().id).getLastIndex(), is(equalTo(2L)));

        leader().logDataUpToIndex(3);
        allPeers((peer) -> assertThat(peer, willCommitEntriesUpTo(lastIndexLogged())));
    }

    /**
     * Private methods
     */

    // Blocks until a leader is elected during some term >= minimumTerm.
    // Throws TimeoutException if that does not occur within the time limit.
    private void waitForALeader(long minimumTerm) {
        waitForALeaderElectedEventMatching(anyLeader(), greaterThanOrEqualTo(minimumTerm));
    }

    private void waitForALeaderWithId(Matcher<Long> leaderIdMatcher) {
        waitForALeaderElectedEventMatching(leaderIdMatcher, anyTerm());
    }

    private void waitForALeaderElectedEventMatching(Matcher<Long> leaderIdMatcher, Matcher<Long> termMatcher) {
        sim.startAllTimeouts();
        eventMonitor.waitFor(leaderElectedEvent(leaderIdMatcher, termMatcher));
        sim.stopAllTimeouts();

        // Wait for at least one other node to recognize the new leader. This is necessary because
        // some tests want to be able to identify a follower right away.
        pickNonLeader().waitForAppendReply(termMatcher);

        final long leaderId = currentLeader();
        assertThat(leaderCount(), is(equalTo(1)));

        sim.startTimeout(leaderId);
    }

    private void havingElectedALeaderAtOrAfter(long minimumTerm) {
        waitForALeader(minimumTerm);
    }

    private void waitForANewLeader() {
        waitForALeader(currentTerm() + 1);
    }

    // Counts leaders in the current term. Used to verify a sensible state.
    // If the simulation is running correctly, this should only ever return 0 or 1.
    private int leaderCount() {
        final long currentTerm = currentTerm();
        int leaderCount = 0;

        for (long id : sim.getOnlinePeers()) {
            if (peer(id).hasWonAnElection(greaterThanOrEqualTo(currentTerm))) {
                leaderCount++;
            }
        }
        return leaderCount;
    }

    private void waitForAnElectionTimeout() {
        eventMonitor.waitFor(aReplicatorEvent(ELECTION_TIMEOUT));
    }

    private void ignoringPreviousReplies() {
        replyMonitor.forgetHistory();
    }

    private static List<ByteBuffer> someData() {
        return Lists.newArrayList(ByteBuffer.wrap("test".getBytes(CharsetUtil.UTF_8)));
    }

    private LeaderController leader() {
        return new LeaderController();
    }

    private long lastIndexLogged() {
        return lastIndexLogged;
    }

    // Syntactic sugar for manipulating leaders

    class LeaderController extends PeerController {
        public LeaderController() {
            super(currentLeader());
        }

        public LeaderController log(List<ByteBuffer> buffers) throws Exception {
            lastIndexLogged = currentLeaderInstance().logData(buffers).get().seqNum;
            return this;
        }

        public LeaderController logDataUpToIndex(long index) throws Exception {
            while (lastIndexLogged < index) {
                log(someData());
            }
            return this;
        }

        private ReplicatorInstance currentLeaderInstance() {
            return sim.getReplicators().get(currentLeader());
        }
    }

    // Syntactic sugar for manipulating peers
    class PeerController {
        public final long id;
        public final ReplicatorLog log;
        public final ReplicatorInstance instance;

        public PeerController(long id) {
            this.id = id;
            this.log = sim.getLog(id);
            this.instance = sim.getReplicators().get(id);
        }

        @Override
        public String toString() {
            return instance.toString();
        }

        public boolean isCurrentLeader() {
            return id == currentLeader();
        }

        public QuorumConfiguration currentConfiguration() throws Exception {
            return instance.getQuorumConfiguration().get();
        }

        public ListenableFuture<ReplicatorReceipt> changeQuorum(Collection<Long> newPeerIds) throws Exception {
            return instance.changeQuorum(newPeerIds);
        }

        public PeerController die() {
            sim.killPeer(id);
            return this;
        }

        public PeerController restart() {
            sim.restartPeer(id);
            return this;
        }

        public boolean isOnline() {
            return !sim.getOfflinePeers().contains(id);
        }

        public PeerController allowToTimeout() {
            sim.startTimeout(id);
            return this;
        }

        public PeerController waitForCommit(long commitIndex) {
            commitMonitor.waitFor(aCommitNotice().withIndex(greaterThanOrEqualTo(commitIndex)).issuedFromPeer(id));
            return this;
        }

        public PeerController waitForQuorumCommit(QuorumConfiguration quorumConfiguration) {
            eventMonitor.waitFor(aQuorumChangeCommittedEvent(quorumConfiguration, equalTo(id)));
            return this;
        }

        public PeerController waitForAppendReply(Matcher<Long> termMatcher) {
            replyMonitor.waitFor(anAppendReply().withTerm(termMatcher));
            return this;
        }

        public PeerController waitForRequest(RequestMatcher requestMatcher) {
            requestMonitor.waitFor(requestMatcher.from(id));
            return this;
        }

        public boolean hasCommittedEntriesUpTo(long index) {
            return commitMonitor.hasAny(aCommitNotice().withIndex(greaterThanOrEqualTo(index)).issuedFromPeer(id));
        }

        public boolean hasWonAnElection(Matcher<Long> termMatcher) {
            return eventMonitor.hasAny(leaderElectedEvent(equalTo(id), termMatcher));
        }

        public void willDropIncomingAppendsUntil(PeerController peer, Matcher<PeerController> matcher) {
            sim.dropMessages((message) -> message.to == id && message.isAppendMessage(),
                    (message) -> matcher.matches(peer));
        }

        public PeerController willDropAllIncomingTraffic() {
            sim.dropMessages((message) -> (message.to == id) && (message.to != message.from), (message) -> false);
            return this;
        }
    }

    private PeerController peer(long peerId) {
        return new PeerController(peerId);
    }

    private Set<PeerController> peers(Collection<Long> peerIds) {
        return peerIds.stream().map(this::peer).collect(Collectors.toSet());
    }

    private <Ex extends Throwable> void allPeers(CheckedConsumer<PeerController, Ex> forEach) throws Ex {
        for (long peerId : sim.getOnlinePeers()) {
            forEach.accept(new PeerController(peerId));
        }
    }

    private <Ex extends Throwable> void allPeersExceptLeader(CheckedConsumer<PeerController, Ex> forEach)
            throws Ex {
        for (long peerId : sim.getOnlinePeers()) {
            if (peerId == currentLeader()) {
                continue;
            }
            forEach.accept(new PeerController(peerId));
        }
    }

    private PeerController anyPeerSuchThat(Predicate<PeerController> predicate) {
        for (long peerId : sim.getOnlinePeers()) {
            if (predicate.test(peer(peerId))) {
                return peer(peerId);
            }
        }
        return null;
    }

    private Set<PeerController> peersBeingRemoved(QuorumConfiguration configuration) {
        assert configuration.isTransitional;

        return Sets.difference(configuration.prevPeers(), configuration.nextPeers()).stream()
                .map(PeerController::new).collect(Collectors.toSet());
    }

    private PeerController pickFollower() {
        PeerController chosenPeer = anyPeerSuchThat(
                (peer) -> currentState(peer.instance) == FOLLOWER && peer.isOnline());
        assertThat(chosenPeer, not(nullValue()));
        return chosenPeer;
    }

    private PeerController pickNonLeader() {
        PeerController chosenPeer = anyPeerSuchThat((peer) -> not(theLeader()).matches(peer) && peer.isOnline());
        assertThat(chosenPeer, not(nullValue()));
        return chosenPeer;
    }

    private long currentTerm() {
        return eventMonitor.getLatest(leaderElectedEvent(anyLeader(), anyTerm())).leaderElectedTerm;
    }

    private long currentLeader() {
        return eventMonitor.getLatest(leaderElectedEvent(anyLeader(), anyTerm())).newLeader;
    }

    private Replicator.State currentState(Replicator replicator) {
        long id = replicator.getId();
        Replicator.State currentState = sim.getStateMonitor(id).getLatest(any(Replicator.State.class));

        if (currentState == null) {
            // No State has been issued via the replicator's State channel, so assume FOLLOWER (default)
            return FOLLOWER;
        } else {
            return currentState;
        }
    }

    private static long term(long term) {
        return term;
    }

    private static long index(long index) {
        return index;
    }

    private static Matcher<Long> anyLeader() {
        return any(Long.class);
    }

    private static Matcher<Long> anyTerm() {
        return any(Long.class);
    }

    private void updateLastCommit(IndexCommitNotice notice) {
        long peerId = notice.nodeId;
        if (notice.lastIndex > lastCommit.getOrDefault(peerId, 0L)) {
            lastCommit.put(peerId, notice.lastIndex);
        }
    }

    private void dropAllAppendsWithThisConfigurationUntilAPreElectionPollTakesPlace(
            QuorumConfiguration configuration) {
        sim.dropMessages(
                (message) -> message.isAppendMessage()
                        && containsQuorumConfiguration(message.getAppendMessage().getEntriesList(), configuration),
                aPreElectionReply()::matches);
    }

    private static Set<Long> initialPeerSet() {
        return Sets.newHashSet(1L, 2L, 3L, 4L, 5L, 6L, 7L);
    }

    private static Set<Long> smallerPeerSetWithOneInCommonWithInitialSet() {
        return Sets.newHashSet(7L, 8L, 9L);
    }

    private static Set<Long> smallerPeerSetWithNoneInCommonWithInitialSet() {
        return Sets.newHashSet(8L, 9L, 10L);
    }

    private static Set<Long> largerPeerSetWithSomeInCommonWithInitialSet() {
        return Sets.newHashSet(4L, 5L, 6L, 7L, 8L, 9L, 10L);
    }
}