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