1 package org.opendaylight.controller.cluster.raft.behaviors;
3 import static org.junit.Assert.assertEquals;
4 import akka.actor.ActorRef;
5 import akka.actor.Props;
6 import akka.testkit.TestActorRef;
7 import com.google.protobuf.ByteString;
8 import java.io.ByteArrayOutputStream;
9 import java.io.IOException;
10 import java.io.ObjectOutputStream;
11 import java.util.ArrayList;
12 import java.util.List;
14 import org.junit.After;
15 import org.junit.Assert;
16 import org.junit.Test;
17 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
18 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
19 import org.opendaylight.controller.cluster.raft.RaftActorContext;
20 import org.opendaylight.controller.cluster.raft.RaftState;
21 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
22 import org.opendaylight.controller.cluster.raft.SerializationUtils;
23 import org.opendaylight.controller.cluster.raft.TestActorFactory;
24 import org.opendaylight.controller.cluster.raft.messages.AppendEntries;
25 import org.opendaylight.controller.cluster.raft.messages.AppendEntriesReply;
26 import org.opendaylight.controller.cluster.raft.messages.RaftRPC;
27 import org.opendaylight.controller.cluster.raft.messages.RequestVote;
28 import org.opendaylight.controller.cluster.raft.messages.RequestVoteReply;
29 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
30 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
31 import org.slf4j.LoggerFactory;
33 public abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
35 protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
37 private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
38 Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
40 RaftActorBehavior behavior;
43 public void tearDown() throws Exception {
44 if(behavior != null) {
52 * This test checks that when a new Raft RPC message is received with a newer
53 * term the RaftActor gets into the Follower state.
58 public void testHandleRaftRPCWithNewerTerm() throws Exception {
59 RaftActorContext actorContext = createActorContext();
61 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
62 createAppendEntriesWithNewerTerm());
64 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
65 createAppendEntriesReplyWithNewerTerm());
67 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
68 createRequestVoteWithNewerTerm());
70 assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
71 createRequestVoteReplyWithNewerTerm());
76 * This test verifies that when an AppendEntries is received with a term that
77 * is less that the currentTerm of the RaftActor then the RaftActor does not
78 * change it's state and it responds back with a failure
83 public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() throws Exception {
84 MockRaftActorContext context = createActorContext();
85 short payloadVersion = 5;
86 context.setPayloadVersion(payloadVersion);
88 // First set the receivers term to a high number (1000)
89 context.getTermInformation().update(1000, "test");
91 AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, null, 101, -1, (short)4);
93 behavior = createBehavior(context);
95 // Send an unknown message so that the state of the RaftActor remains unchanged
96 RaftActorBehavior expected = behavior.handleMessage(behaviorActor, "unknown");
98 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
100 assertEquals("Raft state", expected.state(), raftBehavior.state());
102 // Also expect an AppendEntriesReply to be sent where success is false
104 AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
105 behaviorActor, AppendEntriesReply.class);
107 assertEquals("isSuccess", false, reply.isSuccess());
108 assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
113 public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
114 MockRaftActorContext context = createActorContext();
116 context.getTermInformation().update(2, "test");
118 // Prepare the receivers log
119 MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
120 setLastLogEntry(context, 2, 0, payload);
122 List<ReplicatedLogEntry> entries = new ArrayList<>();
123 entries.add(new MockRaftActorContext.MockReplicatedLogEntry(2, 0, payload));
125 AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
127 behavior = createBehavior(context);
129 if (behavior instanceof Candidate) {
130 // Resetting the Candidates term to make sure it will match
131 // the term sent by AppendEntries. If this was not done then
132 // the test will fail because the Candidate will assume that
133 // the message was sent to it from a lower term peer and will
134 // thus respond with a failure
135 context.getTermInformation().update(2, "test");
138 // Send an unknown message so that the state of the RaftActor remains unchanged
139 RaftActorBehavior expected = behavior.handleMessage(behaviorActor, "unknown");
141 RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
143 assertEquals("Raft state", expected.state(), raftBehavior.state());
145 assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
147 handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
150 protected void handleAppendEntriesAddSameEntryToLogReply(TestActorRef<MessageCollectorActor> replyActor)
152 AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
153 Assert.assertNull("Expected no AppendEntriesReply", reply);
157 * This test verifies that when a RequestVote is received by the RaftActor
158 * with the senders' log is more up to date than the receiver that the receiver grants
159 * the vote to the sender.
162 public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
163 MockRaftActorContext context = createActorContext();
165 behavior = createBehavior(context);
167 context.getTermInformation().update(1, "test");
169 behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
170 "test", 10000, 999));
172 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
173 RequestVoteReply.class);
174 assertEquals("isVoteGranted", true, reply.isVoteGranted());
178 * This test verifies that when a RaftActor receives a RequestVote message
179 * with a term that is greater than it's currentTerm but a less up-to-date
180 * log then the receiving RaftActor will not grant the vote to the sender
183 public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
184 MockRaftActorContext context = createActorContext();
186 behavior = createBehavior(context);
188 context.getTermInformation().update(1, "test");
191 setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
192 new MockRaftActorContext.MockPayload(""));
194 behavior.handleMessage(behaviorActor, new RequestVote(
195 context.getTermInformation().getCurrentTerm(), "test",
196 index - 1, context.getTermInformation().getCurrentTerm()));
198 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
199 RequestVoteReply.class);
200 assertEquals("isVoteGranted", false, reply.isVoteGranted());
206 * This test verifies that the receiving RaftActor will not grant a vote
207 * to a sender if the sender's term is lesser than the currentTerm of the
208 * recipient RaftActor
211 public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
212 RaftActorContext context = createActorContext();
214 context.getTermInformation().update(1000, null);
216 behavior = createBehavior(context);
218 behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
220 RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
221 RequestVoteReply.class);
222 assertEquals("isVoteGranted", false, reply.isVoteGranted());
226 public void testPerformSnapshot() {
227 MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
228 AbstractRaftActorBehavior abstractBehavior = (AbstractRaftActorBehavior) createBehavior(context);
229 if (abstractBehavior instanceof Candidate) {
233 context.getTermInformation().update(1, "test");
235 //log has 1 entry with replicatedToAllIndex = 0, does not do anything, returns the
236 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 1, 1).build());
237 context.setLastApplied(0);
238 abstractBehavior.performSnapshotWithoutCapture(0);
239 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
240 assertEquals(1, context.getReplicatedLog().size());
242 //2 entries, lastApplied still 0, no purging.
243 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
244 context.setLastApplied(0);
245 abstractBehavior.performSnapshotWithoutCapture(0);
246 assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
247 assertEquals(2, context.getReplicatedLog().size());
249 //2 entries, lastApplied still 0, no purging.
250 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
251 context.setLastApplied(1);
252 abstractBehavior.performSnapshotWithoutCapture(0);
253 assertEquals(0, abstractBehavior.getReplicatedToAllIndex());
254 assertEquals(1, context.getReplicatedLog().size());
256 //5 entries, lastApplied =2 and replicatedIndex = 3, but since we want to keep the lastapplied, indices 0 and 1 will only get purged
257 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 5, 1).build());
258 context.setLastApplied(2);
259 abstractBehavior.performSnapshotWithoutCapture(3);
260 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
261 assertEquals(3, context.getReplicatedLog().size());
263 // scenario where Last applied > Replicated to all index (becoz of a slow follower)
264 context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 3, 1).build());
265 context.setLastApplied(2);
266 abstractBehavior.performSnapshotWithoutCapture(1);
267 assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
268 assertEquals(1, context.getReplicatedLog().size());
272 protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(RaftActorContext actorContext,
273 ActorRef actorRef, RaftRPC rpc) throws Exception {
275 Payload p = new MockRaftActorContext.MockPayload("");
276 setLastLogEntry((MockRaftActorContext) actorContext, 1, 0, p);
277 actorContext.getTermInformation().update(1, "test");
279 RaftActorBehavior origBehavior = createBehavior(actorContext);
280 RaftActorBehavior raftBehavior = origBehavior.handleMessage(actorRef, rpc);
282 assertEquals("New raft state", RaftState.Follower, raftBehavior.state());
283 assertEquals("New election term", rpc.getTerm(), actorContext.getTermInformation().getCurrentTerm());
285 origBehavior.close();
286 raftBehavior.close();
289 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
290 MockRaftActorContext actorContext, long term, long index, Payload data) {
291 return setLastLogEntry(actorContext,
292 new MockRaftActorContext.MockReplicatedLogEntry(term, index, data));
295 protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(MockRaftActorContext actorContext,
296 ReplicatedLogEntry logEntry) {
297 MockRaftActorContext.SimpleReplicatedLog log = new MockRaftActorContext.SimpleReplicatedLog();
298 log.append(logEntry);
299 actorContext.setReplicatedLog(log);
304 protected abstract RaftActorBehavior createBehavior(
305 RaftActorContext actorContext);
307 protected RaftActorBehavior createBehavior() {
308 return createBehavior(createActorContext());
311 protected MockRaftActorContext createActorContext() {
312 return new MockRaftActorContext();
315 protected MockRaftActorContext createActorContext(ActorRef actor) {
316 return new MockRaftActorContext("test", getSystem(), actor);
319 protected AppendEntries createAppendEntriesWithNewerTerm() {
320 return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1, (short)0);
323 protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
324 return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
327 protected RequestVote createRequestVoteWithNewerTerm() {
328 return new RequestVote(100, "candidate-1", 10, 100);
331 protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
332 return new RequestVoteReply(100, false);
335 protected Object fromSerializableMessage(Object serializable){
336 return SerializationUtils.fromSerializable(serializable);
339 protected ByteString toByteString(Map<String, String> state) {
340 ByteArrayOutputStream bos = new ByteArrayOutputStream();
341 try(ObjectOutputStream oos = new ObjectOutputStream(bos)) {
342 oos.writeObject(state);
343 return ByteString.copyFrom(bos.toByteArray());
344 } catch (IOException e) {
345 throw new AssertionError("IOException occurred converting Map to Bytestring", e);
349 protected void logStart(String name) {
350 LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);