2 * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.controller.cluster.raft.behaviors;
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertFalse;
13 import static org.junit.Assert.assertNull;
15 import akka.actor.ActorRef;
16 import akka.actor.Props;
17 import akka.testkit.TestActorRef;
18 import com.google.protobuf.ByteString;
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.ObjectOutputStream;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
26 import org.junit.After;
27 import org.junit.Assert;
28 import org.junit.Test;
29 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
30 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
31 import org.opendaylight.controller.cluster.raft.RaftActorContext;
32 import org.opendaylight.controller.cluster.raft.RaftState;
33 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
34 import org.opendaylight.controller.cluster.raft.TestActorFactory;
35 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
36 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
37 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
38 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
39 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
40 import org.opendaylight.controller.cluster.raft.persisted.SimpleReplicatedLogEntry;
41 import org.opendaylight.controller.cluster.raft.policy.RaftPolicy;
42 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
43 import org.opendaylight.controller.cluster.raft.utils.InMemoryJournal;
44 import org.opendaylight.controller.cluster.raft.utils.InMemorySnapshotStore;
45 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
46 import org.slf4j.LoggerFactory;
48 public abstract class AbstractRaftActorBehaviorTest<T extends RaftActorBehavior> extends AbstractActorTest {
50 protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
52 private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
53 Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
55 RaftActorBehavior behavior;
58 public void tearDown() {
59 if (behavior != null) {
65 InMemoryJournal.clear();
66 InMemorySnapshotStore.clear();
70 * This test checks that when a new Raft RPC message is received with a newer
71 * term the RaftActor gets into the Follower state.
74 public void testHandleRaftRPCWithNewerTerm() {
75 MockRaftActorContext actorContext = createActorContext();
77 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
78 createAppendEntriesWithNewerTerm());
80 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
81 createAppendEntriesReplyWithNewerTerm());
83 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
84 createRequestVoteWithNewerTerm());
86 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
87 createRequestVoteReplyWithNewerTerm());
92 * This test verifies that when an AppendEntries is received with a term that
93 * is less that the currentTerm of the RaftActor then the RaftActor does not
94 * change it's state and it responds back with a failure.
97 public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() {
98 MockRaftActorContext context = createActorContext();
99 short payloadVersion = 5;
100 context.setPayloadVersion(payloadVersion);
102 // First set the receivers term to a high number (1000)
103 context.getTermInformation().update(1000, "test");
105 AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, Collections.emptyList(), 101, -1,
108 behavior = createBehavior(context);
110 RaftState expected = behavior.state();
112 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
114 assertEquals("Raft state", expected, raftBehavior.state());
116 // Also expect an AppendEntriesReply to be sent where success is false
118 AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
119 behaviorActor, AppendEntriesReply.class);
121 assertEquals("isSuccess", false, reply.isSuccess());
122 assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
127 public void testHandleAppendEntriesAddSameEntryToLog() {
128 MockRaftActorContext context = createActorContext();
130 context.getTermInformation().update(2, "test");
132 // Prepare the receivers log
133 MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
134 setLastLogEntry(context, 2, 0, payload);
136 List<ReplicatedLogEntry> entries = new ArrayList<>();
137 entries.add(new SimpleReplicatedLogEntry(0, 2, payload));
139 final AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
141 behavior = createBehavior(context);
143 assertFalse("This test should be overridden when testing Candidate", behavior instanceof Candidate);
145 RaftState expected = behavior.state();
147 // Check that the behavior does not handle unknwon message
148 assertNull(behavior.handleMessage(behaviorActor, "unknown"));
150 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
152 assertEquals("Raft state", expected, raftBehavior.state());
154 assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
156 handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
159 protected void handleAppendEntriesAddSameEntryToLogReply(final TestActorRef<MessageCollectorActor> replyActor) {
160 AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
161 Assert.assertNull("Expected no AppendEntriesReply", reply);
165 * This test verifies that when a RequestVote is received by the RaftActor
166 * with the senders' log is more up to date than the receiver that the receiver grants
167 * the vote to the sender.
170 public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
171 MockRaftActorContext context = createActorContext();
173 behavior = createBehavior(context);
175 context.getTermInformation().update(1, "test");
177 behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
178 "test", 10000, 999));
180 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
181 RequestVoteReply.class);
182 assertEquals("isVoteGranted", true, reply.isVoteGranted());
186 * This test verifies that when a RaftActor receives a RequestVote message
187 * with a term that is greater than it's currentTerm but a less up-to-date
188 * log then the receiving RaftActor will not grant the vote to the sender.
191 public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
192 MockRaftActorContext context = createActorContext();
194 behavior = createBehavior(context);
196 context.getTermInformation().update(1, "test");
199 setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
200 new MockRaftActorContext.MockPayload(""));
202 behavior.handleMessage(behaviorActor, new RequestVote(
203 context.getTermInformation().getCurrentTerm(), "test",
204 index - 1, context.getTermInformation().getCurrentTerm()));
206 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
207 RequestVoteReply.class);
208 assertEquals("isVoteGranted", false, reply.isVoteGranted());
214 * This test verifies that the receiving RaftActor will not grant a vote
215 * to a sender if the sender's term is lesser than the currentTerm of the
216 * recipient RaftActor.
219 public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
220 MockRaftActorContext context = createActorContext();
222 context.getTermInformation().update(1000, null);
224 behavior = createBehavior(context);
226 behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
228 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
229 RequestVoteReply.class);
230 assertEquals("isVoteGranted", false, reply.isVoteGranted());
234 public void testPerformSnapshot() {
235 MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
236 AbstractRaftActorBehavior abstractBehavior = (AbstractRaftActorBehavior) createBehavior(context);
237 if (abstractBehavior instanceof Candidate) {
241 context.getTermInformation().update(1, "test");
243 //log has 1 entry with replicatedToAllIndex = 0, does not do anything, returns the
244 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 1, 1).build());
245 context.setLastApplied(0);
246 abstractBehavior.performSnapshotWithoutCapture(0);
247 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
248 assertEquals(1, context.getReplicatedLog().size());
250 //2 entries, lastApplied still 0, no purging.
251 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
252 context.setLastApplied(0);
253 abstractBehavior.performSnapshotWithoutCapture(0);
254 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
255 assertEquals(2, context.getReplicatedLog().size());
257 //2 entries, lastApplied still 0, no purging.
258 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
259 context.setLastApplied(1);
260 abstractBehavior.performSnapshotWithoutCapture(0);
261 assertEquals(0, abstractBehavior.getReplicatedToAllIndex());
262 assertEquals(1, context.getReplicatedLog().size());
264 // 5 entries, lastApplied =2 and replicatedIndex = 3, but since we want to keep the lastapplied, indices 0 and
265 // 1 will only get purged
266 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 5, 1).build());
267 context.setLastApplied(2);
268 abstractBehavior.performSnapshotWithoutCapture(3);
269 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
270 assertEquals(3, context.getReplicatedLog().size());
272 // scenario where Last applied > Replicated to all index (becoz of a slow follower)
273 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 3, 1).build());
274 context.setLastApplied(2);
275 abstractBehavior.performSnapshotWithoutCapture(1);
276 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
277 assertEquals(1, context.getReplicatedLog().size());
281 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(final MockRaftActorContext actorContext,
282 final ActorRef actorRef, final RaftRPC rpc) {
284 Payload payload = new MockRaftActorContext.MockPayload("");
285 setLastLogEntry(actorContext, 1, 0, payload);
286 actorContext.getTermInformation().update(1, "test");
288 RaftActorBehavior origBehavior = createBehavior(actorContext);
289 RaftActorBehavior raftBehavior = origBehavior.handleMessage(actorRef, rpc);
291 assertEquals("New raft state", RaftState.Follower, raftBehavior.state());
292 assertEquals("New election term", rpc.getTerm(), actorContext.getTermInformation().getCurrentTerm());
294 origBehavior.close();
295 raftBehavior.close();
298 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
299 final MockRaftActorContext actorContext, final long term, final long index, final Payload data) {
300 return setLastLogEntry(actorContext, new SimpleReplicatedLogEntry(index, term, data));
303 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(final MockRaftActorContext actorContext,
304 final ReplicatedLogEntry logEntry) {
305 MockRaftActorContext.SimpleReplicatedLog log = new MockRaftActorContext.SimpleReplicatedLog();
306 log.append(logEntry);
307 actorContext.setReplicatedLog(log);
312 protected abstract T createBehavior(RaftActorContext actorContext);
314 protected final T createBehavior(final MockRaftActorContext actorContext) {
315 T ret = createBehavior((RaftActorContext)actorContext);
316 actorContext.setCurrentBehavior(ret);
320 protected RaftActorBehavior createBehavior() {
321 return createBehavior(createActorContext());
324 protected MockRaftActorContext createActorContext() {
325 return new MockRaftActorContext();
328 protected MockRaftActorContext createActorContext(final ActorRef actor) {
329 return new MockRaftActorContext("test", getSystem(), actor);
332 protected AppendEntries createAppendEntriesWithNewerTerm() {
333 return new AppendEntries(100, "leader-1", 0, 0, Collections.emptyList(), 1, -1, (short)0);
336 protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
337 return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
340 protected RequestVote createRequestVoteWithNewerTerm() {
341 return new RequestVote(100, "candidate-1", 10, 100);
344 protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
345 return new RequestVoteReply(100, false);
348 protected ByteString toByteString(final Map<String, String> state) {
349 ByteArrayOutputStream bos = new ByteArrayOutputStream();
350 try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
351 oos.writeObject(state);
352 return ByteString.copyFrom(bos.toByteArray());
353 } catch (IOException e) {
354 throw new AssertionError("IOException occurred converting Map to Bytestring", e);
358 protected void logStart(final String name) {
359 LoggerFactory.getLogger(getClass()).info("Starting " + name);
362 protected RaftPolicy createRaftPolicy(final boolean automaticElectionsEnabled,
363 final boolean applyModificationToStateBeforeConsensus) {
364 return new RaftPolicy() {
366 public boolean automaticElectionsEnabled() {
367 return automaticElectionsEnabled;
371 public boolean applyModificationToStateBeforeConsensus() {
372 return applyModificationToStateBeforeConsensus;