2 * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.controller.cluster.raft.behaviors;
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertTrue;
14 import akka.actor.ActorRef;
15 import akka.dispatch.Dispatchers;
16 import akka.testkit.TestActorRef;
17 import com.google.common.base.Stopwatch;
18 import com.google.common.util.concurrent.MoreExecutors;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.concurrent.TimeUnit;
25 import org.junit.After;
26 import org.junit.Before;
27 import org.junit.Test;
28 import org.mockito.Mockito;
29 import org.opendaylight.controller.cluster.NonPersistentDataProvider;
30 import org.opendaylight.controller.cluster.raft.DefaultConfigParamsImpl;
31 import org.opendaylight.controller.cluster.raft.ElectionTerm;
32 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
33 import org.opendaylight.controller.cluster.raft.RaftActorContext;
34 import org.opendaylight.controller.cluster.raft.RaftActorContextImpl;
35 import org.opendaylight.controller.cluster.raft.RaftState;
36 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
37 import org.opendaylight.controller.cluster.raft.VotingState;
38 import org.opendaylight.controller.cluster.raft.base.messages.ElectionTimeout;
39 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
40 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
41 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
42 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
43 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
44 import org.opendaylight.controller.cluster.raft.persisted.SimpleReplicatedLogEntry;
45 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 public class CandidateTest extends AbstractRaftActorBehaviorTest<Candidate> {
50 static final Logger LOG = LoggerFactory.getLogger(CandidateTest.class);
52 private final TestActorRef<MessageCollectorActor> candidateActor = actorFactory.createTestActor(
53 MessageCollectorActor.props().withDispatcher(Dispatchers.DefaultDispatcherId()),
54 actorFactory.generateActorId("candidate"));
56 private ActorRef[] peerActors;
58 private RaftActorBehavior candidate;
66 public void tearDown() {
67 if (candidate != null) {
75 public void testWhenACandidateIsCreatedItIncrementsTheCurrentTermAndVotesForItself() {
76 RaftActorContext raftActorContext = createActorContext();
77 long expectedTerm = raftActorContext.getTermInformation().getCurrentTerm();
79 candidate = new Candidate(raftActorContext);
81 assertEquals("getCurrentTerm", expectedTerm + 1, raftActorContext.getTermInformation().getCurrentTerm());
82 assertEquals("getVotedFor", raftActorContext.getId(), raftActorContext.getTermInformation().getVotedFor());
86 public void testThatAnElectionTimeoutIsTriggered() {
87 MockRaftActorContext actorContext = createActorContext();
88 candidate = new Candidate(actorContext);
90 MessageCollectorActor.expectFirstMatching(candidateActor, ElectionTimeout.class,
91 actorContext.getConfigParams().getElectionTimeOutInterval().$times(6).toMillis());
95 public void testHandleElectionTimeoutWhenThereAreZeroPeers() {
96 RaftActorContext raftActorContext = createActorContext();
97 candidate = new Candidate(raftActorContext);
99 RaftActorBehavior newBehavior =
100 candidate.handleMessage(candidateActor, ElectionTimeout.INSTANCE);
102 assertEquals("Behavior", RaftState.Leader, newBehavior.state());
106 public void testHandleElectionTimeoutWhenThereAreTwoNodeCluster() {
107 MockRaftActorContext raftActorContext = createActorContext();
108 raftActorContext.setPeerAddresses(setupPeers(1));
109 candidate = new Candidate(raftActorContext);
111 candidate = candidate.handleMessage(candidateActor, ElectionTimeout.INSTANCE);
113 assertEquals("Behavior", RaftState.Candidate, candidate.state());
117 public void testBecomeLeaderOnReceivingMajorityVotesInThreeNodeCluster() {
118 MockRaftActorContext raftActorContext = createActorContext();
119 raftActorContext.setLastApplied(raftActorContext.getReplicatedLog().lastIndex());
120 raftActorContext.setPeerAddresses(setupPeers(2));
121 candidate = new Candidate(raftActorContext);
123 candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, true));
125 assertEquals("Behavior", RaftState.Leader, candidate.state());
129 public void testBecomePreLeaderOnReceivingMajorityVotesInThreeNodeCluster() {
130 MockRaftActorContext raftActorContext = createActorContext();
131 raftActorContext.setLastApplied(-1);
132 raftActorContext.setPeerAddresses(setupPeers(2));
133 candidate = new Candidate(raftActorContext);
135 candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, true));
137 // LastApplied is -1 and behind the last index.
138 assertEquals("Behavior", RaftState.PreLeader, candidate.state());
142 public void testBecomeLeaderOnReceivingMajorityVotesInFiveNodeCluster() {
143 MockRaftActorContext raftActorContext = createActorContext();
144 raftActorContext.getTermInformation().update(2L, "other");
145 raftActorContext.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder()
146 .createEntries(0, 5, 1).build());
147 raftActorContext.setCommitIndex(raftActorContext.getReplicatedLog().lastIndex());
148 raftActorContext.setLastApplied(raftActorContext.getReplicatedLog().lastIndex());
149 raftActorContext.setPeerAddresses(setupPeers(4));
150 candidate = new Candidate(raftActorContext);
152 RequestVote requestVote = MessageCollectorActor.expectFirstMatching(peerActors[0], RequestVote.class);
153 assertEquals("getTerm", 3L, requestVote.getTerm());
154 assertEquals("getCandidateId", raftActorContext.getId(), requestVote.getCandidateId());
155 assertEquals("getLastLogTerm", 1L, requestVote.getLastLogTerm());
156 assertEquals("getLastLogIndex", 4L, requestVote.getLastLogIndex());
158 MessageCollectorActor.expectFirstMatching(peerActors[1], RequestVote.class);
159 MessageCollectorActor.expectFirstMatching(peerActors[2], RequestVote.class);
160 MessageCollectorActor.expectFirstMatching(peerActors[3], RequestVote.class);
162 // First peers denies the vote.
163 candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, false));
165 assertEquals("Behavior", RaftState.Candidate, candidate.state());
167 candidate = candidate.handleMessage(peerActors[1], new RequestVoteReply(1, true));
169 assertEquals("Behavior", RaftState.Candidate, candidate.state());
171 candidate = candidate.handleMessage(peerActors[2], new RequestVoteReply(1, true));
173 assertEquals("Behavior", RaftState.Leader, candidate.state());
177 public void testBecomeLeaderOnReceivingMajorityVotesWithNonVotingPeers() {
178 ElectionTerm mockElectionTerm = Mockito.mock(ElectionTerm.class);
179 Mockito.doReturn(1L).when(mockElectionTerm).getCurrentTerm();
180 RaftActorContext raftActorContext = new RaftActorContextImpl(candidateActor, candidateActor.actorContext(),
181 "candidate", mockElectionTerm, -1, -1, setupPeers(4), new DefaultConfigParamsImpl(),
182 new NonPersistentDataProvider(Runnable::run), applyState -> { }, LOG, MoreExecutors.directExecutor());
183 raftActorContext.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().build());
184 raftActorContext.getPeerInfo("peer1").setVotingState(VotingState.NON_VOTING);
185 raftActorContext.getPeerInfo("peer4").setVotingState(VotingState.NON_VOTING);
186 candidate = new Candidate(raftActorContext);
188 MessageCollectorActor.expectFirstMatching(peerActors[1], RequestVote.class);
189 MessageCollectorActor.expectFirstMatching(peerActors[2], RequestVote.class);
190 MessageCollectorActor.assertNoneMatching(peerActors[0], RequestVote.class, 300);
191 MessageCollectorActor.assertNoneMatching(peerActors[3], RequestVote.class, 100);
193 candidate = candidate.handleMessage(peerActors[1], new RequestVoteReply(1, false));
195 assertEquals("Behavior", RaftState.Candidate, candidate.state());
197 candidate = candidate.handleMessage(peerActors[2], new RequestVoteReply(1, true));
199 assertEquals("Behavior", RaftState.Leader, candidate.state());
203 public void testResponseToHandleAppendEntriesWithLowerTerm() {
204 candidate = new Candidate(createActorContext());
207 RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(1, "test", 0, 0,
208 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
210 AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
211 peerActors[0], AppendEntriesReply.class);
212 assertEquals("isSuccess", false, reply.isSuccess());
213 assertEquals("getTerm", 2, reply.getTerm());
214 assertTrue("New Behavior : " + newBehavior, newBehavior instanceof Candidate);
218 public void testResponseToHandleAppendEntriesWithHigherTerm() {
219 candidate = new Candidate(createActorContext());
222 RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(5, "test", 0, 0,
223 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
225 assertTrue("New Behavior : " + newBehavior, newBehavior instanceof Follower);
229 public void testResponseToHandleAppendEntriesWithEqualTerm() {
230 MockRaftActorContext actorContext = createActorContext();
232 candidate = new Candidate(actorContext);
235 RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(2, "test", 0, 0,
236 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
238 assertTrue("New Behavior : " + newBehavior + " term = " + actorContext.getTermInformation().getCurrentTerm(),
239 newBehavior instanceof Follower);
244 public void testResponseToRequestVoteWithLowerTerm() {
245 candidate = new Candidate(createActorContext());
248 candidate.handleMessage(peerActors[0], new RequestVote(1, "test", 0, 0));
250 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
251 peerActors[0], RequestVoteReply.class);
252 assertEquals("isVoteGranted", false, reply.isVoteGranted());
253 assertEquals("getTerm", 2, reply.getTerm());
257 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForMatches() {
258 MockRaftActorContext context = createActorContext();
259 context.getTermInformation().update(1000, null);
261 // Once a candidate is created it will immediately increment the current term so after
262 // construction the currentTerm should be 1001
263 candidate = new Candidate(context);
266 candidate.handleMessage(peerActors[0], new RequestVote(1001, context.getId(), 10000, 999));
268 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
269 peerActors[0], RequestVoteReply.class);
270 assertEquals("isVoteGranted", true, reply.isVoteGranted());
271 assertEquals("getTerm", 1001, reply.getTerm());
275 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForDoesNotMatch() {
276 MockRaftActorContext context = createActorContext();
277 context.getTermInformation().update(1000, null);
279 // Once a candidate is created it will immediately increment the current term so after
280 // construction the currentTerm should be 1001
281 candidate = new Candidate(context);
285 // RequestVote candidate ID ("candidate2") does not match this candidate's votedFor
286 // (it votes for itself)
287 candidate.handleMessage(peerActors[0], new RequestVote(1001, "candidate2", 10000, 999));
289 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
290 peerActors[0], RequestVoteReply.class);
291 assertEquals("isVoteGranted", false, reply.isVoteGranted());
292 assertEquals("getTerm", 1001, reply.getTerm());
296 public void testCandidateSchedulesElectionTimeoutImmediatelyWhenItHasNoPeers() {
297 MockRaftActorContext context = createActorContext();
299 Stopwatch stopwatch = Stopwatch.createStarted();
301 candidate = createBehavior(context);
303 MessageCollectorActor.expectFirstMatching(candidateActor, ElectionTimeout.class);
305 long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
307 assertTrue(elapsed < context.getConfigParams().getElectionTimeOutInterval().toMillis());
312 public void testHandleAppendEntriesAddSameEntryToLog() {
313 MockRaftActorContext context = createActorContext();
315 context.getTermInformation().update(2, "test");
317 // Prepare the receivers log
318 MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
319 setLastLogEntry(context, 2, 0, payload);
321 List<ReplicatedLogEntry> entries = new ArrayList<>();
322 entries.add(new SimpleReplicatedLogEntry(0, 2, payload));
324 final AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
326 behavior = createBehavior(context);
328 // Resetting the Candidates term to make sure it will match
329 // the term sent by AppendEntries. If this was not done then
330 // the test will fail because the Candidate will assume that
331 // the message was sent to it from a lower term peer and will
332 // thus respond with a failure
333 context.getTermInformation().update(2, "test");
335 // Send an unknown message so that the state of the RaftActor remains unchanged
336 behavior.handleMessage(candidateActor, "unknown");
338 RaftActorBehavior raftBehavior = behavior.handleMessage(candidateActor, appendEntries);
340 assertEquals("Raft state", RaftState.Follower, raftBehavior.state());
342 assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
344 handleAppendEntriesAddSameEntryToLogReply(candidateActor);
348 protected Candidate createBehavior(final RaftActorContext actorContext) {
349 return new Candidate(actorContext);
353 protected MockRaftActorContext createActorContext() {
354 return new MockRaftActorContext("candidate", getSystem(), candidateActor);
357 private Map<String, String> setupPeers(final int count) {
358 Map<String, String> peerMap = new HashMap<>();
359 peerActors = new ActorRef[count];
360 for (int i = 0; i < count; i++) {
361 peerActors[i] = actorFactory.createActor(MessageCollectorActor.props(),
362 actorFactory.generateActorId("peer"));
363 peerMap.put("peer" + (i + 1), peerActors[i].path().toString());
370 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(final MockRaftActorContext actorContext,
371 final ActorRef actorRef, final RaftRPC rpc) {
372 super.assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, actorRef, rpc);
373 if (rpc instanceof RequestVote) {
374 assertEquals("New votedFor", ((RequestVote)rpc).getCandidateId(),
375 actorContext.getTermInformation().getVotedFor());
377 assertEquals("New votedFor", null, actorContext.getTermInformation().getVotedFor());