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