65b3da6e746ec3feca7d2411c92ac2ec41a00d0f
[controller.git] / opendaylight / md-sal / sal-akka-raft / src / test / java / org / opendaylight / controller / cluster / raft / behaviors / CandidateTest.java
1 /*
2  * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.controller.cluster.raft.behaviors;
10
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertTrue;
13 import akka.actor.ActorRef;
14 import akka.actor.Props;
15 import akka.testkit.TestActorRef;
16 import com.google.common.base.Stopwatch;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.TimeUnit;
23 import org.junit.After;
24 import org.junit.Before;
25 import org.junit.Test;
26 import org.mockito.Mockito;
27 import org.opendaylight.controller.cluster.NonPersistentDataProvider;
28 import org.opendaylight.controller.cluster.raft.DefaultConfigParamsImpl;
29 import org.opendaylight.controller.cluster.raft.ElectionTerm;
30 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
31 import org.opendaylight.controller.cluster.raft.RaftActorContext;
32 import org.opendaylight.controller.cluster.raft.RaftActorContextImpl;
33 import org.opendaylight.controller.cluster.raft.RaftState;
34 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
35 import org.opendaylight.controller.cluster.raft.VotingState;
36 import org.opendaylight.controller.cluster.raft.base.messages.ElectionTimeout;
37 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
38 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
39 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
40 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
41 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
42 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 public class CandidateTest extends AbstractRaftActorBehaviorTest<Candidate> {
47     static final Logger LOG = LoggerFactory.getLogger(CandidateTest.class);
48
49     private final TestActorRef<MessageCollectorActor> candidateActor = actorFactory.createTestActor(
50             Props.create(MessageCollectorActor.class), actorFactory.generateActorId("candidate"));
51
52     private TestActorRef<MessageCollectorActor>[] peerActors;
53
54     private RaftActorBehavior candidate;
55
56     @Before
57     public void setUp(){
58     }
59
60     @Override
61     @After
62     public void tearDown() throws Exception {
63         if(candidate != null) {
64             candidate.close();
65         }
66
67         super.tearDown();
68     }
69
70     @Test
71     public void testWhenACandidateIsCreatedItIncrementsTheCurrentTermAndVotesForItself(){
72         RaftActorContext raftActorContext = createActorContext();
73         long expectedTerm = raftActorContext.getTermInformation().getCurrentTerm();
74
75         candidate = new Candidate(raftActorContext);
76
77         assertEquals("getCurrentTerm", expectedTerm+1, raftActorContext.getTermInformation().getCurrentTerm());
78         assertEquals("getVotedFor", raftActorContext.getId(), raftActorContext.getTermInformation().getVotedFor());
79     }
80
81     @Test
82     public void testThatAnElectionTimeoutIsTriggered(){
83          MockRaftActorContext actorContext = createActorContext();
84          candidate = new Candidate(actorContext);
85
86          MessageCollectorActor.expectFirstMatching(candidateActor, ElectionTimeout.class,
87                  actorContext.getConfigParams().getElectionTimeOutInterval().$times(6).toMillis());
88     }
89
90     @Test
91     public void testHandleElectionTimeoutWhenThereAreZeroPeers(){
92         RaftActorContext raftActorContext = createActorContext();
93         candidate = new Candidate(raftActorContext);
94
95         RaftActorBehavior newBehavior =
96             candidate.handleMessage(candidateActor, ElectionTimeout.INSTANCE);
97
98         assertEquals("Behavior", RaftState.Leader, newBehavior.state());
99     }
100
101     @Test
102     public void testHandleElectionTimeoutWhenThereAreTwoNodeCluster(){
103         MockRaftActorContext raftActorContext = createActorContext();
104         raftActorContext.setPeerAddresses(setupPeers(1));
105         candidate = new Candidate(raftActorContext);
106
107         candidate = candidate.handleMessage(candidateActor, ElectionTimeout.INSTANCE);
108
109         assertEquals("Behavior", RaftState.Candidate, candidate.state());
110     }
111
112     @Test
113     public void testBecomeLeaderOnReceivingMajorityVotesInThreeNodeCluster(){
114         MockRaftActorContext raftActorContext = createActorContext();
115         raftActorContext.setLastApplied(raftActorContext.getReplicatedLog().lastIndex());
116         raftActorContext.setPeerAddresses(setupPeers(2));
117         candidate = new Candidate(raftActorContext);
118
119         candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, true));
120
121         assertEquals("Behavior", RaftState.Leader, candidate.state());
122     }
123
124     @Test
125     public void testBecomePreLeaderOnReceivingMajorityVotesInThreeNodeCluster(){
126         MockRaftActorContext raftActorContext = createActorContext();
127         raftActorContext.setLastApplied(-1);
128         raftActorContext.setPeerAddresses(setupPeers(2));
129         candidate = new Candidate(raftActorContext);
130
131         candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, true));
132
133         // LastApplied is -1 and behind the last index.
134         assertEquals("Behavior", RaftState.PreLeader, candidate.state());
135     }
136
137     @Test
138     public void testBecomeLeaderOnReceivingMajorityVotesInFiveNodeCluster(){
139         MockRaftActorContext raftActorContext = createActorContext();
140         raftActorContext.getTermInformation().update(2L, "other");
141         raftActorContext.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().
142                 createEntries(0, 5, 1).build());
143         raftActorContext.setCommitIndex(raftActorContext.getReplicatedLog().lastIndex());
144         raftActorContext.setLastApplied(raftActorContext.getReplicatedLog().lastIndex());
145         raftActorContext.setPeerAddresses(setupPeers(4));
146         candidate = new Candidate(raftActorContext);
147
148         RequestVote requestVote = MessageCollectorActor.expectFirstMatching(peerActors[0], RequestVote.class);
149         assertEquals("getTerm", 3L, requestVote.getTerm());
150         assertEquals("getCandidateId", raftActorContext.getId(), requestVote.getCandidateId());
151         assertEquals("getLastLogTerm", 1L, requestVote.getLastLogTerm());
152         assertEquals("getLastLogIndex", 4L, requestVote.getLastLogIndex());
153
154         MessageCollectorActor.expectFirstMatching(peerActors[1], RequestVote.class);
155         MessageCollectorActor.expectFirstMatching(peerActors[2], RequestVote.class);
156         MessageCollectorActor.expectFirstMatching(peerActors[3], RequestVote.class);
157
158         // First peers denies the vote.
159         candidate = candidate.handleMessage(peerActors[0], new RequestVoteReply(1, false));
160
161         assertEquals("Behavior", RaftState.Candidate, candidate.state());
162
163         candidate = candidate.handleMessage(peerActors[1], new RequestVoteReply(1, true));
164
165         assertEquals("Behavior", RaftState.Candidate, candidate.state());
166
167         candidate = candidate.handleMessage(peerActors[2], new RequestVoteReply(1, true));
168
169         assertEquals("Behavior", RaftState.Leader, candidate.state());
170     }
171
172     @Test
173     public void testBecomeLeaderOnReceivingMajorityVotesWithNonVotingPeers(){
174         ElectionTerm mockElectionTerm = Mockito.mock(ElectionTerm.class);
175         Mockito.doReturn(1L).when(mockElectionTerm).getCurrentTerm();
176         RaftActorContext raftActorContext = new RaftActorContextImpl(candidateActor, candidateActor.actorContext(),
177                 "candidate", mockElectionTerm, -1, -1, setupPeers(4), new DefaultConfigParamsImpl(),
178                 new NonPersistentDataProvider(), LOG);
179         raftActorContext.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().build());
180         raftActorContext.getPeerInfo("peer1").setVotingState(VotingState.NON_VOTING);
181         raftActorContext.getPeerInfo("peer4").setVotingState(VotingState.NON_VOTING);
182         candidate = new Candidate(raftActorContext);
183
184         MessageCollectorActor.expectFirstMatching(peerActors[1], RequestVote.class);
185         MessageCollectorActor.expectFirstMatching(peerActors[2], RequestVote.class);
186         MessageCollectorActor.assertNoneMatching(peerActors[0], RequestVote.class, 300);
187         MessageCollectorActor.assertNoneMatching(peerActors[3], RequestVote.class, 100);
188
189         candidate = candidate.handleMessage(peerActors[1], new RequestVoteReply(1, false));
190
191         assertEquals("Behavior", RaftState.Candidate, candidate.state());
192
193         candidate = candidate.handleMessage(peerActors[2], new RequestVoteReply(1, true));
194
195         assertEquals("Behavior", RaftState.Leader, candidate.state());
196     }
197
198     @Test
199     public void testResponseToHandleAppendEntriesWithLowerTerm() {
200         candidate = new Candidate(createActorContext());
201
202         setupPeers(1);
203         RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(1, "test", 0, 0,
204                 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
205
206         AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
207                 peerActors[0], AppendEntriesReply.class);
208         assertEquals("isSuccess", false, reply.isSuccess());
209         assertEquals("getTerm", 2, reply.getTerm());
210         assertTrue("New Behavior : " + newBehavior, newBehavior instanceof Candidate);
211     }
212
213     @Test
214     public void testResponseToHandleAppendEntriesWithHigherTerm() {
215         candidate = new Candidate(createActorContext());
216
217         setupPeers(1);
218         RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(5, "test", 0, 0,
219                 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
220
221         assertTrue("New Behavior : " + newBehavior, newBehavior instanceof Follower);
222     }
223
224     @Test
225     public void testResponseToHandleAppendEntriesWithEqualTerm() {
226         MockRaftActorContext actorContext = createActorContext();
227
228         candidate = new Candidate(actorContext);
229
230         setupPeers(1);
231         RaftActorBehavior newBehavior = candidate.handleMessage(peerActors[0], new AppendEntries(2, "test", 0, 0,
232                 Collections.<ReplicatedLogEntry>emptyList(), 0, -1, (short) 0));
233
234         assertTrue("New Behavior : " + newBehavior + " term = " + actorContext.getTermInformation().getCurrentTerm(),
235                 newBehavior instanceof Follower);
236     }
237
238
239     @Test
240     public void testResponseToRequestVoteWithLowerTerm() {
241         candidate = new Candidate(createActorContext());
242
243         setupPeers(1);
244         candidate.handleMessage(peerActors[0], new RequestVote(1, "test", 0, 0));
245
246         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
247                 peerActors[0], RequestVoteReply.class);
248         assertEquals("isVoteGranted", false, reply.isVoteGranted());
249         assertEquals("getTerm", 2, reply.getTerm());
250     }
251
252     @Test
253     public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForMatches() {
254         MockRaftActorContext context = createActorContext();
255         context.getTermInformation().update(1000, null);
256
257         // Once a candidate is created it will immediately increment the current term so after
258         // construction the currentTerm should be 1001
259         candidate = new Candidate(context);
260
261         setupPeers(1);
262         candidate.handleMessage(peerActors[0], new RequestVote(1001, context.getId(), 10000, 999));
263
264         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
265                 peerActors[0], RequestVoteReply.class);
266         assertEquals("isVoteGranted", true, reply.isVoteGranted());
267         assertEquals("getTerm", 1001, reply.getTerm());
268     }
269
270     @Test
271     public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForDoesNotMatch() {
272         MockRaftActorContext context = createActorContext();
273         context.getTermInformation().update(1000, null);
274
275         // Once a candidate is created it will immediately increment the current term so after
276         // construction the currentTerm should be 1001
277         candidate = new Candidate(context);
278
279         setupPeers(1);
280
281         // RequestVote candidate ID ("candidate2") does not match this candidate's votedFor
282         // (it votes for itself)
283         candidate.handleMessage(peerActors[0], new RequestVote(1001, "candidate2", 10000, 999));
284
285         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(
286                 peerActors[0], RequestVoteReply.class);
287         assertEquals("isVoteGranted", false, reply.isVoteGranted());
288         assertEquals("getTerm", 1001, reply.getTerm());
289     }
290
291     @Test
292     public void testCandidateSchedulesElectionTimeoutImmediatelyWhenItHasNoPeers(){
293         MockRaftActorContext context = createActorContext();
294
295         Stopwatch stopwatch = Stopwatch.createStarted();
296
297         candidate = createBehavior(context);
298
299         MessageCollectorActor.expectFirstMatching(candidateActor, ElectionTimeout.class);
300
301         long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
302
303         assertTrue(elapsed < context.getConfigParams().getElectionTimeOutInterval().toMillis());
304     }
305
306     @Test
307     @Override
308     public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
309         MockRaftActorContext context = createActorContext();
310
311         context.getTermInformation().update(2, "test");
312
313         // Prepare the receivers log
314         MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
315         setLastLogEntry(context, 2, 0, payload);
316
317         List<ReplicatedLogEntry> entries = new ArrayList<>();
318         entries.add(new MockRaftActorContext.MockReplicatedLogEntry(2, 0, payload));
319
320         AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
321
322         behavior = createBehavior(context);
323
324         // Resetting the Candidates term to make sure it will match
325         // the term sent by AppendEntries. If this was not done then
326         // the test will fail because the Candidate will assume that
327         // the message was sent to it from a lower term peer and will
328         // thus respond with a failure
329         context.getTermInformation().update(2, "test");
330
331         // Send an unknown message so that the state of the RaftActor remains unchanged
332         behavior.handleMessage(candidateActor, "unknown");
333
334         RaftActorBehavior raftBehavior = behavior.handleMessage(candidateActor, appendEntries);
335
336         assertEquals("Raft state", RaftState.Follower, raftBehavior.state());
337
338         assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
339
340         handleAppendEntriesAddSameEntryToLogReply(candidateActor);
341     }
342
343     @Override
344     protected Candidate createBehavior(final RaftActorContext actorContext) {
345         return new Candidate(actorContext);
346     }
347
348     @Override protected MockRaftActorContext createActorContext() {
349         return new MockRaftActorContext("candidate", getSystem(), candidateActor);
350     }
351
352     @SuppressWarnings("unchecked")
353     private Map<String, String> setupPeers(final int count) {
354         Map<String, String> peerMap = new HashMap<>();
355         peerActors = new TestActorRef[count];
356         for(int i = 0; i < count; i++) {
357             peerActors[i] = actorFactory.createTestActor(Props.create(MessageCollectorActor.class),
358                     actorFactory.generateActorId("peer"));
359             peerMap.put("peer" + (i+1), peerActors[i].path().toString());
360         }
361
362         return peerMap;
363     }
364
365     @Override
366     protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(final MockRaftActorContext actorContext,
367             final ActorRef actorRef, final RaftRPC rpc) throws Exception {
368         super.assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, actorRef, rpc);
369         if(rpc instanceof RequestVote) {
370             assertEquals("New votedFor", ((RequestVote)rpc).getCandidateId(), actorContext.getTermInformation().getVotedFor());
371         } else {
372             assertEquals("New votedFor", null, actorContext.getTermInformation().getVotedFor());
373         }
374     }
375 }