1 package org.opendaylight.controller.cluster.raft.behaviors;
3 import akka.actor.ActorRef;
4 import akka.actor.Props;
5 import akka.testkit.JavaTestKit;
6 import junit.framework.Assert;
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.base.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;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.List;
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertNotNull;
26 public class FollowerTest extends AbstractRaftActorBehaviorTest {
28 private final ActorRef followerActor = getSystem().actorOf(Props.create(
29 DoNothingActor.class));
32 @Override protected RaftActorBehavior createBehavior(RaftActorContext actorContext) {
33 return new Follower(actorContext);
36 @Override protected RaftActorContext createActorContext() {
37 return new MockRaftActorContext("test", getSystem(), followerActor);
41 public void testThatAnElectionTimeoutIsTriggered(){
42 new JavaTestKit(getSystem()) {{
44 new Within(duration("1 seconds")) {
45 protected void run() {
47 Follower follower = new Follower(createActorContext(getTestActor()));
49 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "ElectionTimeout") {
50 // do not put code outside this method, will run afterwards
51 protected Boolean match(Object in) {
52 if (in instanceof ElectionTimeout) {
60 assertEquals(true, out);
67 public void testHandleElectionTimeout(){
68 RaftActorContext raftActorContext = createActorContext();
70 new Follower(raftActorContext);
73 follower.handleMessage(followerActor, new ElectionTimeout());
75 Assert.assertEquals(RaftState.Candidate, raftState);
79 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNull(){
80 new JavaTestKit(getSystem()) {{
82 new Within(duration("1 seconds")) {
83 protected void run() {
85 RaftActorContext context = createActorContext(getTestActor());
87 context.getTermInformation().update(1000, null);
89 RaftActorBehavior follower = createBehavior(context);
91 follower.handleMessage(getTestActor(), new RequestVote(1000, "test", 10000, 999));
93 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
94 // do not put code outside this method, will run afterwards
95 protected Boolean match(Object in) {
96 if (in instanceof RequestVoteReply) {
97 RequestVoteReply reply = (RequestVoteReply) in;
98 return reply.isVoteGranted();
105 assertEquals(true, out);
112 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNotTheSameAsCandidateId(){
113 new JavaTestKit(getSystem()) {{
115 new Within(duration("1 seconds")) {
116 protected void run() {
118 RaftActorContext context = createActorContext(getTestActor());
120 context.getTermInformation().update(1000, "test");
122 RaftActorBehavior follower = createBehavior(context);
124 follower.handleMessage(getTestActor(), new RequestVote(1000, "candidate", 10000, 999));
126 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
127 // do not put code outside this method, will run afterwards
128 protected Boolean match(Object in) {
129 if (in instanceof RequestVoteReply) {
130 RequestVoteReply reply = (RequestVoteReply) in;
131 return reply.isVoteGranted();
138 assertEquals(false, out);
145 * This test verifies that when an AppendEntries RPC is received by a RaftActor
146 * with a commitIndex that is greater than what has been applied to the
147 * state machine of the RaftActor, the RaftActor applies the state and
148 * sets it current applied state to the commitIndex of the sender.
153 public void testHandleAppendEntriesWithNewerCommitIndex() throws Exception {
154 new JavaTestKit(getSystem()) {{
156 RaftActorContext context =
157 createActorContext();
159 context.setLastApplied(100);
160 setLastLogEntry((MockRaftActorContext) context, 0, 0, new MockRaftActorContext.MockPayload(""));
162 List<ReplicatedLogEntry> entries =
164 (ReplicatedLogEntry) new MockRaftActorContext.MockReplicatedLogEntry(100, 101,
165 new MockRaftActorContext.MockPayload("foo"))
168 // The new commitIndex is 101
169 AppendEntries appendEntries =
170 new AppendEntries(100, "leader-1", 0, 0, entries, 101);
172 RaftState raftState =
173 createBehavior(context).handleMessage(getRef(), appendEntries);
175 assertEquals(101L, context.getLastApplied());
181 * This test verifies that when an AppendEntries is received a specific prevLogTerm
182 * which does not match the term that is in RaftActors log entry at prevLogIndex
183 * then the RaftActor does not change it's state and it returns a failure.
188 public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
190 new JavaTestKit(getSystem()) {{
192 MockRaftActorContext context = (MockRaftActorContext)
193 createActorContext();
195 // First set the receivers term to lower number
196 context.getTermInformation().update(95, "test");
198 // Set the last log entry term for the receiver to be greater than
199 // what we will be sending as the prevLogTerm in AppendEntries
200 MockRaftActorContext.SimpleReplicatedLog mockReplicatedLog =
201 setLastLogEntry(context, 20, 0, new MockRaftActorContext.MockPayload(""));
203 // AppendEntries is now sent with a bigger term
204 // this will set the receivers term to be the same as the sender's term
205 AppendEntries appendEntries =
206 new AppendEntries(100, "leader-1", 0, 0, null, 101);
208 RaftActorBehavior behavior = createBehavior(context);
210 // Send an unknown message so that the state of the RaftActor remains unchanged
211 RaftState expected = behavior.handleMessage(getRef(), "unknown");
213 RaftState raftState =
214 behavior.handleMessage(getRef(), appendEntries);
216 assertEquals(expected, raftState);
218 // Also expect an AppendEntriesReply to be sent where success is false
219 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
220 "AppendEntriesReply") {
221 // do not put code outside this method, will run afterwards
222 protected Boolean match(Object in) {
223 if (in instanceof AppendEntriesReply) {
224 AppendEntriesReply reply = (AppendEntriesReply) in;
225 return reply.isSuccess();
232 assertEquals(false, out);
241 * This test verifies that when a new AppendEntries message is received with
242 * new entries and the logs of the sender and receiver match that the new
243 * entries get added to the log and the log is incremented by the number of
244 * entries received in appendEntries
249 public void testHandleAppendEntriesAddNewEntries() throws Exception {
250 new JavaTestKit(getSystem()) {{
252 MockRaftActorContext context = (MockRaftActorContext)
253 createActorContext();
255 // First set the receivers term to lower number
256 context.getTermInformation().update(1, "test");
258 // Prepare the receivers log
259 MockRaftActorContext.SimpleReplicatedLog log =
260 new MockRaftActorContext.SimpleReplicatedLog();
262 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
264 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
266 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
268 context.setReplicatedLog(log);
270 // Prepare the entries to be sent with AppendEntries
271 List<ReplicatedLogEntry> entries = new ArrayList<>();
273 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, new MockRaftActorContext.MockPayload("three")));
275 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, new MockRaftActorContext.MockPayload("four")));
277 // Send appendEntries with the same term as was set on the receiver
278 // before the new behavior was created (1 in this case)
279 // This will not work for a Candidate because as soon as a Candidate
280 // is created it increments the term
281 AppendEntries appendEntries =
282 new AppendEntries(1, "leader-1", 2, 1, entries, 4);
284 RaftActorBehavior behavior = createBehavior(context);
286 // Send an unknown message so that the state of the RaftActor remains unchanged
287 RaftState expected = behavior.handleMessage(getRef(), "unknown");
289 RaftState raftState =
290 behavior.handleMessage(getRef(), appendEntries);
292 assertEquals(expected, raftState);
293 assertEquals(5, log.last().getIndex() + 1);
294 assertNotNull(log.get(3));
295 assertNotNull(log.get(4));
297 // Also expect an AppendEntriesReply to be sent where success is false
298 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
299 "AppendEntriesReply") {
300 // do not put code outside this method, will run afterwards
301 protected Boolean match(Object in) {
302 if (in instanceof AppendEntriesReply) {
303 AppendEntriesReply reply = (AppendEntriesReply) in;
304 return reply.isSuccess();
311 assertEquals(true, out);
320 * This test verifies that when a new AppendEntries message is received with
321 * new entries and the logs of the sender and receiver are out-of-sync that
322 * the log is first corrected by removing the out of sync entries from the
323 * log and then adding in the new entries sent with the AppendEntries message
328 public void testHandleAppendEntriesCorrectReceiverLogEntries()
330 new JavaTestKit(getSystem()) {{
332 MockRaftActorContext context = (MockRaftActorContext)
333 createActorContext();
335 // First set the receivers term to lower number
336 context.getTermInformation().update(2, "test");
338 // Prepare the receivers log
339 MockRaftActorContext.SimpleReplicatedLog log =
340 new MockRaftActorContext.SimpleReplicatedLog();
342 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
344 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
346 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
348 context.setReplicatedLog(log);
350 // Prepare the entries to be sent with AppendEntries
351 List<ReplicatedLogEntry> entries = new ArrayList<>();
353 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, new MockRaftActorContext.MockPayload("two-1")));
355 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, new MockRaftActorContext.MockPayload("three")));
357 // Send appendEntries with the same term as was set on the receiver
358 // before the new behavior was created (1 in this case)
359 // This will not work for a Candidate because as soon as a Candidate
360 // is created it increments the term
361 AppendEntries appendEntries =
362 new AppendEntries(2, "leader-1", 1, 1, entries, 3);
364 RaftActorBehavior behavior = createBehavior(context);
366 // Send an unknown message so that the state of the RaftActor remains unchanged
367 RaftState expected = behavior.handleMessage(getRef(), "unknown");
369 RaftState raftState =
370 behavior.handleMessage(getRef(), appendEntries);
372 assertEquals(expected, raftState);
374 // The entry at index 2 will be found out-of-sync with the leader
375 // and will be removed
376 // Then the two new entries will be added to the log
377 // Thus making the log to have 4 entries
378 assertEquals(4, log.last().getIndex() + 1);
379 assertNotNull(log.get(2));
381 assertEquals("one", log.get(1).getData().toString());
383 // Check that the entry at index 2 has the new data
384 assertEquals("two-1", log.get(2).getData().toString());
386 assertEquals("three", log.get(3).getData().toString());
388 assertNotNull(log.get(3));
390 // Also expect an AppendEntriesReply to be sent where success is false
391 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
392 "AppendEntriesReply") {
393 // do not put code outside this method, will run afterwards
394 protected Boolean match(Object in) {
395 if (in instanceof AppendEntriesReply) {
396 AppendEntriesReply reply = (AppendEntriesReply) in;
397 return reply.isSuccess();
404 assertEquals(true, out);