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, 0, 0, new MockRaftActorContext.MockPayload(""));
163 List<ReplicatedLogEntry> entries =
165 (ReplicatedLogEntry) new MockRaftActorContext.MockReplicatedLogEntry(100, 101,
166 new MockRaftActorContext.MockPayload("foo"))
169 // The new commitIndex is 101
170 AppendEntries appendEntries =
171 new AppendEntries(100, "leader-1", 0, 0, entries, 101);
173 RaftState raftState =
174 createBehavior(context).handleMessage(getRef(), appendEntries);
176 assertEquals(101L, context.getLastApplied());
182 * This test verifies that when an AppendEntries is received a specific prevLogTerm
183 * which does not match the term that is in RaftActors log entry at prevLogIndex
184 * then the RaftActor does not change it's state and it returns a failure.
189 public void testHandleAppendEntriesSenderPrevLogTermNotSameAsReceiverPrevLogTerm()
191 new JavaTestKit(getSystem()) {{
193 MockRaftActorContext context = (MockRaftActorContext)
194 createActorContext();
196 // First set the receivers term to lower number
197 context.getTermInformation().update(95, "test");
199 // Set the last log entry term for the receiver to be greater than
200 // what we will be sending as the prevLogTerm in AppendEntries
201 MockRaftActorContext.SimpleReplicatedLog mockReplicatedLog =
202 setLastLogEntry(context, 20, 0, new MockRaftActorContext.MockPayload(""));
204 // AppendEntries is now sent with a bigger term
205 // this will set the receivers term to be the same as the sender's term
206 AppendEntries appendEntries =
207 new AppendEntries(100, "leader-1", 0, 0, null, 101);
209 RaftActorBehavior behavior = createBehavior(context);
211 // Send an unknown message so that the state of the RaftActor remains unchanged
212 RaftState expected = behavior.handleMessage(getRef(), "unknown");
214 RaftState raftState =
215 behavior.handleMessage(getRef(), appendEntries);
217 assertEquals(expected, raftState);
219 // Also expect an AppendEntriesReply to be sent where success is false
220 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
221 "AppendEntriesReply") {
222 // do not put code outside this method, will run afterwards
223 protected Boolean match(Object in) {
224 if (in instanceof AppendEntriesReply) {
225 AppendEntriesReply reply = (AppendEntriesReply) in;
226 return reply.isSuccess();
233 assertEquals(false, out);
242 * This test verifies that when a new AppendEntries message is received with
243 * new entries and the logs of the sender and receiver match that the new
244 * entries get added to the log and the log is incremented by the number of
245 * entries received in appendEntries
250 public void testHandleAppendEntriesAddNewEntries() throws Exception {
251 new JavaTestKit(getSystem()) {{
253 MockRaftActorContext context = (MockRaftActorContext)
254 createActorContext();
256 // First set the receivers term to lower number
257 context.getTermInformation().update(1, "test");
259 // Prepare the receivers log
260 MockRaftActorContext.SimpleReplicatedLog log =
261 new MockRaftActorContext.SimpleReplicatedLog();
263 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
265 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
267 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
269 context.setReplicatedLog(log);
271 // Prepare the entries to be sent with AppendEntries
272 List<ReplicatedLogEntry> entries = new ArrayList<>();
274 new MockRaftActorContext.MockReplicatedLogEntry(1, 3, new MockRaftActorContext.MockPayload("three")));
276 new MockRaftActorContext.MockReplicatedLogEntry(1, 4, new MockRaftActorContext.MockPayload("four")));
278 // Send appendEntries with the same term as was set on the receiver
279 // before the new behavior was created (1 in this case)
280 // This will not work for a Candidate because as soon as a Candidate
281 // is created it increments the term
282 AppendEntries appendEntries =
283 new AppendEntries(1, "leader-1", 2, 1, entries, 4);
285 RaftActorBehavior behavior = createBehavior(context);
287 // Send an unknown message so that the state of the RaftActor remains unchanged
288 RaftState expected = behavior.handleMessage(getRef(), "unknown");
290 RaftState raftState =
291 behavior.handleMessage(getRef(), appendEntries);
293 assertEquals(expected, raftState);
294 assertEquals(5, log.last().getIndex() + 1);
295 assertNotNull(log.get(3));
296 assertNotNull(log.get(4));
298 // Also expect an AppendEntriesReply to be sent where success is false
299 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
300 "AppendEntriesReply") {
301 // do not put code outside this method, will run afterwards
302 protected Boolean match(Object in) {
303 if (in instanceof AppendEntriesReply) {
304 AppendEntriesReply reply = (AppendEntriesReply) in;
305 return reply.isSuccess();
312 assertEquals(true, out);
321 * This test verifies that when a new AppendEntries message is received with
322 * new entries and the logs of the sender and receiver are out-of-sync that
323 * the log is first corrected by removing the out of sync entries from the
324 * log and then adding in the new entries sent with the AppendEntries message
329 public void testHandleAppendEntriesCorrectReceiverLogEntries()
331 new JavaTestKit(getSystem()) {{
333 MockRaftActorContext context = (MockRaftActorContext)
334 createActorContext();
336 // First set the receivers term to lower number
337 context.getTermInformation().update(2, "test");
339 // Prepare the receivers log
340 MockRaftActorContext.SimpleReplicatedLog log =
341 new MockRaftActorContext.SimpleReplicatedLog();
343 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
345 new MockRaftActorContext.MockReplicatedLogEntry(1, 1, new MockRaftActorContext.MockPayload("one")));
347 new MockRaftActorContext.MockReplicatedLogEntry(1, 2, new MockRaftActorContext.MockPayload("two")));
349 context.setReplicatedLog(log);
351 // Prepare the entries to be sent with AppendEntries
352 List<ReplicatedLogEntry> entries = new ArrayList<>();
354 new MockRaftActorContext.MockReplicatedLogEntry(2, 2, new MockRaftActorContext.MockPayload("two-1")));
356 new MockRaftActorContext.MockReplicatedLogEntry(2, 3, new MockRaftActorContext.MockPayload("three")));
358 // Send appendEntries with the same term as was set on the receiver
359 // before the new behavior was created (1 in this case)
360 // This will not work for a Candidate because as soon as a Candidate
361 // is created it increments the term
362 AppendEntries appendEntries =
363 new AppendEntries(2, "leader-1", 1, 1, entries, 3);
365 RaftActorBehavior behavior = createBehavior(context);
367 // Send an unknown message so that the state of the RaftActor remains unchanged
368 RaftState expected = behavior.handleMessage(getRef(), "unknown");
370 RaftState raftState =
371 behavior.handleMessage(getRef(), appendEntries);
373 assertEquals(expected, raftState);
375 // The entry at index 2 will be found out-of-sync with the leader
376 // and will be removed
377 // Then the two new entries will be added to the log
378 // Thus making the log to have 4 entries
379 assertEquals(4, log.last().getIndex() + 1);
380 assertNotNull(log.get(2));
382 assertEquals("one", log.get(1).getData().toString());
384 // Check that the entry at index 2 has the new data
385 assertEquals("two-1", log.get(2).getData().toString());
387 assertEquals("three", log.get(3).getData().toString());
389 assertNotNull(log.get(3));
391 // Also expect an AppendEntriesReply to be sent where success is false
392 final Boolean out = new ExpectMsg<Boolean>(duration("1 seconds"),
393 "AppendEntriesReply") {
394 // do not put code outside this method, will run afterwards
395 protected Boolean match(Object in) {
396 if (in instanceof AppendEntriesReply) {
397 AppendEntriesReply reply = (AppendEntriesReply) in;
398 return reply.isSuccess();
405 assertEquals(true, out);