Bug 3020: Add version to AppendEntries and AppendEntriesReply
[controller.git] / opendaylight / md-sal / sal-akka-raft / src / test / java / org / opendaylight / controller / cluster / raft / behaviors / AbstractRaftActorBehaviorTest.java
1 package org.opendaylight.controller.cluster.raft.behaviors;
2
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;
13 import java.util.Map;
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;
32
33 public abstract class AbstractRaftActorBehaviorTest extends AbstractActorTest {
34
35     protected final TestActorFactory actorFactory = new TestActorFactory(getSystem());
36
37     private final TestActorRef<MessageCollectorActor> behaviorActor = actorFactory.createTestActor(
38             Props.create(MessageCollectorActor.class), actorFactory.generateActorId("behavior"));
39
40     RaftActorBehavior behavior;
41
42     @After
43     public void tearDown() throws Exception {
44         if(behavior != null) {
45             behavior.close();
46         }
47
48         actorFactory.close();
49     }
50
51     /**
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.
54      *
55      * @throws Exception
56      */
57     @Test
58     public void testHandleRaftRPCWithNewerTerm() throws Exception {
59         RaftActorContext actorContext = createActorContext();
60
61         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
62                 createAppendEntriesWithNewerTerm());
63
64         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
65                 createAppendEntriesReplyWithNewerTerm());
66
67         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
68                 createRequestVoteWithNewerTerm());
69
70         assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(actorContext, behaviorActor,
71                 createRequestVoteReplyWithNewerTerm());
72     }
73
74
75     /**
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
79      *
80      * @throws Exception
81      */
82     @Test
83     public void testHandleAppendEntriesSenderTermLessThanReceiverTerm() throws Exception {
84         MockRaftActorContext context = createActorContext();
85         short payloadVersion = 5;
86         context.setPayloadVersion(payloadVersion);
87
88         // First set the receivers term to a high number (1000)
89         context.getTermInformation().update(1000, "test");
90
91         AppendEntries appendEntries = new AppendEntries(100, "leader-1", 0, 0, null, 101, -1, (short)4);
92
93         behavior = createBehavior(context);
94
95         // Send an unknown message so that the state of the RaftActor remains unchanged
96         RaftActorBehavior expected = behavior.handleMessage(behaviorActor, "unknown");
97
98         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
99
100         assertEquals("Raft state", expected.state(), raftBehavior.state());
101
102         // Also expect an AppendEntriesReply to be sent where success is false
103
104         AppendEntriesReply reply = MessageCollectorActor.expectFirstMatching(
105                 behaviorActor, AppendEntriesReply.class);
106
107         assertEquals("isSuccess", false, reply.isSuccess());
108         assertEquals("getPayloadVersion", payloadVersion, reply.getPayloadVersion());
109     }
110
111
112     @Test
113     public void testHandleAppendEntriesAddSameEntryToLog() throws Exception {
114         MockRaftActorContext context = createActorContext();
115
116         context.getTermInformation().update(2, "test");
117
118         // Prepare the receivers log
119         MockRaftActorContext.MockPayload payload = new MockRaftActorContext.MockPayload("zero");
120         setLastLogEntry(context, 2, 0, payload);
121
122         List<ReplicatedLogEntry> entries = new ArrayList<>();
123         entries.add(new MockRaftActorContext.MockReplicatedLogEntry(2, 0, payload));
124
125         AppendEntries appendEntries = new AppendEntries(2, "leader-1", -1, -1, entries, 2, -1, (short)0);
126
127         behavior = createBehavior(context);
128
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");
136         }
137
138         // Send an unknown message so that the state of the RaftActor remains unchanged
139         RaftActorBehavior expected = behavior.handleMessage(behaviorActor, "unknown");
140
141         RaftActorBehavior raftBehavior = behavior.handleMessage(behaviorActor, appendEntries);
142
143         assertEquals("Raft state", expected.state(), raftBehavior.state());
144
145         assertEquals("ReplicatedLog size", 1, context.getReplicatedLog().size());
146
147         handleAppendEntriesAddSameEntryToLogReply(behaviorActor);
148     }
149
150     protected void handleAppendEntriesAddSameEntryToLogReply(TestActorRef<MessageCollectorActor> replyActor)
151             throws Exception {
152         AppendEntriesReply reply = MessageCollectorActor.getFirstMatching(replyActor, AppendEntriesReply.class);
153         Assert.assertNull("Expected no AppendEntriesReply", reply);
154     }
155
156     /**
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.
160      */
161     @Test
162     public void testHandleRequestVoteWhenSenderLogMoreUpToDate() {
163         MockRaftActorContext context = createActorContext();
164
165         behavior = createBehavior(context);
166
167         context.getTermInformation().update(1, "test");
168
169         behavior.handleMessage(behaviorActor, new RequestVote(context.getTermInformation().getCurrentTerm(),
170                 "test", 10000, 999));
171
172         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
173                 RequestVoteReply.class);
174         assertEquals("isVoteGranted", true, reply.isVoteGranted());
175     }
176
177     /**
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
181      */
182     @Test
183     public void testHandleRequestVoteWhenSenderLogLessUptoDate() {
184         MockRaftActorContext context = createActorContext();
185
186         behavior = createBehavior(context);
187
188         context.getTermInformation().update(1, "test");
189
190         int index = 2000;
191         setLastLogEntry(context, context.getTermInformation().getCurrentTerm(), index,
192                 new MockRaftActorContext.MockPayload(""));
193
194         behavior.handleMessage(behaviorActor, new RequestVote(
195                 context.getTermInformation().getCurrentTerm(), "test",
196                 index - 1, context.getTermInformation().getCurrentTerm()));
197
198         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
199                 RequestVoteReply.class);
200         assertEquals("isVoteGranted", false, reply.isVoteGranted());
201     }
202
203
204
205     /**
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
209      */
210     @Test
211     public void testHandleRequestVoteWhenSenderTermLessThanCurrentTerm() {
212         RaftActorContext context = createActorContext();
213
214         context.getTermInformation().update(1000, null);
215
216         behavior = createBehavior(context);
217
218         behavior.handleMessage(behaviorActor, new RequestVote(999, "test", 10000, 999));
219
220         RequestVoteReply reply = MessageCollectorActor.expectFirstMatching(behaviorActor,
221                 RequestVoteReply.class);
222         assertEquals("isVoteGranted", false, reply.isVoteGranted());
223     }
224
225     @Test
226     public void testPerformSnapshot() {
227         MockRaftActorContext context = new MockRaftActorContext("test", getSystem(), behaviorActor);
228         AbstractRaftActorBehavior abstractBehavior =  (AbstractRaftActorBehavior) createBehavior(context);
229         if (abstractBehavior instanceof Candidate) {
230             return;
231         }
232
233         context.getTermInformation().update(1, "test");
234
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());
241
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());
248
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());
255
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());
262
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());
269     }
270
271
272     protected void assertStateChangesToFollowerWhenRaftRPCHasNewerTerm(RaftActorContext actorContext,
273             ActorRef actorRef, RaftRPC rpc) throws Exception {
274
275         Payload p = new MockRaftActorContext.MockPayload("");
276         setLastLogEntry((MockRaftActorContext) actorContext, 1, 0, p);
277         actorContext.getTermInformation().update(1, "test");
278
279         RaftActorBehavior origBehavior = createBehavior(actorContext);
280         RaftActorBehavior raftBehavior = origBehavior.handleMessage(actorRef, rpc);
281
282         assertEquals("New raft state", RaftState.Follower, raftBehavior.state());
283         assertEquals("New election term", rpc.getTerm(), actorContext.getTermInformation().getCurrentTerm());
284
285         origBehavior.close();
286         raftBehavior.close();
287     }
288
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));
293     }
294
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);
300
301         return log;
302     }
303
304     protected abstract RaftActorBehavior createBehavior(
305         RaftActorContext actorContext);
306
307     protected RaftActorBehavior createBehavior() {
308         return createBehavior(createActorContext());
309     }
310
311     protected MockRaftActorContext createActorContext() {
312         return new MockRaftActorContext();
313     }
314
315     protected MockRaftActorContext createActorContext(ActorRef actor) {
316         return new MockRaftActorContext("test", getSystem(), actor);
317     }
318
319     protected AppendEntries createAppendEntriesWithNewerTerm() {
320         return new AppendEntries(100, "leader-1", 0, 0, null, 1, -1, (short)0);
321     }
322
323     protected AppendEntriesReply createAppendEntriesReplyWithNewerTerm() {
324         return new AppendEntriesReply("follower-1", 100, false, 100, 100, (short)0);
325     }
326
327     protected RequestVote createRequestVoteWithNewerTerm() {
328         return new RequestVote(100, "candidate-1", 10, 100);
329     }
330
331     protected RequestVoteReply createRequestVoteReplyWithNewerTerm() {
332         return new RequestVoteReply(100, false);
333     }
334
335     protected Object fromSerializableMessage(Object serializable){
336         return SerializationUtils.fromSerializable(serializable);
337     }
338
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);
346         }
347     }
348
349     protected void logStart(String name) {
350         LoggerFactory.getLogger(LeaderTest.class).info("Starting " + name);
351     }
352 }