Test RaftActor using a simple program
[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.MockReplicatedLog mockReplicatedLog =
194                 setLastLogEntry(context, 20, 0, "");
195
196             // Also set the entry at index 0 with term 20 which will be greater
197             // than the prevLogTerm sent by the sender
198             mockReplicatedLog.setReplicatedLogEntry(
199                 new MockRaftActorContext.MockReplicatedLogEntry(20, 0, ""));
200
201             // AppendEntries is now sent with a bigger term
202             // this will set the receivers term to be the same as the sender's term
203             AppendEntries appendEntries =
204                 new AppendEntries(100, "leader-1", 0, 0, null, 101);
205
206             RaftActorBehavior behavior = createBehavior(context);
207
208             // Send an unknown message so that the state of the RaftActor remains unchanged
209             RaftState expected = behavior.handleMessage(getRef(), "unknown");
210
211             RaftState raftState =
212                 behavior.handleMessage(getRef(), appendEntries);
213
214             assertEquals(expected, raftState);
215
216             // Also expect an AppendEntriesReply to be sent where success is false
217             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
218                 "AppendEntriesReply") {
219                 // do not put code outside this method, will run afterwards
220                 protected Boolean match(Object in) {
221                     if (in instanceof AppendEntriesReply) {
222                         AppendEntriesReply reply = (AppendEntriesReply) in;
223                         return reply.isSuccess();
224                     } else {
225                         throw noMatch();
226                     }
227                 }
228             }.get();
229
230             assertEquals(false, out);
231
232
233         }};
234     }
235
236
237
238     /**
239      * This test verifies that when a new AppendEntries message is received with
240      * new entries and the logs of the sender and receiver match that the new
241      * entries get added to the log and the log is incremented by the number of
242      * entries received in appendEntries
243      *
244      * @throws Exception
245      */
246     @Test
247     public void testHandleAppendEntriesAddNewEntries() throws Exception {
248         new JavaTestKit(getSystem()) {{
249
250             MockRaftActorContext context = (MockRaftActorContext)
251                 createActorContext();
252
253             // First set the receivers term to lower number
254             context.getTermInformation().update(1, "test");
255
256             // Prepare the receivers log
257             MockRaftActorContext.SimpleReplicatedLog log =
258                 new MockRaftActorContext.SimpleReplicatedLog();
259             log.append(
260                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
261             log.append(
262                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
263             log.append(
264                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
265
266             context.setReplicatedLog(log);
267
268             // Prepare the entries to be sent with AppendEntries
269             List<ReplicatedLogEntry> entries = new ArrayList<>();
270             entries.add(
271                 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, "three"));
272             entries.add(
273                 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, "four"));
274
275             // Send appendEntries with the same term as was set on the receiver
276             // before the new behavior was created (1 in this case)
277             // This will not work for a Candidate because as soon as a Candidate
278             // is created it increments the term
279             AppendEntries appendEntries =
280                 new AppendEntries(1, "leader-1", 2, 1, entries, 4);
281
282             RaftActorBehavior behavior = createBehavior(context);
283
284             // Send an unknown message so that the state of the RaftActor remains unchanged
285             RaftState expected = behavior.handleMessage(getRef(), "unknown");
286
287             RaftState raftState =
288                 behavior.handleMessage(getRef(), appendEntries);
289
290             assertEquals(expected, raftState);
291             assertEquals(5, log.last().getIndex() + 1);
292             assertNotNull(log.get(3));
293             assertNotNull(log.get(4));
294
295             // Also expect an AppendEntriesReply to be sent where success is false
296             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
297                 "AppendEntriesReply") {
298                 // do not put code outside this method, will run afterwards
299                 protected Boolean match(Object in) {
300                     if (in instanceof AppendEntriesReply) {
301                         AppendEntriesReply reply = (AppendEntriesReply) in;
302                         return reply.isSuccess();
303                     } else {
304                         throw noMatch();
305                     }
306                 }
307             }.get();
308
309             assertEquals(true, out);
310
311
312         }};
313     }
314
315
316
317     /**
318      * This test verifies that when a new AppendEntries message is received with
319      * new entries and the logs of the sender and receiver are out-of-sync that
320      * the log is first corrected by removing the out of sync entries from the
321      * log and then adding in the new entries sent with the AppendEntries message
322      *
323      * @throws Exception
324      */
325     @Test
326     public void testHandleAppendEntriesCorrectReceiverLogEntries()
327         throws Exception {
328         new JavaTestKit(getSystem()) {{
329
330             MockRaftActorContext context = (MockRaftActorContext)
331                 createActorContext();
332
333             // First set the receivers term to lower number
334             context.getTermInformation().update(2, "test");
335
336             // Prepare the receivers log
337             MockRaftActorContext.SimpleReplicatedLog log =
338                 new MockRaftActorContext.SimpleReplicatedLog();
339             log.append(
340                 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
341             log.append(
342                 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
343             log.append(
344                 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
345
346             context.setReplicatedLog(log);
347
348             // Prepare the entries to be sent with AppendEntries
349             List<ReplicatedLogEntry> entries = new ArrayList<>();
350             entries.add(
351                 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, "two-1"));
352             entries.add(
353                 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, "three"));
354
355             // Send appendEntries with the same term as was set on the receiver
356             // before the new behavior was created (1 in this case)
357             // This will not work for a Candidate because as soon as a Candidate
358             // is created it increments the term
359             AppendEntries appendEntries =
360                 new AppendEntries(2, "leader-1", 1, 1, entries, 3);
361
362             RaftActorBehavior behavior = createBehavior(context);
363
364             // Send an unknown message so that the state of the RaftActor remains unchanged
365             RaftState expected = behavior.handleMessage(getRef(), "unknown");
366
367             RaftState raftState =
368                 behavior.handleMessage(getRef(), appendEntries);
369
370             assertEquals(expected, raftState);
371
372             // The entry at index 2 will be found out-of-sync with the leader
373             // and will be removed
374             // Then the two new entries will be added to the log
375             // Thus making the log to have 4 entries
376             assertEquals(4, log.last().getIndex() + 1);
377             assertNotNull(log.get(2));
378
379             // Check that the entry at index 2 has the new data
380             assertEquals("two-1", log.get(2).getData());
381             assertNotNull(log.get(3));
382
383             // Also expect an AppendEntriesReply to be sent where success is false
384             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
385                 "AppendEntriesReply") {
386                 // do not put code outside this method, will run afterwards
387                 protected Boolean match(Object in) {
388                     if (in instanceof AppendEntriesReply) {
389                         AppendEntriesReply reply = (AppendEntriesReply) in;
390                         return reply.isSuccess();
391                     } else {
392                         throw noMatch();
393                     }
394                 }
395             }.get();
396
397             assertEquals(true, out);
398
399
400         }};
401     }
402
403 }