4cfcc3fb9e57b98f9868f3497b5e797c90315b6b
[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 static org.junit.Assert.assertEquals;
4 import static org.junit.Assert.assertTrue;
5 import akka.actor.ActorRef;
6 import akka.actor.Props;
7 import akka.testkit.JavaTestKit;
8 import com.google.protobuf.ByteString;
9 import java.io.ByteArrayOutputStream;
10 import java.io.IOException;
11 import java.io.ObjectOutputStream;
12 import java.util.ArrayList;
13 import java.util.List;
14 import java.util.Map;
15 import org.junit.Test;
16 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
17 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
18 import org.opendaylight.controller.cluster.raft.RaftActorContext;
19 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
20 import org.opendaylight.controller.cluster.raft.SerializationUtils;
21 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
22 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
23 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
24 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
25 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
26 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
27 import org.opendaylight.controller.cluster.raft.utils.DoNothingActor;
28 import org.slf4j.LoggerFactory;
29
30 public abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
31
32     private final ActorRef behaviorActor = getSystem().actorOf(Props.create(
33         DoNothingActor.class));
34
35     /**
36      * This test checks that when a new Raft RPC message is received with a newer
37      * term the RaftActor gets into the Follower state.
38      *
39      * @throws Exception
40      */
41     @Test
42     public void testHandleRaftRPCWithNewerTerm() throws Exception {
43         new JavaTestKit(getSystem()) {{
44
45             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
46                 createAppendEntriesWithNewerTerm());
47
48             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
49                 createAppendEntriesReplyWithNewerTerm());
50
51             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
52                 createRequestVoteWithNewerTerm());
53
54             assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
55                 createRequestVoteReplyWithNewerTerm());
56
57
58         }};
59     }
60
61
62     /**
63      * This test verifies that when an AppendEntries is received with a term that
64      * is less that the currentTerm of the RaftActor then the RaftActor does not
65      * change it's state and it responds back with a failure
66      *
67      * @throws Exception
68      */
69     @Test
70     public void testHandleAppendEntriesSenderTermLessThanReceiverTerm()
71         throws Exception {
72         new JavaTestKit(getSystem()) {{
73
74             MockRaftActorContext context = createActorContext();
75
76             // First set the receivers term to a high number (1000)
77             context.getTermInformation().update(1000, "test");
78
79             AppendEntries appendEntries =
80                 new AppendEntries(100, "leader-1", 0, 0, null, 101, -1);
81
82             RaftActorBehavior behavior = createBehavior(context);
83
84             // Send an unknown message so that the state of the RaftActor remains unchanged
85             RaftActorBehavior expected = behavior.handleMessage(getRef(), "unknown");
86
87             RaftActorBehavior raftBehavior =
88                 behavior.handleMessage(getRef(), appendEntries);
89
90             assertEquals(expected, raftBehavior);
91
92             // Also expect an AppendEntriesReply to be sent where success is false
93             final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
94                 "AppendEntriesReply") {
95                 // do not put code outside this method, will run afterwards
96                 @Override
97                 protected Boolean match(Object in) {
98                     if (in instanceof AppendEntriesReply) {
99                         AppendEntriesReply reply = (AppendEntriesReply) in;
100                         return reply.isSuccess();
101                     } else {
102                         throw noMatch();
103                     }
104                 }
105             }.get();
106
107             assertEquals(false, out);
108
109
110         }};
111     }
112
113
114     @Test
115     public void testHandleAppendEntriesAddSameEntryToLog(){
116         new JavaTestKit(getSystem()) {
117             {
118
119                 MockRaftActorContext context = createActorContext();
120
121                 // First set the receivers term to lower number
122                 context.getTermInformation().update(2, "test");
123
124                 // Prepare the receivers log
125                 MockRaftActorContext.SimpleReplicatedLog log =
126                     new MockRaftActorContext.SimpleReplicatedLog();
127                 log.append(
128                     new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
129
130                 context.setReplicatedLog(log);
131
132                 List<ReplicatedLogEntry> entries = new ArrayList<>();
133                 entries.add(
134                     new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
135
136                 AppendEntries appendEntries =
137                     new AppendEntries(2, "leader-1", -1, 1, entries, 0, -1);
138
139                 RaftActorBehavior behavior = createBehavior(context);
140
141                 if (AbstractRaftActorBehaviorTest.this instanceof CandidateTest) {
142                     // Resetting the Candidates term to make sure it will match
143                     // the term sent by AppendEntries. If this was not done then
144                     // the test will fail because the Candidate will assume that
145                     // the message was sent to it from a lower term peer and will
146                     // thus respond with a failure
147                     context.getTermInformation().update(2, "test");
148                 }
149
150                 // Send an unknown message so that the state of the RaftActor remains unchanged
151                 RaftActorBehavior expected = behavior.handleMessage(getRef(), "unknown");
152
153                 RaftActorBehavior raftBehavior =
154                     behavior.handleMessage(getRef(), appendEntries);
155
156                 assertEquals(expected, raftBehavior);
157
158                 assertEquals(1, log.size());
159
160
161             }};
162     }
163
164     /**
165      * This test verifies that when a RequestVote is received by the RaftActor
166      * with a term which is greater than the RaftActors' currentTerm and the
167      * senders' log is more upto date than the receiver that the receiver grants
168      * the vote to the sender
169      */
170     @Test
171     public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermAndSenderLogMoreUpToDate() {
172         new JavaTestKit(getSystem()) {{
173
174             new Within(duration("1 seconds")) {
175                 @Override
176                 protected void run() {
177
178                     RaftActorBehavior behavior = createBehavior(
179                         createActorContext(behaviorActor));
180
181                     RaftActorBehavior raftBehavior = behavior.handleMessage(getTestActor(),
182                         new RequestVote(1000, "test", 10000, 999));
183
184                     if(!(behavior instanceof Follower)){
185                         assertTrue(raftBehavior instanceof Follower);
186                     } else {
187
188                         final Boolean out =
189                             new ExpectMsg<Boolean>(duration("1 seconds"),
190                                 "RequestVoteReply") {
191                                 // do not put code outside this method, will run afterwards
192                                 @Override
193                                 protected Boolean match(Object in) {
194                                     if (in instanceof RequestVoteReply) {
195                                         RequestVoteReply reply =
196                                             (RequestVoteReply) in;
197                                         return reply.isVoteGranted();
198                                     } else {
199                                         throw noMatch();
200                                     }
201                                 }
202                             }.get();
203
204                         assertEquals(true, out);
205                     }
206                 }
207             };
208         }};
209     }
210
211     /**
212      * This test verifies that when a RaftActor receives a RequestVote message
213      * with a term that is greater than it's currentTerm but a less up-to-date
214      * log then the receiving RaftActor will not grant the vote to the sender
215      */
216     @Test
217     public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermButSenderLogLessUptoDate() {
218         new JavaTestKit(getSystem()) {{
219
220             new Within(duration("1 seconds")) {
221                 @Override
222                 protected void run() {
223
224                     RaftActorContext actorContext =
225                         createActorContext(behaviorActor);
226
227                     MockRaftActorContext.SimpleReplicatedLog
228                         log = new MockRaftActorContext.SimpleReplicatedLog();
229                     log.append(
230                         new MockRaftActorContext.MockReplicatedLogEntry(20000,
231                             1000000, new MockRaftActorContext.MockPayload("")));
232
233                     ((MockRaftActorContext) actorContext).setReplicatedLog(log);
234
235                     RaftActorBehavior behavior = createBehavior(actorContext);
236
237                     RaftActorBehavior raftBehavior = behavior.handleMessage(getTestActor(),
238                         new RequestVote(1000, "test", 10000, 999));
239
240                     if(!(behavior instanceof Follower)){
241                         assertTrue(raftBehavior instanceof Follower);
242                     } else {
243                         final Boolean out =
244                             new ExpectMsg<Boolean>(duration("1 seconds"),
245                                 "RequestVoteReply") {
246                                 // do not put code outside this method, will run afterwards
247                                 @Override
248                                 protected Boolean match(Object in) {
249                                     if (in instanceof RequestVoteReply) {
250                                         RequestVoteReply reply =
251                                             (RequestVoteReply) in;
252                                         return reply.isVoteGranted();
253                                     } else {
254                                         throw noMatch();
255                                     }
256                                 }
257                             }.get();
258
259                         assertEquals(false, out);
260                     }
261                 }
262             };
263         }};
264     }
265
266
267
268     /**
269      * This test verifies that the receiving RaftActor will not grant a vote
270      * to a sender if the sender's term is lesser than the currentTerm of the
271      * recipient RaftActor
272      */
273     @Test
274     public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
275         new JavaTestKit(getSystem()) {{
276
277             new Within(duration("1 seconds")) {
278                 @Override
279                 protected void run() {
280
281                     RaftActorContext context =
282                         createActorContext(behaviorActor);
283
284                     context.getTermInformation().update(1000, null);
285
286                     RaftActorBehavior follower = createBehavior(context);
287
288                     follower.handleMessage(getTestActor(),
289                         new RequestVote(999, "test", 10000, 999));
290
291                     final Boolean out =
292                         new ExpectMsg<Boolean>(duration("1 seconds"),
293                             "RequestVoteReply") {
294                             // do not put code outside this method, will run afterwards
295                             @Override
296                             protected Boolean match(Object in) {
297                                 if (in instanceof RequestVoteReply) {
298                                     RequestVoteReply reply =
299                                         (RequestVoteReply) in;
300                                     return reply.isVoteGranted();
301                                 } else {
302                                     throw noMatch();
303                                 }
304                             }
305                         }.get();
306
307                     assertEquals(false, out);
308                 }
309             };
310         }};
311     }
312
313     @Test
314     public void testPerformSnapshot() {
315         MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
316         AbstractRaftActorBehavior abstractBehavior =  (AbstractRaftActorBehavior) createBehavior(context);
317         if (abstractBehavior instanceof Candidate) {
318             return;
319         }
320
321         context.getTermInformation().update(1, "test");
322
323         //log has 1 entry with replicatedToAllIndex = 0, does not do anything, returns the
324         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 1, 1).build());
325         context.setLastApplied(0);
326         abstractBehavior.performSnapshotWithoutCapture(0);
327         assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
328         assertEquals(1, context.getReplicatedLog().size());
329
330         //2 entries, lastApplied still 0, no purging.
331         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
332         context.setLastApplied(0);
333         abstractBehavior.performSnapshotWithoutCapture(0);
334         assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
335         assertEquals(2, context.getReplicatedLog().size());
336
337         //2 entries, lastApplied still 0, no purging.
338         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
339         context.setLastApplied(1);
340         abstractBehavior.performSnapshotWithoutCapture(0);
341         assertEquals(0, abstractBehavior.getReplicatedToAllIndex());
342         assertEquals(1, context.getReplicatedLog().size());
343
344         //5 entries, lastApplied =2 and replicatedIndex = 3, but since we want to keep the lastapplied, indices 0 and 1 will only get purged
345         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 5, 1).build());
346         context.setLastApplied(2);
347         abstractBehavior.performSnapshotWithoutCapture(3);
348         assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
349         assertEquals(3, context.getReplicatedLog().size());
350
351         // scenario where Last applied > Replicated to all index (becoz of a slow follower)
352         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 3, 1).build());
353         context.setLastApplied(2);
354         abstractBehavior.performSnapshotWithoutCapture(1);
355         assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
356         assertEquals(1, context.getReplicatedLog().size());
357     }
358
359
360     protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(
361         ActorRef actorRef, RaftRPC rpc) {
362
363         RaftActorContext actorContext = createActorContext();
364         Payload p = new MockRaftActorContext.MockPayload("");
365         setLastLogEntry(
366             (MockRaftActorContext) actorContext, 0, 0, p);
367
368         RaftActorBehavior raftBehavior = createBehavior(actorContext)
369             .handleMessage(actorRef, rpc);
370
371         assertTrue(raftBehavior instanceof Follower);
372     }
373
374     protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
375         MockRaftActorContext actorContext, long term, long index, Payload data) {
376         return setLastLogEntry(actorContext,
377             new MockRaftActorContext.MockReplicatedLogEntry(term, index, data));
378     }
379
380     protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
381         MockRaftActorContext actorContext, ReplicatedLogEntry logEntry) {
382         MockRaftActorContext.SimpleReplicatedLog
383             log = new MockRaftActorContext.SimpleReplicatedLog();
384         log.append(logEntry);
385         actorContext.setReplicatedLog(log);
386
387         return log;
388     }
389
390     protected abstract RaftActorBehavior createBehavior(
391         RaftActorContext actorContext);
392
393     protected RaftActorBehavior createBehavior() {
394         return createBehavior(createActorContext());
395     }
396
397     protected MockRaftActorContext createActorContext() {
398         return new MockRaftActorContext();
399     }
400
401     protected MockRaftActorContext createActorContext(ActorRef actor) {
402         return new MockRaftActorContext("test", getSystem(), actor);
403     }
404
405     protected AppendEntries createAppendEntriesWithNewerTerm() {
406         return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1);
407     }
408
409     protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
410         return new AppendEntriesReply("follower-1", 100, false, 100, 100);
411     }
412
413     protected RequestVote createRequestVoteWithNewerTerm() {
414         return new RequestVote(100, "candidate-1", 10, 100);
415     }
416
417     protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
418         return new RequestVoteReply(100, false);
419     }
420
421     protected Object fromSerializableMessage(Object serializable){
422         return SerializationUtils.fromSerializable(serializable);
423     }
424
425     protected ByteString toByteString(Map<String, String> state) {
426         ByteArrayOutputStream bos = new ByteArrayOutputStream();
427         try(ObjectOutputStream oos = new ObjectOutputStream(bos)) {
428             oos.writeObject(state);
429             return ByteString.copyFrom(bos.toByteArray());
430         } catch (IOException e) {
431             throw new AssertionError("IOException occurred converting Map to Bytestring", e);
432         }
433     }
434
435     protected void logStart(String name) {
436         LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);
437     }
438 }