1 package org.opendaylight.controller.cluster.raft.behaviors;
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;
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;
30 public abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
32 private final ActorRef behaviorActor = getSystem().actorOf(Props.create(
33 DoNothingActor.class));
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.
42 public void testHandleRaftRPCWithNewerTerm() throws Exception {
43 new JavaTestKit(getSystem()) {{
45 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
46 createAppendEntriesWithNewerTerm());
48 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
49 createAppendEntriesReplyWithNewerTerm());
51 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
52 createRequestVoteWithNewerTerm());
54 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(getTestActor(),
55 createRequestVoteReplyWithNewerTerm());
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
70 public void testHandleAppendEntriesSenderTermLessThanReceiverTerm()
72 new JavaTestKit(getSystem()) {{
74 MockRaftActorContext context = createActorContext();
76 // First set the receivers term to a high number (1000)
77 context.getTermInformation().update(1000, "test");
79 AppendEntries appendEntries =
80 new AppendEntries(100, "leader-1", 0, 0, null, 101, -1);
82 RaftActorBehavior behavior = createBehavior(context);
84 // Send an unknown message so that the state of the RaftActor remains unchanged
85 RaftActorBehavior expected = behavior.handleMessage(getRef(), "unknown");
87 RaftActorBehavior raftBehavior =
88 behavior.handleMessage(getRef(), appendEntries);
90 assertEquals(expected, raftBehavior);
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
97 protected Boolean match(Object in) {
98 if (in instanceof AppendEntriesReply) {
99 AppendEntriesReply reply = (AppendEntriesReply) in;
100 return reply.isSuccess();
107 assertEquals(false, out);
115 public void testHandleAppendEntriesAddSameEntryToLog(){
116 new JavaTestKit(getSystem()) {
119 MockRaftActorContext context = createActorContext();
121 // First set the receivers term to lower number
122 context.getTermInformation().update(2, "test");
124 // Prepare the receivers log
125 MockRaftActorContext.SimpleReplicatedLog log =
126 new MockRaftActorContext.SimpleReplicatedLog();
128 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
130 context.setReplicatedLog(log);
132 List<ReplicatedLogEntry> entries = new ArrayList<>();
134 new MockRaftActorContext.MockReplicatedLogEntry(1, 0, new MockRaftActorContext.MockPayload("zero")));
136 AppendEntries appendEntries =
137 new AppendEntries(2, "leader-1", -1, 1, entries, 0, -1);
139 RaftActorBehavior behavior = createBehavior(context);
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");
150 // Send an unknown message so that the state of the RaftActor remains unchanged
151 RaftActorBehavior expected = behavior.handleMessage(getRef(), "unknown");
153 RaftActorBehavior raftBehavior =
154 behavior.handleMessage(getRef(), appendEntries);
156 assertEquals(expected, raftBehavior);
158 assertEquals(1, log.size());
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
171 public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermAndSenderLogMoreUpToDate() {
172 new JavaTestKit(getSystem()) {{
174 new Within(duration("1 seconds")) {
176 protected void run() {
178 RaftActorBehavior behavior = createBehavior(
179 createActorContext(behaviorActor));
181 RaftActorBehavior raftBehavior = behavior.handleMessage(getTestActor(),
182 new RequestVote(1000, "test", 10000, 999));
184 if(!(behavior instanceof Follower)){
185 assertTrue(raftBehavior instanceof Follower);
189 new ExpectMsg<Boolean>(duration("1 seconds"),
190 "RequestVoteReply") {
191 // do not put code outside this method, will run afterwards
193 protected Boolean match(Object in) {
194 if (in instanceof RequestVoteReply) {
195 RequestVoteReply reply =
196 (RequestVoteReply) in;
197 return reply.isVoteGranted();
204 assertEquals(true, out);
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
217 public void testHandleRequestVoteWhenSenderTermGreaterThanCurrentTermButSenderLogLessUptoDate() {
218 new JavaTestKit(getSystem()) {{
220 new Within(duration("1 seconds")) {
222 protected void run() {
224 RaftActorContext actorContext =
225 createActorContext(behaviorActor);
227 MockRaftActorContext.SimpleReplicatedLog
228 log = new MockRaftActorContext.SimpleReplicatedLog();
230 new MockRaftActorContext.MockReplicatedLogEntry(20000,
231 1000000, new MockRaftActorContext.MockPayload("")));
233 ((MockRaftActorContext) actorContext).setReplicatedLog(log);
235 RaftActorBehavior behavior = createBehavior(actorContext);
237 RaftActorBehavior raftBehavior = behavior.handleMessage(getTestActor(),
238 new RequestVote(1000, "test", 10000, 999));
240 if(!(behavior instanceof Follower)){
241 assertTrue(raftBehavior instanceof Follower);
244 new ExpectMsg<Boolean>(duration("1 seconds"),
245 "RequestVoteReply") {
246 // do not put code outside this method, will run afterwards
248 protected Boolean match(Object in) {
249 if (in instanceof RequestVoteReply) {
250 RequestVoteReply reply =
251 (RequestVoteReply) in;
252 return reply.isVoteGranted();
259 assertEquals(false, out);
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
274 public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
275 new JavaTestKit(getSystem()) {{
277 new Within(duration("1 seconds")) {
279 protected void run() {
281 RaftActorContext context =
282 createActorContext(behaviorActor);
284 context.getTermInformation().update(1000, null);
286 RaftActorBehavior follower = createBehavior(context);
288 follower.handleMessage(getTestActor(),
289 new RequestVote(999, "test", 10000, 999));
292 new ExpectMsg<Boolean>(duration("1 seconds"),
293 "RequestVoteReply") {
294 // do not put code outside this method, will run afterwards
296 protected Boolean match(Object in) {
297 if (in instanceof RequestVoteReply) {
298 RequestVoteReply reply =
299 (RequestVoteReply) in;
300 return reply.isVoteGranted();
307 assertEquals(false, out);
314 public void testPerformSnapshot() {
315 MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
316 AbstractRaftActorBehavior abstractBehavior = (AbstractRaftActorBehavior) createBehavior(context);
317 if (abstractBehavior instanceof Candidate) {
321 context.getTermInformation().update(1, "test");
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());
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());
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());
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());
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());
360 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(
361 ActorRef actorRef, RaftRPC rpc) {
363 RaftActorContext actorContext = createActorContext();
364 Payload p = new MockRaftActorContext.MockPayload("");
366 (MockRaftActorContext) actorContext, 0, 0, p);
368 RaftActorBehavior raftBehavior = createBehavior(actorContext)
369 .handleMessage(actorRef, rpc);
371 assertTrue(raftBehavior instanceof Follower);
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));
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);
390 protected abstract RaftActorBehavior createBehavior(
391 RaftActorContext actorContext);
393 protected RaftActorBehavior createBehavior() {
394 return createBehavior(createActorContext());
397 protected MockRaftActorContext createActorContext() {
398 return new MockRaftActorContext();
401 protected MockRaftActorContext createActorContext(ActorRef actor) {
402 return new MockRaftActorContext("test", getSystem(), actor);
405 protected AppendEntries createAppendEntriesWithNewerTerm() {
406 return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1);
409 protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
410 return new AppendEntriesReply("follower-1", 100, false, 100, 100);
413 protected RequestVote createRequestVoteWithNewerTerm() {
414 return new RequestVote(100, "candidate-1", 10, 100);
417 protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
418 return new RequestVoteReply(100, false);
421 protected Object fromSerializableMessage(Object serializable){
422 return SerializationUtils.fromSerializable(serializable);
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);
435 protected void logStart(String name) {
436 LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);