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.DefaultConfigParamsImpl;
9 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
10 import org.opendaylight.controller.cluster.raft.RaftActorContext;
11 import org.opendaylight.controller.cluster.raft.RaftState;
12 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
13 import org.opendaylight.controller.cluster.raft.base.messages.ElectionTimeout;
14 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
15 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
16 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
17 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
18 import org.opendaylight.controller.cluster.raft.utils.DoNothingActor;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.List;
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertNotNull;
27 public class FollowerTest extends AbstractRaftActorBehaviorTest {
29 private final ActorRef followerActor = getSystem().actorOf(Props.create(
30 DoNothingActor.class));
33 @Override protected RaftActorBehavior createBehavior(RaftActorContext actorContext) {
34 return new Follower(actorContext);
37 @Override protected RaftActorContext createActorContext() {
38 return new MockRaftActorContext("test", getSystem(), followerActor);
42 public void testThatAnElectionTimeoutIsTriggered(){
43 new JavaTestKit(getSystem()) {{
45 new Within(DefaultConfigParamsImpl.HEART_BEAT_INTERVAL.$times(6)) {
46 protected void run() {
48 Follower follower = new Follower(createActorContext(getTestActor()));
50 final Boolean out = new ExpectMsg<Boolean>(DefaultConfigParamsImpl.HEART_BEAT_INTERVAL.$times(6), "ElectionTimeout") {
51 // do not put code outside this method, will run afterwards
52 protected Boolean match(Object in) {
53 if (in instanceof ElectionTimeout) {
61 assertEquals(true, out);
68 public void testHandleElectionTimeout(){
69 RaftActorContext raftActorContext = createActorContext();
71 new Follower(raftActorContext);
74 follower.handleMessage(followerActor, new ElectionTimeout());
76 Assert.assertEquals(RaftState.Candidate, raftState);
80 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNull(){
81 new JavaTestKit(getSystem()) {{
83 new Within(duration("1 seconds")) {
84 protected void run() {
86 RaftActorContext context = createActorContext(getTestActor());
88 context.getTermInformation().update(1000, null);
90 RaftActorBehavior follower = createBehavior(context);
92 follower.handleMessage(getTestActor(), new RequestVote(1000, "test", 10000, 999));
94 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
95 // do not put code outside this method, will run afterwards
96 protected Boolean match(Object in) {
97 if (in instanceof RequestVoteReply) {
98 RequestVoteReply reply = (RequestVoteReply) in;
99 return reply.isVoteGranted();
106 assertEquals(true, out);
113 public void testHandleRequestVoteWhenSenderTermEqualToCurrentTermAndVotedForIsNotTheSameAsCandidateId(){
114 new JavaTestKit(getSystem()) {{
116 new Within(duration("1 seconds")) {
117 protected void run() {
119 RaftActorContext context = createActorContext(getTestActor());
121 context.getTermInformation().update(1000, "test");
123 RaftActorBehavior follower = createBehavior(context);
125 follower.handleMessage(getTestActor(), new RequestVote(1000, "candidate", 10000, 999));
127 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"), "RequestVoteReply") {
128 // do not put code outside this method, will run afterwards
129 protected Boolean match(Object in) {
130 if (in instanceof RequestVoteReply) {
131 RequestVoteReply reply = (RequestVoteReply) in;
132 return reply.isVoteGranted();
139 assertEquals(false, out);
146 * This test verifies that when an AppendEntries RPC is received by a RaftActor
147 * with a commitIndex that is greater than what has been applied to the
148 * state machine of the RaftActor, the RaftActor applies the state and
149 * sets it current applied state to the commitIndex of the sender.
154 public void testHandleAppendEntriesWithNewerCommitIndex() throws Exception {
155 new JavaTestKit(getSystem()) {{
157 RaftActorContext context =
158 createActorContext();
160 context.setLastApplied(100);
161 setLastLogEntry((MockRaftActorContext) context, 1, 100, new MockRaftActorContext.MockPayload(""));
162 ((MockRaftActorContext) context).getReplicatedLog().setSnapshotIndex(99);
164 List<ReplicatedLogEntry> entries =
166 (ReplicatedLogEntry) new MockRaftActorContext.MockReplicatedLogEntry(2, 101,
167 new MockRaftActorContext.MockPayload("foo"))
170 // The new commitIndex is 101
171 AppendEntries appendEntries =
172 new AppendEntries(2, "leader-1", 100, 1, entries, 101);
174 RaftState raftState =
175 createBehavior(context).handleMessage(getRef(), appendEntries);
177 assertEquals(101L, context.getLastApplied());
183 * This test verifies that when an AppendEntries is received a specific prevLogTerm
184 * which does not match the term that is in RaftActors log entry at prevLogIndex
185 * then the RaftActor does not change it's state and it returns a failure.
190 public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
192 new JavaTestKit(getSystem()) {{
194 MockRaftActorContext context = (MockRaftActorContext)
195 createActorContext();
197 // First set the receivers term to lower number
198 context.getTermInformation().update(95, "test");
200 // Set the last log entry term for the receiver to be greater than
201 // what we will be sending as the prevLogTerm in AppendEntries
202 MockRaftActorContext.SimpleReplicatedLog mockReplicatedLog =
203 setLastLogEntry(context, 20, 0, new MockRaftActorContext.MockPayload(""));
205 // AppendEntries is now sent with a bigger term
206 // this will set the receivers term to be the same as the sender's term
207 AppendEntries appendEntries =
208 new AppendEntries(100, "leader-1", 0, 0, null, 101);
210 RaftActorBehavior behavior = createBehavior(context);
212 // Send an unknown message so that the state of the RaftActor remains unchanged
213 RaftState expected = behavior.handleMessage(getRef(), "unknown");
215 RaftState raftState =
216 behavior.handleMessage(getRef(), appendEntries);
218 assertEquals(expected, raftState);
220 // Also expect an AppendEntriesReply to be sent where success is false
221 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
222 "AppendEntriesReply") {
223 // do not put code outside this method, will run afterwards
224 protected Boolean match(Object in) {
225 if (in instanceof AppendEntriesReply) {
226 AppendEntriesReply reply = (AppendEntriesReply) in;
227 return reply.isSuccess();
234 assertEquals(false, out);
243 * This test verifies that when a new AppendEntries message is received with
244 * new entries and the logs of the sender and receiver match that the new
245 * entries get added to the log and the log is incremented by the number of
246 * entries received in appendEntries
251 public void testHandleAppendEntriesAddNewEntries() throws Exception {
252 new JavaTestKit(getSystem()) {{
254 MockRaftActorContext context = (MockRaftActorContext)
255 createActorContext();
257 // First set the receivers term to lower number
258 context.getTermInformation().update(1, "test");
260 // Prepare the receivers log
261 MockRaftActorContext.SimpleReplicatedLog log =
262 new MockRaftActorContext.SimpleReplicatedLog();
264 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
266 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
268 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
270 context.setReplicatedLog(log);
272 // Prepare the entries to be sent with AppendEntries
273 List<ReplicatedLogEntry> entries = new ArrayList<>();
275 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, new MockRaftActorContext.MockPayload("three")));
277 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, new MockRaftActorContext.MockPayload("four")));
279 // Send appendEntries with the same term as was set on the receiver
280 // before the new behavior was created (1 in this case)
281 // This will not work for a Candidate because as soon as a Candidate
282 // is created it increments the term
283 AppendEntries appendEntries =
284 new AppendEntries(1, "leader-1", 2, 1, entries, 4);
286 RaftActorBehavior behavior = createBehavior(context);
288 // Send an unknown message so that the state of the RaftActor remains unchanged
289 RaftState expected = behavior.handleMessage(getRef(), "unknown");
291 RaftState raftState =
292 behavior.handleMessage(getRef(), appendEntries);
294 assertEquals(expected, raftState);
295 assertEquals(5, log.last().getIndex() + 1);
296 assertNotNull(log.get(3));
297 assertNotNull(log.get(4));
299 // Also expect an AppendEntriesReply to be sent where success is false
300 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
301 "AppendEntriesReply") {
302 // do not put code outside this method, will run afterwards
303 protected Boolean match(Object in) {
304 if (in instanceof AppendEntriesReply) {
305 AppendEntriesReply reply = (AppendEntriesReply) in;
306 return reply.isSuccess();
313 assertEquals(true, out);
322 * This test verifies that when a new AppendEntries message is received with
323 * new entries and the logs of the sender and receiver are out-of-sync that
324 * the log is first corrected by removing the out of sync entries from the
325 * log and then adding in the new entries sent with the AppendEntries message
330 public void testHandleAppendEntriesCorrectReceiverLogEntries()
332 new JavaTestKit(getSystem()) {{
334 MockRaftActorContext context = (MockRaftActorContext)
335 createActorContext();
337 // First set the receivers term to lower number
338 context.getTermInformation().update(2, "test");
340 // Prepare the receivers log
341 MockRaftActorContext.SimpleReplicatedLog log =
342 new MockRaftActorContext.SimpleReplicatedLog();
344 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
346 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
348 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
350 context.setReplicatedLog(log);
352 // Prepare the entries to be sent with AppendEntries
353 List<ReplicatedLogEntry> entries = new ArrayList<>();
355 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, new MockRaftActorContext.MockPayload("two-1")));
357 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, new MockRaftActorContext.MockPayload("three")));
359 // Send appendEntries with the same term as was set on the receiver
360 // before the new behavior was created (1 in this case)
361 // This will not work for a Candidate because as soon as a Candidate
362 // is created it increments the term
363 AppendEntries appendEntries =
364 new AppendEntries(2, "leader-1", 1, 1, entries, 3);
366 RaftActorBehavior behavior = createBehavior(context);
368 // Send an unknown message so that the state of the RaftActor remains unchanged
369 RaftState expected = behavior.handleMessage(getRef(), "unknown");
371 RaftState raftState =
372 behavior.handleMessage(getRef(), appendEntries);
374 assertEquals(expected, raftState);
376 // The entry at index 2 will be found out-of-sync with the leader
377 // and will be removed
378 // Then the two new entries will be added to the log
379 // Thus making the log to have 4 entries
380 assertEquals(4, log.last().getIndex() + 1);
381 assertNotNull(log.get(2));
383 assertEquals("one", log.get(1).getData().toString());
385 // Check that the entry at index 2 has the new data
386 assertEquals("two-1", log.get(2).getData().toString());
388 assertEquals("three", log.get(3).getData().toString());
390 assertNotNull(log.get(3));
392 // Also expect an AppendEntriesReply to be sent where success is false
393 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
394 "AppendEntriesReply") {
395 // do not put code outside this method, will run afterwards
396 protected Boolean match(Object in) {
397 if (in instanceof AppendEntriesReply) {
398 AppendEntriesReply reply = (AppendEntriesReply) in;
399 return reply.isSuccess();
406 assertEquals(true, out);