1 package org.opendaylight.controller.cluster.raft.behaviors;
3 import akka.actor.ActorRef;
4 import akka.actor.Props;
5 import akka.testkit.JavaTestKit;
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;
19 import java.util.ArrayList;
20 import java.util.List;
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
25 public abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
27 private final ActorRef behaviorActor = getSystem().actorOf(Props.create(
28 DoNothingActor.class));
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.
37 public void testHandleRaftRPCWithNewerTerm() throws Exception {
38 new JavaTestKit(getSystem()) {{
40 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
41 createAppendEntriesWithNewerTerm());
43 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
44 createAppendEntriesReplyWithNewerTerm());
46 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
47 createRequestVoteWithNewerTerm());
49 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
50 createRequestVoteReplyWithNewerTerm());
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.
65 public void testHandleAppendEntriesWithNewerCommitIndex() throws Exception {
66 new JavaTestKit(getSystem()) {{
68 RaftActorContext context =
71 context.setLastApplied(100);
72 setLastLogEntry((MockRaftActorContext) context, 0, 0, "");
74 // The new commitIndex is 101
75 AppendEntries appendEntries =
76 new AppendEntries(100, "leader-1", 0, 0, null, 101);
79 createBehavior(context).handleMessage(getRef(), appendEntries);
81 assertEquals(101L, context.getLastApplied());
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
94 public void testHandleAppendEntriesSenderTermLessThanReceiverTerm()
96 new JavaTestKit(getSystem()) {{
98 MockRaftActorContext context = (MockRaftActorContext)
101 // First set the receivers term to a high number (1000)
102 context.getTermInformation().update(1000, "test");
104 AppendEntries appendEntries =
105 new AppendEntries(100, "leader-1", 0, 0, null, 101);
107 RaftActorBehavior behavior = createBehavior(context);
109 // Send an unknown message so that the state of the RaftActor remains unchanged
110 RaftState expected = behavior.handleMessage(getRef(), "unknown");
112 RaftState raftState =
113 behavior.handleMessage(getRef(), appendEntries);
115 assertEquals(expected, raftState);
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();
131 assertEquals(false, out);
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.
145 public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
147 new JavaTestKit(getSystem()) {{
149 MockRaftActorContext context = (MockRaftActorContext)
150 createActorContext();
152 // First set the receivers term to lower number
153 context.getTermInformation().update(95, "test");
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, "");
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, ""));
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);
170 RaftActorBehavior behavior = createBehavior(context);
172 // Send an unknown message so that the state of the RaftActor remains unchanged
173 RaftState expected = behavior.handleMessage(getRef(), "unknown");
175 RaftState raftState =
176 behavior.handleMessage(getRef(), appendEntries);
178 assertEquals(expected, raftState);
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();
194 assertEquals(false, out);
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
209 public void testHandleAppendEntriesAddNewEntries() throws Exception {
210 new JavaTestKit(getSystem()) {{
212 MockRaftActorContext context = (MockRaftActorContext)
213 createActorContext();
215 // First set the receivers term to lower number
216 context.getTermInformation().update(1, "test");
218 // Prepare the receivers log
219 MockRaftActorContext.SimpleReplicatedLog log =
220 new MockRaftActorContext.SimpleReplicatedLog();
222 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
224 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
226 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
228 context.setReplicatedLog(log);
230 // Prepare the entries to be sent with AppendEntries
231 List<ReplicatedLogEntry> entries = new ArrayList<>();
233 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, "three"));
235 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, "four"));
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);
244 RaftActorBehavior behavior = createBehavior(context);
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");
255 // Send an unknown message so that the state of the RaftActor remains unchanged
256 RaftState expected = behavior.handleMessage(getRef(), "unknown");
258 RaftState raftState =
259 behavior.handleMessage(getRef(), appendEntries);
261 assertEquals(expected, raftState);
262 assertEquals(5, log.last().getIndex() + 1);
263 assertNotNull(log.get(3));
264 assertNotNull(log.get(4));
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();
280 assertEquals(true, out);
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
295 public void testHandleAppendEntriesCorrectReceiverLogEntries()
297 new JavaTestKit(getSystem()) {{
299 MockRaftActorContext context = (MockRaftActorContext)
300 createActorContext();
302 // First set the receivers term to lower number
303 context.getTermInformation().update(2, "test");
305 // Prepare the receivers log
306 MockRaftActorContext.SimpleReplicatedLog log =
307 new MockRaftActorContext.SimpleReplicatedLog();
309 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, "zero"));
311 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, "one"));
313 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, "two"));
315 context.setReplicatedLog(log);
317 // Prepare the entries to be sent with AppendEntries
318 List<ReplicatedLogEntry> entries = new ArrayList<>();
320 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, "two-1"));
322 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, "three"));
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);
331 RaftActorBehavior behavior = createBehavior(context);
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");
342 // Send an unknown message so that the state of the RaftActor remains unchanged
343 RaftState expected = behavior.handleMessage(getRef(), "unknown");
345 RaftState raftState =
346 behavior.handleMessage(getRef(), appendEntries);
348 assertEquals(expected, raftState);
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));
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));
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();
375 assertEquals(true, out);
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
388 public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermAndSenderLogMoreUpToDate() {
389 new JavaTestKit(getSystem()) {{
391 new Within(duration("1 seconds")) {
392 protected void run() {
394 RaftActorBehavior follower = createBehavior(
395 createActorContext(behaviorActor));
397 follower.handleMessage(getTestActor(),
398 new RequestVote(1000, "test", 10000, 999));
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();
415 assertEquals(true, out);
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
427 public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermButSenderLogLessUptoDate() {
428 new JavaTestKit(getSystem()) {{
430 new Within(duration("1 seconds")) {
431 protected void run() {
433 RaftActorContext actorContext =
434 createActorContext(behaviorActor);
436 MockRaftActorContext.MockReplicatedLog
437 log = new MockRaftActorContext.MockReplicatedLog();
438 log.setReplicatedLogEntry(
439 new MockRaftActorContext.MockReplicatedLogEntry(20000,
442 new MockRaftActorContext.MockReplicatedLogEntry(20000,
446 ((MockRaftActorContext) actorContext).setReplicatedLog(log);
448 RaftActorBehavior follower = createBehavior(actorContext);
450 follower.handleMessage(getTestActor(),
451 new RequestVote(1000, "test", 10000, 999));
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();
468 assertEquals(false, out);
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
482 public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
483 new JavaTestKit(getSystem()) {{
485 new Within(duration("1 seconds")) {
486 protected void run() {
488 RaftActorContext context =
489 createActorContext(behaviorActor);
491 context.getTermInformation().update(1000, null);
493 RaftActorBehavior follower = createBehavior(context);
495 follower.handleMessage(getTestActor(),
496 new RequestVote(999, "test", 10000, 999));
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();
513 assertEquals(false, out);
519 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(
520 ActorRef actorRef, RaftRPC rpc) {
522 RaftActorContext actorContext = createActorContext();
524 (MockRaftActorContext) actorContext, 0, 0, "");
526 RaftState raftState = createBehavior(actorContext)
527 .handleMessage(actorRef, rpc);
529 assertEquals(RaftState.Follower, raftState);
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));
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);
549 protected abstract RaftActorBehavior createBehavior(
550 RaftActorContext actorContext);
552 protected RaftActorBehavior createBehavior() {
553 return createBehavior(createActorContext());
556 protected RaftActorContext createActorContext() {
557 return new MockRaftActorContext();
560 protected RaftActorContext createActorContext(ActorRef actor) {
561 return new MockRaftActorContext("test", getSystem(), actor);
564 protected AppendEntries createAppendEntriesWithNewerTerm() {
565 return new AppendEntries(100, "leader-1", 0, 0, null, 1);
568 protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
569 return new AppendEntriesReply(100, false);
572 protected RequestVote createRequestVoteWithNewerTerm() {
573 return new RequestVote(100, "candidate-1", 10, 100);
576 protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
577 return new RequestVoteReply(100, false);