Implement handling of AppendEntries from a recipient perspective
[controller.git] / opendaylight / md-sal / sal-akka-raft / src / test / java / org / opendaylight / controller / cluster / raft / behaviors / AbstractRaftActorBehaviorTest.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 org.junit.Test;
7 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
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.messages.AppendEntries;
13 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
14 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
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 abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
26
27     private final ActorRef behaviorActor = getSystem().actorOf(Props.create(
28         DoNothingActor.class));
29
30     /**
31      * This test checks that when a new Raft RPC message is received with a newer
32      * term the RaftActor gets into the Follower state.
33      *
34      * @throws Exception
35      */
36     @Test
37     public void testHandleRaftRPCWithNewerTerm() throws Exception {
38         new JavaTestKit(getSystem()) {{
39
40             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
41                 createAppendEntriesWithNewerTerm());
42
43             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
44                 createAppendEntriesReplyWithNewerTerm());
45
46             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
47                 createRequestVoteWithNewerTerm());
48
49             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
50                 createRequestVoteReplyWithNewerTerm());
51
52
53         }};
54     }
55
56     /**
57      * This test verifies that when an AppendEntries RPC is received by a RaftActor
58      * with a commitIndex that is greater than what has been applied to the
59      * state machine of the RaftActor, the RaftActor applies the state and
60      * sets it current applied state to the commitIndex of the sender.
61      *
62      * @throws Exception
63      */
64     @Test
65     public void testHandleAppendEntriesWithNewerCommitIndex() throws Exception {
66         new JavaTestKit(getSystem()) {{
67
68             RaftActorContext context =
69                 createActorContext();
70
71             context.setLastApplied(100);
72             setLastLogEntry((MockRaftActorContext) context, 0, 0, "");
73
74             // The new commitIndex is 101
75             AppendEntries appendEntries =
76                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
77
78             RaftState raftState =
79                 createBehavior(context).handleMessage(getRef(), appendEntries);
80
81             assertEquals(101L, context.getLastApplied());
82
83         }};
84     }
85
86     /**
87      * This test verifies that when an AppendEntries is received with a term that
88      * is less that the currentTerm of the RaftActor then the RaftActor does not
89      * change it's state and it responds back with a failure
90      *
91      * @throws Exception
92      */
93     @Test
94     public void testHandleAppendEntriesSenderTermLessThanReceiverTerm()
95         throws Exception {
96         new JavaTestKit(getSystem()) {{
97
98             MockRaftActorContext context = (MockRaftActorContext)
99                 createActorContext();
100
101             // First set the receivers term to a high number (1000)
102             context.getTermInformation().update(1000, "test");
103
104             AppendEntries appendEntries =
105                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
106
107             RaftActorBehavior behavior = createBehavior(context);
108
109             // Send an unknown message so that the state of the RaftActor remains unchanged
110             RaftState expected = behavior.handleMessage(getRef(), "unknown");
111
112             RaftState raftState =
113                 behavior.handleMessage(getRef(), appendEntries);
114
115             assertEquals(expected, raftState);
116
117             // Also expect an AppendEntriesReply to be sent where success is false
118             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
119                 "AppendEntriesReply") {
120                 // do not put code outside this method, will run afterwards
121                 protected Boolean match(Object in) {
122                     if (in instanceof AppendEntriesReply) {
123                         AppendEntriesReply reply = (AppendEntriesReply) in;
124                         return reply.isSuccess();
125                     } else {
126                         throw noMatch();
127                     }
128                 }
129             }.get();
130
131             assertEquals(false, out);
132
133
134         }};
135     }
136
137     /**
138      * This test verifies that when an AppendEntries is received a specific prevLogTerm
139      * which does not match the term that is in RaftActors log entry at prevLogIndex
140      * then the RaftActor does not change it's state and it returns a failure.
141      *
142      * @throws Exception
143      */
144     @Test
145     public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
146         throws Exception {
147         new JavaTestKit(getSystem()) {{
148
149             MockRaftActorContext context = (MockRaftActorContext)
150                 createActorContext();
151
152             // First set the receivers term to lower number
153             context.getTermInformation().update(95, "test");
154
155             // Set the last log entry term for the receiver to be greater than
156             // what we will be sending as the prevLogTerm in AppendEntries
157             MockRaftActorContext.MockReplicatedLog mockReplicatedLog =
158                 setLastLogEntry(context, 20, 0, "");
159
160             // Also set the entry at index 0 with term 20 which will be greater
161             // than the prevLogTerm sent by the sender
162             mockReplicatedLog.setReplicatedLogEntry(
163                 new MockRaftActorContext.MockReplicatedLogEntry(20, 0, ""));
164
165             // AppendEntries is now sent with a bigger term
166             // this will set the receivers term to be the same as the sender's term
167             AppendEntries appendEntries =
168                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
169
170             RaftActorBehavior behavior = createBehavior(context);
171
172             // Send an unknown message so that the state of the RaftActor remains unchanged
173             RaftState expected = behavior.handleMessage(getRef(), "unknown");
174
175             RaftState raftState =
176                 behavior.handleMessage(getRef(), appendEntries);
177
178             assertEquals(expected, raftState);
179
180             // Also expect an AppendEntriesReply to be sent where success is false
181             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
182                 "AppendEntriesReply") {
183                 // do not put code outside this method, will run afterwards
184                 protected Boolean match(Object in) {
185                     if (in instanceof AppendEntriesReply) {
186                         AppendEntriesReply reply = (AppendEntriesReply) in;
187                         return reply.isSuccess();
188                     } else {
189                         throw noMatch();
190                     }
191                 }
192             }.get();
193
194             assertEquals(false, out);
195
196
197         }};
198     }
199
200     /**
201      * This test verifies that when a new AppendEntries message is received with
202      * new entries and the logs of the sender and receiver match that the new
203      * entries get added to the log and the log is incremented by the number of
204      * entries received in appendEntries
205      *
206      * @throws Exception
207      */
208     @Test
209     public void testHandleAppendEntriesAddNewEntries() throws Exception {
210         new JavaTestKit(getSystem()) {{
211
212             MockRaftActorContext context = (MockRaftActorContext)
213                 createActorContext();
214
215             // First set the receivers term to lower number
216             context.getTermInformation().update(1, "test");
217
218             // Prepare the receivers log
219             MockRaftActorContext.SimpleReplicatedLog log =
220                 new MockRaftActorContext.SimpleReplicatedLog();
221             log.append(
222                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
223             log.append(
224                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
225             log.append(
226                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
227
228             context.setReplicatedLog(log);
229
230             // Prepare the entries to be sent with AppendEntries
231             List<ReplicatedLogEntry> entries = new ArrayList<>();
232             entries.add(
233                 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, "three"));
234             entries.add(
235                 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, "four"));
236
237             // Send appendEntries with the same term as was set on the receiver
238             // before the new behavior was created (1 in this case)
239             // This will not work for a Candidate because as soon as a Candidate
240             // is created it increments the term
241             AppendEntries appendEntries =
242                 new AppendEntries(1, "leader-1", 2, 1, entries, 101);
243
244             RaftActorBehavior behavior = createBehavior(context);
245
246             if (AbstractRaftActorBehaviorTest.this instanceof CandidateTest) {
247                 // Resetting the Candidates term to make sure it will match
248                 // the term sent by AppendEntries. If this was not done then
249                 // the test will fail because the Candidate will assume that
250                 // the message was sent to it from a lower term peer and will
251                 // thus respond with a failure
252                 context.getTermInformation().update(1, "test");
253             }
254
255             // Send an unknown message so that the state of the RaftActor remains unchanged
256             RaftState expected = behavior.handleMessage(getRef(), "unknown");
257
258             RaftState raftState =
259                 behavior.handleMessage(getRef(), appendEntries);
260
261             assertEquals(expected, raftState);
262             assertEquals(5, log.last().getIndex() + 1);
263             assertNotNull(log.get(3));
264             assertNotNull(log.get(4));
265
266             // Also expect an AppendEntriesReply to be sent where success is false
267             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
268                 "AppendEntriesReply") {
269                 // do not put code outside this method, will run afterwards
270                 protected Boolean match(Object in) {
271                     if (in instanceof AppendEntriesReply) {
272                         AppendEntriesReply reply = (AppendEntriesReply) in;
273                         return reply.isSuccess();
274                     } else {
275                         throw noMatch();
276                     }
277                 }
278             }.get();
279
280             assertEquals(true, out);
281
282
283         }};
284     }
285
286     /**
287      * This test verifies that when a new AppendEntries message is received with
288      * new entries and the logs of the sender and receiver are out-of-sync that
289      * the log is first corrected by removing the out of sync entries from the
290      * log and then adding in the new entries sent with the AppendEntries message
291      *
292      * @throws Exception
293      */
294     @Test
295     public void testHandleAppendEntriesCorrectReceiverLogEntries()
296         throws Exception {
297         new JavaTestKit(getSystem()) {{
298
299             MockRaftActorContext context = (MockRaftActorContext)
300                 createActorContext();
301
302             // First set the receivers term to lower number
303             context.getTermInformation().update(2, "test");
304
305             // Prepare the receivers log
306             MockRaftActorContext.SimpleReplicatedLog log =
307                 new MockRaftActorContext.SimpleReplicatedLog();
308             log.append(
309                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
310             log.append(
311                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
312             log.append(
313                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
314
315             context.setReplicatedLog(log);
316
317             // Prepare the entries to be sent with AppendEntries
318             List<ReplicatedLogEntry> entries = new ArrayList<>();
319             entries.add(
320                 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, "two-1"));
321             entries.add(
322                 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, "three"));
323
324             // Send appendEntries with the same term as was set on the receiver
325             // before the new behavior was created (1 in this case)
326             // This will not work for a Candidate because as soon as a Candidate
327             // is created it increments the term
328             AppendEntries appendEntries =
329                 new AppendEntries(2, "leader-1", 1, 1, entries, 101);
330
331             RaftActorBehavior behavior = createBehavior(context);
332
333             if (AbstractRaftActorBehaviorTest.this instanceof CandidateTest) {
334                 // Resetting the Candidates term to make sure it will match
335                 // the term sent by AppendEntries. If this was not done then
336                 // the test will fail because the Candidate will assume that
337                 // the message was sent to it from a lower term peer and will
338                 // thus respond with a failure
339                 context.getTermInformation().update(2, "test");
340             }
341
342             // Send an unknown message so that the state of the RaftActor remains unchanged
343             RaftState expected = behavior.handleMessage(getRef(), "unknown");
344
345             RaftState raftState =
346                 behavior.handleMessage(getRef(), appendEntries);
347
348             assertEquals(expected, raftState);
349
350             // The entry at index 2 will be found out-of-sync with the leader
351             // and will be removed
352             // Then the two new entries will be added to the log
353             // Thus making the log to have 4 entries
354             assertEquals(4, log.last().getIndex() + 1);
355             assertNotNull(log.get(2));
356
357             // Check that the entry at index 2 has the new data
358             assertEquals("two-1", log.get(2).getData());
359             assertNotNull(log.get(3));
360
361             // Also expect an AppendEntriesReply to be sent where success is false
362             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
363                 "AppendEntriesReply") {
364                 // do not put code outside this method, will run afterwards
365                 protected Boolean match(Object in) {
366                     if (in instanceof AppendEntriesReply) {
367                         AppendEntriesReply reply = (AppendEntriesReply) in;
368                         return reply.isSuccess();
369                     } else {
370                         throw noMatch();
371                     }
372                 }
373             }.get();
374
375             assertEquals(true, out);
376
377
378         }};
379     }
380
381     /**
382      * This test verifies that when a RequestVote is received by the RaftActor
383      * with a term which is greater than the RaftActors' currentTerm and the
384      * senders' log is more upto date than the receiver that the receiver grants
385      * the vote to the sender
386      */
387     @Test
388     public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermAndSenderLogMoreUpToDate() {
389         new JavaTestKit(getSystem()) {{
390
391             new Within(duration("1 seconds")) {
392                 protected void run() {
393
394                     RaftActorBehavior follower = createBehavior(
395                         createActorContext(behaviorActor));
396
397                     follower.handleMessage(getTestActor(),
398                         new RequestVote(1000, "test", 10000, 999));
399
400                     final Boolean out =
401                         new ExpectMsg<Boolean>(duration("1 seconds"),
402                             "RequestVoteReply") {
403                             // do not put code outside this method, will run afterwards
404                             protected Boolean match(Object in) {
405                                 if (in instanceof RequestVoteReply) {
406                                     RequestVoteReply reply =
407                                         (RequestVoteReply) in;
408                                     return reply.isVoteGranted();
409                                 } else {
410                                     throw noMatch();
411                                 }
412                             }
413                         }.get();
414
415                     assertEquals(true, out);
416                 }
417             };
418         }};
419     }
420
421     /**
422      * This test verifies that when a RaftActor receives a RequestVote message
423      * with a term that is greater than it's currentTerm but a less up-to-date
424      * log then the receiving RaftActor will not grant the vote to the sender
425      */
426     @Test
427     public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermButSenderLogLessUptoDate() {
428         new JavaTestKit(getSystem()) {{
429
430             new Within(duration("1 seconds")) {
431                 protected void run() {
432
433                     RaftActorContext actorContext =
434                         createActorContext(behaviorActor);
435
436                     MockRaftActorContext.MockReplicatedLog
437                         log = new MockRaftActorContext.MockReplicatedLog();
438                     log.setReplicatedLogEntry(
439                         new MockRaftActorContext.MockReplicatedLogEntry(20000,
440                             1000000, ""));
441                     log.setLast(
442                         new MockRaftActorContext.MockReplicatedLogEntry(20000,
443                             1000000, "")
444                     );
445
446                     ((MockRaftActorContext) actorContext).setReplicatedLog(log);
447
448                     RaftActorBehavior follower = createBehavior(actorContext);
449
450                     follower.handleMessage(getTestActor(),
451                         new RequestVote(1000, "test", 10000, 999));
452
453                     final Boolean out =
454                         new ExpectMsg<Boolean>(duration("1 seconds"),
455                             "RequestVoteReply") {
456                             // do not put code outside this method, will run afterwards
457                             protected Boolean match(Object in) {
458                                 if (in instanceof RequestVoteReply) {
459                                     RequestVoteReply reply =
460                                         (RequestVoteReply) in;
461                                     return reply.isVoteGranted();
462                                 } else {
463                                     throw noMatch();
464                                 }
465                             }
466                         }.get();
467
468                     assertEquals(false, out);
469                 }
470             };
471         }};
472     }
473
474
475
476     /**
477      * This test verifies that the receiving RaftActor will not grant a vote
478      * to a sender if the sender's term is lesser than the currentTerm of the
479      * recipient RaftActor
480      */
481     @Test
482     public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
483         new JavaTestKit(getSystem()) {{
484
485             new Within(duration("1 seconds")) {
486                 protected void run() {
487
488                     RaftActorContext context =
489                         createActorContext(behaviorActor);
490
491                     context.getTermInformation().update(1000, null);
492
493                     RaftActorBehavior follower = createBehavior(context);
494
495                     follower.handleMessage(getTestActor(),
496                         new RequestVote(999, "test", 10000, 999));
497
498                     final Boolean out =
499                         new ExpectMsg<Boolean>(duration("1 seconds"),
500                             "RequestVoteReply") {
501                             // do not put code outside this method, will run afterwards
502                             protected Boolean match(Object in) {
503                                 if (in instanceof RequestVoteReply) {
504                                     RequestVoteReply reply =
505                                         (RequestVoteReply) in;
506                                     return reply.isVoteGranted();
507                                 } else {
508                                     throw noMatch();
509                                 }
510                             }
511                         }.get();
512
513                     assertEquals(false, out);
514                 }
515             };
516         }};
517     }
518
519     protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(
520         ActorRef actorRef, RaftRPC rpc) {
521
522         RaftActorContext actorContext = createActorContext();
523         setLastLogEntry(
524             (MockRaftActorContext) actorContext, 0, 0, "");
525
526         RaftState raftState = createBehavior(actorContext)
527             .handleMessage(actorRef, rpc);
528
529         assertEquals(RaftState.Follower, raftState);
530     }
531
532     protected MockRaftActorContext.MockReplicatedLog setLastLogEntry(
533         MockRaftActorContext actorContext, long term, long index, Object data) {
534         return setLastLogEntry(actorContext,
535             new MockRaftActorContext.MockReplicatedLogEntry(term, index, data));
536     }
537
538     protected MockRaftActorContext.MockReplicatedLog setLastLogEntry(
539         MockRaftActorContext actorContext, ReplicatedLogEntry logEntry) {
540         MockRaftActorContext.MockReplicatedLog
541             log = new MockRaftActorContext.MockReplicatedLog();
542         // By default MockReplicateLog has last entry set to (1,1,"")
543         log.setLast(logEntry);
544         actorContext.setReplicatedLog(log);
545
546         return log;
547     }
548
549     protected abstract RaftActorBehavior createBehavior(
550         RaftActorContext actorContext);
551
552     protected RaftActorBehavior createBehavior() {
553         return createBehavior(createActorContext());
554     }
555
556     protected RaftActorContext createActorContext() {
557         return new MockRaftActorContext();
558     }
559
560     protected RaftActorContext createActorContext(ActorRef actor) {
561         return new MockRaftActorContext("test", getSystem(), actor);
562     }
563
564     protected AppendEntries createAppendEntriesWithNewerTerm() {
565         return new AppendEntries(100, "leader-1", 0, 0, null, 1);
566     }
567
568     protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
569         return new AppendEntriesReply(100, false);
570     }
571
572     protected RequestVote createRequestVoteWithNewerTerm() {
573         return new RequestVote(100, "candidate-1", 10, 100);
574     }
575
576     protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
577         return new RequestVoteReply(100, false);
578     }
579
580
581
582 }