677442402c61a6c928e000ded711819ddcca91eb
[controller.git] / opendaylight / md-sal / sal-akka-raft / src / test / java / org / opendaylight / controller / cluster / raft / behaviors / FollowerTest.java
1 package org.opendaylight.controller.cluster.raft.behaviors;
2
3 import akka.actor.ActorRef;
4 import akka.actor.Props;
5 import akka.testkit.JavaTestKit;
6 import junit.framework.Assert;
7 import org.junit.Test;
8 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
9 import org.opendaylight.controller.cluster.raft.RaftActorContext;
10 import org.opendaylight.controller.cluster.raft.RaftState;
11 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
12 import org.opendaylight.controller.cluster.raft.internal.messages.ElectionTimeout;
13 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
14 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
15 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
16 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
17 import org.opendaylight.controller.cluster.raft.utils.DoNothingActor;
18
19 import java.util.ArrayList;
20 import java.util.List;
21
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
24
25 public class FollowerTest extends AbstractRaftActorBehaviorTest {
26
27     private final ActorRef followerActor = getSystem().actorOf(Props.create(
28         DoNothingActor.class));
29
30
31     @Override protected RaftActorBehavior createBehavior(RaftActorContext actorContext) {
32         return new Follower(actorContext);
33     }
34
35     @Override protected RaftActorContext createActorContext() {
36         return new MockRaftActorContext("test", getSystem(), followerActor);
37     }
38
39     @Test
40     public void testThatAnElectionTimeoutIsTriggered(){
41         new JavaTestKit(getSystem()) {{
42
43             new Within(duration("1 seconds")) {
44                 protected void run() {
45
46                     Follower follower = new Follower(createActorContext(getTestActor()));
47
48                     final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "ElectionTimeout") {
49                         // do not put code outside this method, will run afterwards
50                         protected Boolean match(Object in) {
51                             if (in instanceof ElectionTimeout) {
52                                 return true;
53                             } else {
54                                 throw noMatch();
55                             }
56                         }
57                     }.get();
58
59                     assertEquals(true, out);
60                 }
61             };
62         }};
63     }
64
65     @Test
66     public void testHandleElectionTimeout(){
67         RaftActorContext raftActorContext = createActorContext();
68         Follower follower =
69             new Follower(raftActorContext);
70
71         RaftState raftState =
72             follower.handleMessage(followerActor, new ElectionTimeout());
73
74         Assert.assertEquals(RaftState.Candidate, raftState);
75     }
76
77     @Test
78     public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNull(){
79         new JavaTestKit(getSystem()) {{
80
81             new Within(duration("1 seconds")) {
82                 protected void run() {
83
84                     RaftActorContext context = createActorContext(getTestActor());
85
86                     context.getTermInformation().update(1000, null);
87
88                     RaftActorBehavior follower = createBehavior(context);
89
90                     follower.handleMessage(getTestActor(), new RequestVote(1000, "test", 10000, 999));
91
92                     final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
93                         // do not put code outside this method, will run afterwards
94                         protected Boolean match(Object in) {
95                             if (in instanceof RequestVoteReply) {
96                                 RequestVoteReply reply = (RequestVoteReply) in;
97                                 return reply.isVoteGranted();
98                             } else {
99                                 throw noMatch();
100                             }
101                         }
102                     }.get();
103
104                     assertEquals(true, out);
105                 }
106             };
107         }};
108     }
109
110     @Test
111     public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNotTheSameAsCandidateId(){
112         new JavaTestKit(getSystem()) {{
113
114             new Within(duration("1 seconds")) {
115                 protected void run() {
116
117                     RaftActorContext context = createActorContext(getTestActor());
118
119                     context.getTermInformation().update(1000, "test");
120
121                     RaftActorBehavior follower = createBehavior(context);
122
123                     follower.handleMessage(getTestActor(), new RequestVote(1000, "candidate", 10000, 999));
124
125                     final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
126                         // do not put code outside this method, will run afterwards
127                         protected Boolean match(Object in) {
128                             if (in instanceof RequestVoteReply) {
129                                 RequestVoteReply reply = (RequestVoteReply) in;
130                                 return reply.isVoteGranted();
131                             } else {
132                                 throw noMatch();
133                             }
134                         }
135                     }.get();
136
137                     assertEquals(false, out);
138                 }
139             };
140         }};
141     }
142
143     /**
144      * This test verifies that when an AppendEntries RPC is received by a RaftActor
145      * with a commitIndex that is greater than what has been applied to the
146      * state machine of the RaftActor, the RaftActor applies the state and
147      * sets it current applied state to the commitIndex of the sender.
148      *
149      * @throws Exception
150      */
151     @Test
152     public void testHandleAppendEntriesWithNewerCommitIndex() throws Exception {
153         new JavaTestKit(getSystem()) {{
154
155             RaftActorContext context =
156                 createActorContext();
157
158             context.setLastApplied(100);
159             setLastLogEntry((MockRaftActorContext) context, 0, 0, "");
160
161             // The new commitIndex is 101
162             AppendEntries appendEntries =
163                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
164
165             RaftState raftState =
166                 createBehavior(context).handleMessage(getRef(), appendEntries);
167
168             assertEquals(101L, context.getLastApplied());
169
170         }};
171     }
172
173     /**
174      * This test verifies that when an AppendEntries is received a specific prevLogTerm
175      * which does not match the term that is in RaftActors log entry at prevLogIndex
176      * then the RaftActor does not change it's state and it returns a failure.
177      *
178      * @throws Exception
179      */
180     @Test
181     public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
182         throws Exception {
183         new JavaTestKit(getSystem()) {{
184
185             MockRaftActorContext context = (MockRaftActorContext)
186                 createActorContext();
187
188             // First set the receivers term to lower number
189             context.getTermInformation().update(95, "test");
190
191             // Set the last log entry term for the receiver to be greater than
192             // what we will be sending as the prevLogTerm in AppendEntries
193             MockRaftActorContext.SimpleReplicatedLog mockReplicatedLog =
194                 setLastLogEntry(context, 20, 0, "");
195
196             // AppendEntries is now sent with a bigger term
197             // this will set the receivers term to be the same as the sender's term
198             AppendEntries appendEntries =
199                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
200
201             RaftActorBehavior behavior = createBehavior(context);
202
203             // Send an unknown message so that the state of the RaftActor remains unchanged
204             RaftState expected = behavior.handleMessage(getRef(), "unknown");
205
206             RaftState raftState =
207                 behavior.handleMessage(getRef(), appendEntries);
208
209             assertEquals(expected, raftState);
210
211             // Also expect an AppendEntriesReply to be sent where success is false
212             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
213                 "AppendEntriesReply") {
214                 // do not put code outside this method, will run afterwards
215                 protected Boolean match(Object in) {
216                     if (in instanceof AppendEntriesReply) {
217                         AppendEntriesReply reply = (AppendEntriesReply) in;
218                         return reply.isSuccess();
219                     } else {
220                         throw noMatch();
221                     }
222                 }
223             }.get();
224
225             assertEquals(false, out);
226
227
228         }};
229     }
230
231
232
233     /**
234      * This test verifies that when a new AppendEntries message is received with
235      * new entries and the logs of the sender and receiver match that the new
236      * entries get added to the log and the log is incremented by the number of
237      * entries received in appendEntries
238      *
239      * @throws Exception
240      */
241     @Test
242     public void testHandleAppendEntriesAddNewEntries() throws Exception {
243         new JavaTestKit(getSystem()) {{
244
245             MockRaftActorContext context = (MockRaftActorContext)
246                 createActorContext();
247
248             // First set the receivers term to lower number
249             context.getTermInformation().update(1, "test");
250
251             // Prepare the receivers log
252             MockRaftActorContext.SimpleReplicatedLog log =
253                 new MockRaftActorContext.SimpleReplicatedLog();
254             log.append(
255                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
256             log.append(
257                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
258             log.append(
259                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
260
261             context.setReplicatedLog(log);
262
263             // Prepare the entries to be sent with AppendEntries
264             List<ReplicatedLogEntry> entries = new ArrayList<>();
265             entries.add(
266                 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, "three"));
267             entries.add(
268                 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, "four"));
269
270             // Send appendEntries with the same term as was set on the receiver
271             // before the new behavior was created (1 in this case)
272             // This will not work for a Candidate because as soon as a Candidate
273             // is created it increments the term
274             AppendEntries appendEntries =
275                 new AppendEntries(1, "leader-1", 2, 1, entries, 4);
276
277             RaftActorBehavior behavior = createBehavior(context);
278
279             // Send an unknown message so that the state of the RaftActor remains unchanged
280             RaftState expected = behavior.handleMessage(getRef(), "unknown");
281
282             RaftState raftState =
283                 behavior.handleMessage(getRef(), appendEntries);
284
285             assertEquals(expected, raftState);
286             assertEquals(5, log.last().getIndex() + 1);
287             assertNotNull(log.get(3));
288             assertNotNull(log.get(4));
289
290             // Also expect an AppendEntriesReply to be sent where success is false
291             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
292                 "AppendEntriesReply") {
293                 // do not put code outside this method, will run afterwards
294                 protected Boolean match(Object in) {
295                     if (in instanceof AppendEntriesReply) {
296                         AppendEntriesReply reply = (AppendEntriesReply) in;
297                         return reply.isSuccess();
298                     } else {
299                         throw noMatch();
300                     }
301                 }
302             }.get();
303
304             assertEquals(true, out);
305
306
307         }};
308     }
309
310
311
312     /**
313      * This test verifies that when a new AppendEntries message is received with
314      * new entries and the logs of the sender and receiver are out-of-sync that
315      * the log is first corrected by removing the out of sync entries from the
316      * log and then adding in the new entries sent with the AppendEntries message
317      *
318      * @throws Exception
319      */
320     @Test
321     public void testHandleAppendEntriesCorrectReceiverLogEntries()
322         throws Exception {
323         new JavaTestKit(getSystem()) {{
324
325             MockRaftActorContext context = (MockRaftActorContext)
326                 createActorContext();
327
328             // First set the receivers term to lower number
329             context.getTermInformation().update(2, "test");
330
331             // Prepare the receivers log
332             MockRaftActorContext.SimpleReplicatedLog log =
333                 new MockRaftActorContext.SimpleReplicatedLog();
334             log.append(
335                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
336             log.append(
337                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
338             log.append(
339                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
340
341             context.setReplicatedLog(log);
342
343             // Prepare the entries to be sent with AppendEntries
344             List<ReplicatedLogEntry> entries = new ArrayList<>();
345             entries.add(
346                 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, "two-1"));
347             entries.add(
348                 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, "three"));
349
350             // Send appendEntries with the same term as was set on the receiver
351             // before the new behavior was created (1 in this case)
352             // This will not work for a Candidate because as soon as a Candidate
353             // is created it increments the term
354             AppendEntries appendEntries =
355                 new AppendEntries(2, "leader-1", 1, 1, entries, 3);
356
357             RaftActorBehavior behavior = createBehavior(context);
358
359             // Send an unknown message so that the state of the RaftActor remains unchanged
360             RaftState expected = behavior.handleMessage(getRef(), "unknown");
361
362             RaftState raftState =
363                 behavior.handleMessage(getRef(), appendEntries);
364
365             assertEquals(expected, raftState);
366
367             // The entry at index 2 will be found out-of-sync with the leader
368             // and will be removed
369             // Then the two new entries will be added to the log
370             // Thus making the log to have 4 entries
371             assertEquals(4, log.last().getIndex() + 1);
372             assertNotNull(log.get(2));
373
374             // Check that the entry at index 2 has the new data
375             assertEquals("two-1", log.get(2).getData());
376             assertNotNull(log.get(3));
377
378             // Also expect an AppendEntriesReply to be sent where success is false
379             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
380                 "AppendEntriesReply") {
381                 // do not put code outside this method, will run afterwards
382                 protected Boolean match(Object in) {
383                     if (in instanceof AppendEntriesReply) {
384                         AppendEntriesReply reply = (AppendEntriesReply) in;
385                         return reply.isSuccess();
386                     } else {
387                         throw noMatch();
388                     }
389                 }
390             }.get();
391
392             assertEquals(true, out);
393
394
395         }};
396     }
397
398 }