df219436007be602f73946e178f8bf510577df84
[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
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;
24 import java.util.Map;
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;
44
45 public abstract class AbstractRaftActorBehaviorTest<T extends RaftActorBehavior> extends AbstractActorTest {
46
47     protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
48
49     private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
50             Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
51
52     RaftActorBehavior behavior;
53
54     @After
55     public void tearDown() throws Exception {
56         if (behavior != null) {
57             behavior.close();
58         }
59
60         actorFactory.close();
61     }
62
63     /**
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.
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     @Test
91     public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() throws Exception {
92         MockRaftActorContext context = createActorContext();
93         short payloadVersion = 5;
94         context.setPayloadVersion(payloadVersion);
95
96         // First set the receivers term to a high number (1000)
97         context.getTermInformation().update(1000, "test");
98
99         AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, null, 101, -1, (short)4);
100
101         behavior = createBehavior(context);
102
103         RaftState expected = behavior.state();
104
105         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
106
107         assertEquals("Raft state", expected, raftBehavior.state());
108
109         // Also expect an AppendEntriesReply to be sent where success is false
110
111         AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
112                 behaviorActor, AppendEntriesReply.class);
113
114         assertEquals("isSuccess", false, reply.isSuccess());
115         assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
116     }
117
118
119     @Test
120     public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
121         MockRaftActorContext context = createActorContext();
122
123         context.getTermInformation().update(2, "test");
124
125         // Prepare the receivers log
126         MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
127         setLastLogEntry(context, 2, 0, payload);
128
129         List<ReplicatedLogEntry> entries = new ArrayList<>();
130         entries.add(new SimpleReplicatedLogEntry(0, 2, payload));
131
132         final AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
133
134         behavior = createBehavior(context);
135
136         assertFalse("This test should be overridden when testing Candidate", behavior instanceof Candidate);
137
138         RaftState expected = behavior.state();
139
140         // Check that the behavior does not handle unknwon message
141         assertNull(behavior.handleMessage(behaviorActor, "unknown"));
142
143         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
144
145         assertEquals("Raft state", expected, raftBehavior.state());
146
147         assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
148
149         handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
150     }
151
152     protected void handleAppendEntriesAddSameEntryToLogReply(TestActorRef<MessageCollectorActor> replyActor)
153             throws Exception {
154         AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
155         Assert.assertNull("Expected no AppendEntriesReply", reply);
156     }
157
158     /**
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.
162      */
163     @Test
164     public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
165         MockRaftActorContext context = createActorContext();
166
167         behavior = createBehavior(context);
168
169         context.getTermInformation().update(1, "test");
170
171         behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
172                 "test", 10000, 999));
173
174         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
175                 RequestVoteReply.class);
176         assertEquals("isVoteGranted", true, reply.isVoteGranted());
177     }
178
179     /**
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.
183      */
184     @Test
185     public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
186         MockRaftActorContext context = createActorContext();
187
188         behavior = createBehavior(context);
189
190         context.getTermInformation().update(1, "test");
191
192         int index = 2000;
193         setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
194                 new MockRaftActorContext.MockPayload(""));
195
196         behavior.handleMessage(behaviorActor, new RequestVote(
197                 context.getTermInformation().getCurrentTerm(), "test",
198                 index - 1, context.getTermInformation().getCurrentTerm()));
199
200         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
201                 RequestVoteReply.class);
202         assertEquals("isVoteGranted", false, reply.isVoteGranted());
203     }
204
205
206
207     /**
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.
211      */
212     @Test
213     public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
214         MockRaftActorContext context = createActorContext();
215
216         context.getTermInformation().update(1000, null);
217
218         behavior = createBehavior(context);
219
220         behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
221
222         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
223                 RequestVoteReply.class);
224         assertEquals("isVoteGranted", false, reply.isVoteGranted());
225     }
226
227     @Test
228     public void testPerformSnapshot() {
229         MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
230         AbstractRaftActorBehavior abstractBehavior =  (AbstractRaftActorBehavior) createBehavior(context);
231         if (abstractBehavior instanceof Candidate) {
232             return;
233         }
234
235         context.getTermInformation().update(1, "test");
236
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());
243
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());
250
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());
257
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());
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 payload = new MockRaftActorContext.MockPayload("");
279         setLastLogEntry(actorContext, 1, 0, payload);
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, new SimpleReplicatedLogEntry(index, term, data));
295     }
296
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);
302
303         return log;
304     }
305
306     protected abstract T createBehavior(RaftActorContext actorContext);
307
308     protected final T createBehavior(MockRaftActorContext actorContext) {
309         T ret = createBehavior((RaftActorContext)actorContext);
310         actorContext.setCurrentBehavior(ret);
311         return ret;
312     }
313
314     protected RaftActorBehavior createBehavior() {
315         return createBehavior(createActorContext());
316     }
317
318     protected MockRaftActorContext createActorContext() {
319         return new MockRaftActorContext();
320     }
321
322     protected MockRaftActorContext createActorContext(ActorRef actor) {
323         return new MockRaftActorContext("test", getSystem(), actor);
324     }
325
326     protected AppendEntries createAppendEntriesWithNewerTerm() {
327         return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1, (short)0);
328     }
329
330     protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
331         return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
332     }
333
334     protected RequestVote createRequestVoteWithNewerTerm() {
335         return new RequestVote(100, "candidate-1", 10, 100);
336     }
337
338     protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
339         return new RequestVoteReply(100, false);
340     }
341
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);
349         }
350     }
351
352     protected void logStart(String name) {
353         LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);
354     }
355
356     protected RaftPolicy createRaftPolicy(final boolean automaticElectionsEnabled,
357                                           final boolean applyModificationToStateBeforeConsensus) {
358         return new RaftPolicy() {
359             @Override
360             public boolean automaticElectionsEnabled() {
361                 return automaticElectionsEnabled;
362             }
363
364             @Override
365             public boolean applyModificationToStateBeforeConsensus() {
366                 return applyModificationToStateBeforeConsensus;
367             }
368         };
369     }
370 }