b5be834fec064d21d0c40a10000c413c9fd64f60
[controller.git] / opendaylight / md-sal / sal-akka-raft / src / test / java / org / opendaylight / controller / cluster / raft / behaviors / AbstractRaftActorBehaviorTest.java
1 /*
2  * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.controller.cluster.raft.behaviors;
10
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertFalse;
13 import static org.junit.Assert.assertNull;
14 import akka.actor.ActorRef;
15 import akka.actor.Props;
16 import akka.testkit.TestActorRef;
17 import com.google.protobuf.ByteString;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.io.ObjectOutputStream;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
24 import org.junit.After;
25 import org.junit.Assert;
26 import org.junit.Test;
27 import org.opendaylight.controller.cluster.raft.AbstractActorTest;
28 import org.opendaylight.controller.cluster.raft.MockRaftActorContext;
29 import org.opendaylight.controller.cluster.raft.RaftActorContext;
30 import org.opendaylight.controller.cluster.raft.RaftState;
31 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
32 import org.opendaylight.controller.cluster.raft.SerializationUtils;
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.policy.RaftPolicy;
40 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
41 import org.opendaylight.controller.cluster.raft.utils.MessageCollectorActor;
42 import org.slf4j.LoggerFactory;
43
44 public abstract class AbstractRaftActorBehaviorTest<T extends RaftActorBehavior> extends AbstractActorTest {
45
46     protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
47
48     private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
49             Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
50
51     RaftActorBehavior behavior;
52
53     @After
54     public void tearDown() throws Exception {
55         if(behavior != null) {
56             behavior.close();
57         }
58
59         actorFactory.close();
60     }
61
62     /**
63      * This test checks that when a new Raft RPC message is received with a newer
64      * term the RaftActor gets into the Follower state.
65      *
66      * @throws Exception
67      */
68     @Test
69     public void testHandleRaftRPCWithNewerTerm() throws Exception {
70         MockRaftActorContext actorContext = createActorContext();
71
72         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
73                 createAppendEntriesWithNewerTerm());
74
75         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
76                 createAppendEntriesReplyWithNewerTerm());
77
78         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
79                 createRequestVoteWithNewerTerm());
80
81         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
82                 createRequestVoteReplyWithNewerTerm());
83     }
84
85
86     /**
87      * This test verifies that when an AppendEntries is received with a term that
88      * is less that the currentTerm of the RaftActor then the RaftActor does not
89      * change it's state and it responds back with a failure
90      *
91      * @throws Exception
92      */
93     @Test
94     public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() throws Exception {
95         MockRaftActorContext context = createActorContext();
96         short payloadVersion = 5;
97         context.setPayloadVersion(payloadVersion);
98
99         // First set the receivers term to a high number (1000)
100         context.getTermInformation().update(1000, "test");
101
102         AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, null, 101, -1, (short)4);
103
104         behavior = createBehavior(context);
105
106         RaftState expected = behavior.state();
107
108         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
109
110         assertEquals("Raft state", expected, raftBehavior.state());
111
112         // Also expect an AppendEntriesReply to be sent where success is false
113
114         AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
115                 behaviorActor, AppendEntriesReply.class);
116
117         assertEquals("isSuccess", false, reply.isSuccess());
118         assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
119     }
120
121
122     @Test
123     public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
124         MockRaftActorContext context = createActorContext();
125
126         context.getTermInformation().update(2, "test");
127
128         // Prepare the receivers log
129         MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
130         setLastLogEntry(context, 2, 0, payload);
131
132         List<ReplicatedLogEntry> entries = new ArrayList<>();
133         entries.add(new MockRaftActorContext.MockReplicatedLogEntry(2, 0, payload));
134
135         AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
136
137         behavior = createBehavior(context);
138
139         assertFalse("This test should be overridden when testing Candidate", behavior instanceof Candidate);
140
141         RaftState expected = behavior.state();
142
143         // Check that the behavior does not handle unknwon message
144         assertNull(behavior.handleMessage(behaviorActor, "unknown"));
145
146         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
147
148         assertEquals("Raft state", expected, raftBehavior.state());
149
150         assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
151
152         handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
153     }
154
155     protected void handleAppendEntriesAddSameEntryToLogReply(TestActorRef<MessageCollectorActor> replyActor)
156             throws Exception {
157         AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
158         Assert.assertNull("Expected no AppendEntriesReply", reply);
159     }
160
161     /**
162      * This test verifies that when a RequestVote is received by the RaftActor
163      * with the senders' log is more up to date than the receiver that the receiver grants
164      * the vote to the sender.
165      */
166     @Test
167     public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
168         MockRaftActorContext context = createActorContext();
169
170         behavior = createBehavior(context);
171
172         context.getTermInformation().update(1, "test");
173
174         behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
175                 "test", 10000, 999));
176
177         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
178                 RequestVoteReply.class);
179         assertEquals("isVoteGranted", true, reply.isVoteGranted());
180     }
181
182     /**
183      * This test verifies that when a RaftActor receives a RequestVote message
184      * with a term that is greater than it's currentTerm but a less up-to-date
185      * log then the receiving RaftActor will not grant the vote to the sender
186      */
187     @Test
188     public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
189         MockRaftActorContext context = createActorContext();
190
191         behavior = createBehavior(context);
192
193         context.getTermInformation().update(1, "test");
194
195         int index = 2000;
196         setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
197                 new MockRaftActorContext.MockPayload(""));
198
199         behavior.handleMessage(behaviorActor, new RequestVote(
200                 context.getTermInformation().getCurrentTerm(), "test",
201                 index - 1, context.getTermInformation().getCurrentTerm()));
202
203         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
204                 RequestVoteReply.class);
205         assertEquals("isVoteGranted", false, reply.isVoteGranted());
206     }
207
208
209
210     /**
211      * This test verifies that the receiving RaftActor will not grant a vote
212      * to a sender if the sender's term is lesser than the currentTerm of the
213      * recipient RaftActor
214      */
215     @Test
216     public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
217         MockRaftActorContext context = createActorContext();
218
219         context.getTermInformation().update(1000, null);
220
221         behavior = createBehavior(context);
222
223         behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
224
225         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
226                 RequestVoteReply.class);
227         assertEquals("isVoteGranted", false, reply.isVoteGranted());
228     }
229
230     @Test
231     public void testPerformSnapshot() {
232         MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
233         AbstractRaftActorBehavior abstractBehavior =  (AbstractRaftActorBehavior) createBehavior(context);
234         if (abstractBehavior instanceof Candidate) {
235             return;
236         }
237
238         context.getTermInformation().update(1, "test");
239
240         //log has 1 entry with replicatedToAllIndex = 0, does not do anything, returns the
241         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 1, 1).build());
242         context.setLastApplied(0);
243         abstractBehavior.performSnapshotWithoutCapture(0);
244         assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
245         assertEquals(1, context.getReplicatedLog().size());
246
247         //2 entries, lastApplied still 0, no purging.
248         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
249         context.setLastApplied(0);
250         abstractBehavior.performSnapshotWithoutCapture(0);
251         assertEquals(-1, abstractBehavior.getReplicatedToAllIndex());
252         assertEquals(2, context.getReplicatedLog().size());
253
254         //2 entries, lastApplied still 0, no purging.
255         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 2, 1).build());
256         context.setLastApplied(1);
257         abstractBehavior.performSnapshotWithoutCapture(0);
258         assertEquals(0, abstractBehavior.getReplicatedToAllIndex());
259         assertEquals(1, context.getReplicatedLog().size());
260
261         //5 entries, lastApplied =2 and replicatedIndex = 3, but since we want to keep the lastapplied, indices 0 and 1 will only get purged
262         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 5, 1).build());
263         context.setLastApplied(2);
264         abstractBehavior.performSnapshotWithoutCapture(3);
265         assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
266         assertEquals(3, context.getReplicatedLog().size());
267
268         // scenario where Last applied > Replicated to all index (becoz of a slow follower)
269         context.setReplicatedLog(new MockRaftActorContext.MockReplicatedLogBuilder().createEntries(0, 3, 1).build());
270         context.setLastApplied(2);
271         abstractBehavior.performSnapshotWithoutCapture(1);
272         assertEquals(1, abstractBehavior.getReplicatedToAllIndex());
273         assertEquals(1, context.getReplicatedLog().size());
274     }
275
276
277     protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(MockRaftActorContext actorContext,
278             ActorRef actorRef, RaftRPC rpc) throws Exception {
279
280         Payload p = new MockRaftActorContext.MockPayload("");
281         setLastLogEntry(actorContext, 1, 0, p);
282         actorContext.getTermInformation().update(1, "test");
283
284         RaftActorBehavior origBehavior = createBehavior(actorContext);
285         RaftActorBehavior raftBehavior = origBehavior.handleMessage(actorRef, rpc);
286
287         assertEquals("New raft state", RaftState.Follower, raftBehavior.state());
288         assertEquals("New election term", rpc.getTerm(), actorContext.getTermInformation().getCurrentTerm());
289
290         origBehavior.close();
291         raftBehavior.close();
292     }
293
294     protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(
295         MockRaftActorContext actorContext, long term, long index, Payload data) {
296         return setLastLogEntry(actorContext,
297             new MockRaftActorContext.MockReplicatedLogEntry(term, index, data));
298     }
299
300     protected MockRaftActorContext.SimpleReplicatedLog setLastLogEntry(MockRaftActorContext actorContext,
301             ReplicatedLogEntry logEntry) {
302         MockRaftActorContext.SimpleReplicatedLog log = new MockRaftActorContext.SimpleReplicatedLog();
303         log.append(logEntry);
304         actorContext.setReplicatedLog(log);
305
306         return log;
307     }
308
309     protected abstract T createBehavior(RaftActorContext actorContext);
310
311     protected final T createBehavior(MockRaftActorContext actorContext) {
312         T ret = createBehavior((RaftActorContext)actorContext);
313         actorContext.setCurrentBehavior(ret);
314         return ret;
315     }
316
317     protected RaftActorBehavior createBehavior() {
318         return createBehavior(createActorContext());
319     }
320
321     protected MockRaftActorContext createActorContext() {
322         return new MockRaftActorContext();
323     }
324
325     protected MockRaftActorContext createActorContext(ActorRef actor) {
326         return new MockRaftActorContext("test", getSystem(), actor);
327     }
328
329     protected AppendEntries createAppendEntriesWithNewerTerm() {
330         return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1, (short)0);
331     }
332
333     protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
334         return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
335     }
336
337     protected RequestVote createRequestVoteWithNewerTerm() {
338         return new RequestVote(100, "candidate-1", 10, 100);
339     }
340
341     protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
342         return new RequestVoteReply(100, false);
343     }
344
345     protected Object fromSerializableMessage(Object serializable){
346         return SerializationUtils.fromSerializable(serializable);
347     }
348
349     protected ByteString toByteString(Map<String, String> state) {
350         ByteArrayOutputStream bos = new ByteArrayOutputStream();
351         try(ObjectOutputStream oos = new ObjectOutputStream(bos)) {
352             oos.writeObject(state);
353             return ByteString.copyFrom(bos.toByteArray());
354         } catch (IOException e) {
355             throw new AssertionError("IOException occurred converting Map to Bytestring", e);
356         }
357     }
358
359     protected void logStart(String name) {
360         LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);
361     }
362
363     protected RaftPolicy createRaftPolicy(final boolean automaticElectionsEnabled,
364                                           final boolean applyModificationToStateBeforeConsensus){
365         return new RaftPolicy() {
366             @Override
367             public boolean automaticElectionsEnabled() {
368                 return automaticElectionsEnabled;
369             }
370
371             @Override
372             public boolean applyModificationToStateBeforeConsensus() {
373                 return applyModificationToStateBeforeConsensus;
374             }
375         };
376     }
377 }