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.List;
25 import org.junit.After;
26 import org.junit.Assert;
27 import org.junit.Test;
28 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
29 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
30 import org.opendaylight.controller.cluster.raft.RaftActorContext;
31 import org.opendaylight.controller.cluster.raft.RaftState;
32 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
33 import org.opendaylight.controller.cluster.raft.TestActorFactory;
34 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
35 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
36 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
37 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
38 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
39 import org.opendaylight.controller.cluster.raft.persisted.SimpleReplicatedLogEntry;
40 import org.opendaylight.controller.cluster.raft.policy.RaftPolicy;
41 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
42 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
43 import org.slf4j.LoggerFactory;
45 public abstract class AbstractRaftActorBehaviorTest<T extends RaftActorBehavior> extends AbstractActorTest {
47 protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
49 private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
50 Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
52 RaftActorBehavior behavior;
55 public void tearDown() throws Exception {
56 if (behavior != null) {
64 * This test checks that when a new Raft RPC message is received with a newer
65 * term the RaftActor gets into the Follower state.
68 public void testHandleRaftRPCWithNewerTerm() throws Exception {
69 MockRaftActorContext actorContext = createActorContext();
71 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
72 createAppendEntriesWithNewerTerm());
74 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
75 createAppendEntriesReplyWithNewerTerm());
77 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
78 createRequestVoteWithNewerTerm());
80 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
81 createRequestVoteReplyWithNewerTerm());
86 * This test verifies that when an AppendEntries is received with a term that
87 * is less that the currentTerm of the RaftActor then the RaftActor does not
88 * change it's state and it responds back with a failure.
91 public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() throws Exception {
92 MockRaftActorContext context = createActorContext();
93 short payloadVersion = 5;
94 context.setPayloadVersion(payloadVersion);
96 // First set the receivers term to a high number (1000)
97 context.getTermInformation().update(1000, "test");
99 AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, null, 101, -1, (short)4);
101 behavior = createBehavior(context);
103 RaftState expected = behavior.state();
105 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
107 assertEquals("Raft state", expected, raftBehavior.state());
109 // Also expect an AppendEntriesReply to be sent where success is false
111 AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
112 behaviorActor, AppendEntriesReply.class);
114 assertEquals("isSuccess", false, reply.isSuccess());
115 assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
120 public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
121 MockRaftActorContext context = createActorContext();
123 context.getTermInformation().update(2, "test");
125 // Prepare the receivers log
126 MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
127 setLastLogEntry(context, 2, 0, payload);
129 List<ReplicatedLogEntry> entries = new ArrayList<>();
130 entries.add(new SimpleReplicatedLogEntry(0, 2, payload));
132 final AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
134 behavior = createBehavior(context);
136 assertFalse("This test should be overridden when testing Candidate", behavior instanceof Candidate);
138 RaftState expected = behavior.state();
140 // Check that the behavior does not handle unknwon message
141 assertNull(behavior.handleMessage(behaviorActor, "unknown"));
143 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
145 assertEquals("Raft state", expected, raftBehavior.state());
147 assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
149 handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
152 protected void handleAppendEntriesAddSameEntryToLogReply(TestActorRef<MessageCollectorActor> replyActor)
154 AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
155 Assert.assertNull("Expected no AppendEntriesReply", reply);
159 * This test verifies that when a RequestVote is received by the RaftActor
160 * with the senders' log is more up to date than the receiver that the receiver grants
161 * the vote to the sender.
164 public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
165 MockRaftActorContext context = createActorContext();
167 behavior = createBehavior(context);
169 context.getTermInformation().update(1, "test");
171 behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
172 "test", 10000, 999));
174 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
175 RequestVoteReply.class);
176 assertEquals("isVoteGranted", true, reply.isVoteGranted());
180 * This test verifies that when a RaftActor receives a RequestVote message
181 * with a term that is greater than it's currentTerm but a less up-to-date
182 * log then the receiving RaftActor will not grant the vote to the sender.
185 public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
186 MockRaftActorContext context = createActorContext();
188 behavior = createBehavior(context);
190 context.getTermInformation().update(1, "test");
193 setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
194 new MockRaftActorContext.MockPayload(""));
196 behavior.handleMessage(behaviorActor, new RequestVote(
197 context.getTermInformation().getCurrentTerm(), "test",
198 index - 1, context.getTermInformation().getCurrentTerm()));
200 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
201 RequestVoteReply.class);
202 assertEquals("isVoteGranted", false, reply.isVoteGranted());
208 * This test verifies that the receiving RaftActor will not grant a vote
209 * to a sender if the sender's term is lesser than the currentTerm of the
210 * recipient RaftActor.
213 public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
214 MockRaftActorContext context = createActorContext();
216 context.getTermInformation().update(1000, null);
218 behavior = createBehavior(context);
220 behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
222 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
223 RequestVoteReply.class);
224 assertEquals("isVoteGranted", false, reply.isVoteGranted());
228 public void testPerformSnapshot() {
229 MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
230 AbstractRaftActorBehavior abstractBehavior = (AbstractRaftActorBehavior) createBehavior(context);
231 if (abstractBehavior instanceof Candidate) {
235 context.getTermInformation().update(1, "test");
237 //log has 1 entry with replicatedToAllIndex = 0, does not do anything, returns the
238 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 1, 1).build());
239 context.setLastApplied(0);
240 abstractBehavior.performSnapshotWithoutCapture(0);
241 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
242 assertEquals(1, context.getReplicatedLog().size());
244 //2 entries, lastApplied still 0, no purging.
245 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
246 context.setLastApplied(0);
247 abstractBehavior.performSnapshotWithoutCapture(0);
248 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
249 assertEquals(2, context.getReplicatedLog().size());
251 //2 entries, lastApplied still 0, no purging.
252 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
253 context.setLastApplied(1);
254 abstractBehavior.performSnapshotWithoutCapture(0);
255 assertEquals(0, abstractBehavior.getReplicatedToAllIndex());
256 assertEquals(1, context.getReplicatedLog().size());
258 // 5 entries, lastApplied =2 and replicatedIndex = 3, but since we want to keep the lastapplied, indices 0 and
259 // 1 will only get purged
260 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 5, 1).build());
261 context.setLastApplied(2);
262 abstractBehavior.performSnapshotWithoutCapture(3);
263 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
264 assertEquals(3, context.getReplicatedLog().size());
266 // scenario where Last applied > Replicated to all index (becoz of a slow follower)
267 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 3, 1).build());
268 context.setLastApplied(2);
269 abstractBehavior.performSnapshotWithoutCapture(1);
270 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
271 assertEquals(1, context.getReplicatedLog().size());
275 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(MockRaftActorContext actorContext,
276 ActorRef actorRef, RaftRPC rpc) throws Exception {
278 Payload payload = new MockRaftActorContext.MockPayload("");
279 setLastLogEntry(actorContext, 1, 0, payload);
280 actorContext.getTermInformation().update(1, "test");
282 RaftActorBehavior origBehavior = createBehavior(actorContext);
283 RaftActorBehavior raftBehavior = origBehavior.handleMessage(actorRef, rpc);
285 assertEquals("New raft state", RaftState.Follower, raftBehavior.state());
286 assertEquals("New election term", rpc.getTerm(), actorContext.getTermInformation().getCurrentTerm());
288 origBehavior.close();
289 raftBehavior.close();
292 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
293 MockRaftActorContext actorContext, long term, long index, Payload data) {
294 return setLastLogEntry(actorContext, new SimpleReplicatedLogEntry(index, term, data));
297 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(MockRaftActorContext actorContext,
298 ReplicatedLogEntry logEntry) {
299 MockRaftActorContext.SimpleReplicatedLog log = new MockRaftActorContext.SimpleReplicatedLog();
300 log.append(logEntry);
301 actorContext.setReplicatedLog(log);
306 protected abstract T createBehavior(RaftActorContext actorContext);
308 protected final T createBehavior(MockRaftActorContext actorContext) {
309 T ret = createBehavior((RaftActorContext)actorContext);
310 actorContext.setCurrentBehavior(ret);
314 protected RaftActorBehavior createBehavior() {
315 return createBehavior(createActorContext());
318 protected MockRaftActorContext createActorContext() {
319 return new MockRaftActorContext();
322 protected MockRaftActorContext createActorContext(ActorRef actor) {
323 return new MockRaftActorContext("test", getSystem(), actor);
326 protected AppendEntries createAppendEntriesWithNewerTerm() {
327 return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1, (short)0);
330 protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
331 return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
334 protected RequestVote createRequestVoteWithNewerTerm() {
335 return new RequestVote(100, "candidate-1", 10, 100);
338 protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
339 return new RequestVoteReply(100, false);
342 protected ByteString toByteString(Map<String, String> state) {
343 ByteArrayOutputStream bos = new ByteArrayOutputStream();
344 try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
345 oos.writeObject(state);
346 return ByteString.copyFrom(bos.toByteArray());
347 } catch (IOException e) {
348 throw new AssertionError("IOException occurred converting Map to Bytestring", e);
352 protected void logStart(String name) {
353 LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);
356 protected RaftPolicy createRaftPolicy(final boolean automaticElectionsEnabled,
357 final boolean applyModificationToStateBeforeConsensus) {
358 return new RaftPolicy() {
360 public boolean automaticElectionsEnabled() {
361 return automaticElectionsEnabled;
365 public boolean applyModificationToStateBeforeConsensus() {
366 return applyModificationToStateBeforeConsensus;