+ @Test
+ public void testRemoveServerNonExistentServer() {
+ LOG.info("testRemoveServerNonExistentServer starting");
+
+ RaftActorContext initialActorContext = new MockRaftActorContext();
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(FOLLOWER_ID, followerActor.path().toString()),
+ initialActorContext).withDispatcher(Dispatchers.DefaultDispatcherId()),
+ actorFactory.generateActorId(LEADER_ID));
+
+ leaderActor.tell(new RemoveServer(NEW_SERVER_ID), testKit.getRef());
+ RemoveServerReply removeServerReply = testKit.expectMsgClass(Duration.ofSeconds(5), RemoveServerReply.class);
+ assertEquals("getStatus", ServerChangeStatus.DOES_NOT_EXIST, removeServerReply.getStatus());
+
+ LOG.info("testRemoveServerNonExistentServer ending");
+ }
+
+ @Test
+ public void testRemoveServerForwardToLeader() {
+ LOG.info("testRemoveServerForwardToLeader starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(1, TimeUnit.DAYS));
+
+ ActorRef leaderActor = actorFactory.createTestActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId(LEADER_ID));
+
+ TestActorRef<MockRaftActor> followerRaftActor = actorFactory.createTestActor(
+ MockRaftActor.builder().id(FOLLOWER_ID).peerAddresses(Map.of(LEADER_ID,
+ leaderActor.path().toString())).config(configParams).persistent(Optional.of(false))
+ .props().withDispatcher(Dispatchers.DefaultDispatcherId()),
+ actorFactory.generateActorId(FOLLOWER_ID));
+ followerRaftActor.underlyingActor().waitForInitializeBehaviorComplete();
+
+ followerRaftActor.tell(new AppendEntries(1, LEADER_ID, 0, 1, List.of(), -1, -1, (short)0), leaderActor);
+
+ followerRaftActor.tell(new RemoveServer(FOLLOWER_ID), testKit.getRef());
+ expectFirstMatching(leaderActor, RemoveServer.class);
+
+ LOG.info("testRemoveServerForwardToLeader ending");
+ }
+
+ @Test
+ public void testRemoveServer() {
+ LOG.info("testRemoveServer starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(1, TimeUnit.DAYS));
+ configParams.setCustomRaftPolicyImplementationClass(DisableElectionsRaftPolicy.class.getName());
+
+ final String follower1ActorId = actorFactory.generateActorId(FOLLOWER_ID);
+ final String follower1ActorPath = actorFactory.createTestActorPath(follower1ActorId);
+ final String follower2ActorId = actorFactory.generateActorId(FOLLOWER_ID2);
+ final String follower2ActorPath = actorFactory.createTestActorPath(follower2ActorId);
+ RaftActorContext initialActorContext = new MockRaftActorContext();
+
+ final String downNodeId = "downNode";
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(MockLeaderRaftActor.props(
+ Map.of(FOLLOWER_ID, follower1ActorPath, FOLLOWER_ID2, follower2ActorPath, downNodeId, ""),
+ initialActorContext).withDispatcher(Dispatchers.DefaultDispatcherId()),
+ actorFactory.generateActorId(LEADER_ID));
+
+ final ActorRef leaderCollector = newLeaderCollectorActor(leaderActor.underlyingActor());
+
+ ActorRef follower1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower1Actor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID2, follower2ActorPath, downNodeId, ""), configParams, NO_PERSISTENCE,
+ follower1Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), follower1ActorId);
+
+ ActorRef follower2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower2Actor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID2, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID, follower1ActorPath, downNodeId, ""), configParams, NO_PERSISTENCE,
+ follower2Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), follower2ActorId);
+
+ leaderActor.underlyingActor().waitForInitializeBehaviorComplete();
+ follower1Actor.underlyingActor().waitForInitializeBehaviorComplete();
+ follower2Actor.underlyingActor().waitForInitializeBehaviorComplete();
+
+ leaderActor.tell(new RemoveServer(FOLLOWER_ID), testKit.getRef());
+ RemoveServerReply removeServerReply = testKit.expectMsgClass(Duration.ofSeconds(5), RemoveServerReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, removeServerReply.getStatus());
+
+ ApplyState applyState = MessageCollectorActor.expectFirstMatching(leaderCollector, ApplyState.class);
+ assertEquals(0L, applyState.getReplicatedLogEntry().getIndex());
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ votingServer(LEADER_ID), votingServer(FOLLOWER_ID2), votingServer(downNodeId));
+
+ applyState = MessageCollectorActor.expectFirstMatching(follower2Collector, ApplyState.class);
+ assertEquals(0L, applyState.getReplicatedLogEntry().getIndex());
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ votingServer(LEADER_ID), votingServer(FOLLOWER_ID2), votingServer(downNodeId));
+
+ RaftActorBehavior currentBehavior = leaderActor.underlyingActor().getCurrentBehavior();
+ assertTrue("Expected Leader", currentBehavior instanceof Leader);
+ assertEquals("Follower ids size", 2, ((Leader)currentBehavior).getFollowerIds().size());
+
+ MessageCollectorActor.expectFirstMatching(follower1Collector, ServerRemoved.class);
+
+ LOG.info("testRemoveServer ending");
+ }
+
+ @Test
+ public void testRemoveServerLeader() {
+ LOG.info("testRemoveServerLeader starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(1, TimeUnit.DAYS));
+ configParams.setCustomRaftPolicyImplementationClass(DisableElectionsRaftPolicy.class.getName());
+
+ final String followerActorId = actorFactory.generateActorId(FOLLOWER_ID);
+ final String followerActorPath = actorFactory.createTestActorPath(followerActorId);
+ RaftActorContext initialActorContext = new MockRaftActorContext();
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(FOLLOWER_ID, followerActorPath),
+ initialActorContext).withDispatcher(Dispatchers.DefaultDispatcherId()),
+ actorFactory.generateActorId(LEADER_ID));
+
+ final ActorRef leaderCollector = newLeaderCollectorActor(leaderActor.underlyingActor());
+
+ final ActorRef followerCollector =
+ actorFactory.createActor(MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID, Map.of(LEADER_ID, leaderActor.path().toString()),
+ configParams, NO_PERSISTENCE, followerCollector)
+ .withDispatcher(Dispatchers.DefaultDispatcherId()),
+ followerActorId);
+
+ leaderActor.tell(new RemoveServer(LEADER_ID), testKit.getRef());
+ RemoveServerReply removeServerReply = testKit.expectMsgClass(Duration.ofSeconds(5), RemoveServerReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, removeServerReply.getStatus());
+
+ final ApplyState applyState = MessageCollectorActor.expectFirstMatching(followerCollector, ApplyState.class);
+ assertEquals(0L, applyState.getReplicatedLogEntry().getIndex());
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ votingServer(FOLLOWER_ID));
+
+ MessageCollectorActor.expectFirstMatching(leaderCollector, ServerRemoved.class);
+
+ LOG.info("testRemoveServerLeader ending");
+ }
+
+ @Test
+ public void testRemoveServerLeaderWithNoFollowers() {
+ LOG.info("testRemoveServerLeaderWithNoFollowers starting");
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(),
+ new MockRaftActorContext()).withDispatcher(Dispatchers.DefaultDispatcherId()),
+ actorFactory.generateActorId(LEADER_ID));
+
+ leaderActor.tell(new RemoveServer(LEADER_ID), testKit.getRef());
+ RemoveServerReply removeServerReply = testKit.expectMsgClass(Duration.ofSeconds(5), RemoveServerReply.class);
+ assertEquals("getStatus", ServerChangeStatus.NOT_SUPPORTED, removeServerReply.getStatus());
+
+ LOG.info("testRemoveServerLeaderWithNoFollowers ending");
+ }
+
+ @Test
+ public void testChangeServersVotingStatus() {
+ LOG.info("testChangeServersVotingStatus starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(1, TimeUnit.DAYS));
+ configParams.setCustomRaftPolicyImplementationClass(DisableElectionsRaftPolicy.class.getName());
+
+ final String follower1ActorId = actorFactory.generateActorId(FOLLOWER_ID);
+ final String follower1ActorPath = actorFactory.createTestActorPath(follower1ActorId);
+ final String follower2ActorId = actorFactory.generateActorId(FOLLOWER_ID2);
+ final String follower2ActorPath = actorFactory.createTestActorPath(follower2ActorId);
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(FOLLOWER_ID, follower1ActorPath,
+ FOLLOWER_ID2, follower2ActorPath), new MockRaftActorContext())
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), actorFactory.generateActorId(LEADER_ID));
+ ActorRef leaderCollector = newLeaderCollectorActor(leaderActor.underlyingActor());
+
+ ActorRef follower1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower1RaftActor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID2, follower2ActorPath), configParams, NO_PERSISTENCE, follower1Collector)
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), follower1ActorId);
+
+ ActorRef follower2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower2RaftActor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID2, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID, follower1ActorPath), configParams, NO_PERSISTENCE, follower2Collector)
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), follower2ActorId);
+
+ // Send first ChangeServersVotingStatus message
+
+ leaderActor.tell(new ChangeServersVotingStatus(Map.of(FOLLOWER_ID, false, FOLLOWER_ID2, false)),
+ testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ final ApplyState applyState = MessageCollectorActor.expectFirstMatching(leaderCollector, ApplyState.class);
+ assertEquals(0L, applyState.getReplicatedLogEntry().getIndex());
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ votingServer(LEADER_ID), nonVotingServer(FOLLOWER_ID), nonVotingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower1Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower1RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), votingServer(LEADER_ID), nonVotingServer(FOLLOWER_ID),
+ nonVotingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower2Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower2RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), votingServer(LEADER_ID), nonVotingServer(FOLLOWER_ID),
+ nonVotingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.clearMessages(leaderCollector);
+ MessageCollectorActor.clearMessages(follower1Collector);
+ MessageCollectorActor.clearMessages(follower2Collector);
+
+ // Send second ChangeServersVotingStatus message
+
+ leaderActor.tell(new ChangeServersVotingStatus(Map.of(FOLLOWER_ID, true)), testKit.getRef());
+ reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ MessageCollectorActor.expectFirstMatching(leaderCollector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ votingServer(LEADER_ID), votingServer(FOLLOWER_ID), nonVotingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower1Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower1RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), votingServer(LEADER_ID), votingServer(FOLLOWER_ID), nonVotingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower2Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower2RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), votingServer(LEADER_ID), votingServer(FOLLOWER_ID), nonVotingServer(FOLLOWER_ID2));
+
+ LOG.info("testChangeServersVotingStatus ending");
+ }
+
+ @Test
+ public void testChangeLeaderToNonVoting() {
+ LOG.info("testChangeLeaderToNonVoting starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(500, TimeUnit.MILLISECONDS));
+
+ final String follower1ActorId = actorFactory.generateActorId(FOLLOWER_ID);
+ final String follower1ActorPath = actorFactory.createTestActorPath(follower1ActorId);
+ final String follower2ActorId = actorFactory.generateActorId(FOLLOWER_ID2);
+ final String follower2ActorPath = actorFactory.createTestActorPath(follower2ActorId);
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(FOLLOWER_ID, follower1ActorPath,
+ FOLLOWER_ID2, follower2ActorPath), new MockRaftActorContext())
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), actorFactory.generateActorId(LEADER_ID));
+ ActorRef leaderCollector = newLeaderCollectorActor(leaderActor.underlyingActor());
+
+ ActorRef follower1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower1RaftActor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID2, follower2ActorPath), configParams, NO_PERSISTENCE, follower1Collector)
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), follower1ActorId);
+
+ ActorRef follower2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ final TestActorRef<CollectingMockRaftActor> follower2RaftActor = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(FOLLOWER_ID2, Map.of(LEADER_ID, leaderActor.path().toString(),
+ FOLLOWER_ID, follower1ActorPath), configParams, NO_PERSISTENCE, follower2Collector)
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), follower2ActorId);
+
+ // Send ChangeServersVotingStatus message
+
+ leaderActor.tell(new ChangeServersVotingStatus(Map.of(LEADER_ID, false)), testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ MessageCollectorActor.expectFirstMatching(leaderCollector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(leaderActor.underlyingActor().getRaftActorContext().getReplicatedLog(),
+ nonVotingServer(LEADER_ID), votingServer(FOLLOWER_ID), votingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower1Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower1RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), nonVotingServer(LEADER_ID), votingServer(FOLLOWER_ID), votingServer(FOLLOWER_ID2));
+
+ MessageCollectorActor.expectFirstMatching(follower2Collector, ApplyState.class);
+ verifyServerConfigurationPayloadEntry(follower2RaftActor.underlyingActor().getRaftActorContext()
+ .getReplicatedLog(), nonVotingServer(LEADER_ID), votingServer(FOLLOWER_ID), votingServer(FOLLOWER_ID2));
+
+ verifyRaftState(RaftState.Leader, follower1RaftActor.underlyingActor(), follower2RaftActor.underlyingActor());
+ verifyRaftState(RaftState.Follower, leaderActor.underlyingActor());
+
+ MessageCollectorActor.expectMatching(leaderCollector, AppendEntries.class, 2);
+
+ LOG.info("testChangeLeaderToNonVoting ending");
+ }
+
+ @Test
+ public void testChangeLeaderToNonVotingInSingleNode() {
+ LOG.info("testChangeLeaderToNonVotingInSingleNode starting");
+
+ TestActorRef<MockLeaderRaftActor> leaderActor = actorFactory.createTestActor(
+ MockLeaderRaftActor.props(Map.of(), new MockRaftActorContext())
+ .withDispatcher(Dispatchers.DefaultDispatcherId()), actorFactory.generateActorId(LEADER_ID));
+
+ leaderActor.tell(new ChangeServersVotingStatus(Map.of(LEADER_ID, false)), testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.INVALID_REQUEST, reply.getStatus());
+
+ LOG.info("testChangeLeaderToNonVotingInSingleNode ending");
+ }
+
+ @Test
+ public void testChangeToVotingWithNoLeader() {
+ LOG.info("testChangeToVotingWithNoLeader starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(100, TimeUnit.MILLISECONDS));
+ configParams.setElectionTimeoutFactor(5);
+
+ final String node1ID = "node1";
+ final String node2ID = "node2";
+
+ // Set up a persisted ServerConfigurationPayload. Initially node1 and node2 will come up as non-voting.
+ // via the server config. The server config will also contain 2 voting peers that are down (ie no
+ // actors created).
+
+ ServerConfigurationPayload persistedServerConfig = new ServerConfigurationPayload(List.of(
+ new ServerInfo(node1ID, false), new ServerInfo(node2ID, false),
+ new ServerInfo("downNode1", true), new ServerInfo("downNode2", true)));
+ SimpleReplicatedLogEntry persistedServerConfigEntry = new SimpleReplicatedLogEntry(0, 1, persistedServerConfig);
+
+ InMemoryJournal.addEntry(node1ID, 1, new UpdateElectionTerm(1, "downNode1"));
+ InMemoryJournal.addEntry(node1ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node1ID, 3, new ApplyJournalEntries(0));
+ InMemoryJournal.addEntry(node2ID, 1, new UpdateElectionTerm(1, "downNode2"));
+ InMemoryJournal.addEntry(node2ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node2ID, 3, new ApplyJournalEntries(0));
+
+ ActorRef node1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node1RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node1ID, Map.of(), configParams,
+ PERSISTENT, node1Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node1ID);
+ CollectingMockRaftActor node1RaftActor = node1RaftActorRef.underlyingActor();
+
+ ActorRef node2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node2RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node2ID, Map.of(), configParams,
+ PERSISTENT, node2Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node2ID);
+ CollectingMockRaftActor node2RaftActor = node2RaftActorRef.underlyingActor();
+
+ node1RaftActor.waitForInitializeBehaviorComplete();
+ node2RaftActor.waitForInitializeBehaviorComplete();
+
+ // Verify the intended server config was loaded and applied.
+ verifyServerConfigurationPayloadEntry(node1RaftActor.getRaftActorContext().getReplicatedLog(),
+ nonVotingServer(node1ID), nonVotingServer(node2ID), votingServer("downNode1"),
+ votingServer("downNode2"));
+ assertEquals("isVotingMember", false, node1RaftActor.getRaftActorContext().isVotingMember());
+ assertEquals("getRaftState", RaftState.Follower, node1RaftActor.getRaftState());
+ assertEquals("getLeaderId", null, node1RaftActor.getLeaderId());
+
+ verifyServerConfigurationPayloadEntry(node2RaftActor.getRaftActorContext().getReplicatedLog(),
+ nonVotingServer(node1ID), nonVotingServer(node2ID), votingServer("downNode1"),
+ votingServer("downNode2"));
+ assertEquals("isVotingMember", false, node2RaftActor.getRaftActorContext().isVotingMember());
+
+ // For the test, we send a ChangeServersVotingStatus message to node1 to flip the voting states for
+ // each server, ie node1 and node2 to voting and the 2 down nodes to non-voting. This should cause
+ // node1 to try to elect itself as leader in order to apply the new server config. Since the 2
+ // down nodes are switched to non-voting, node1 should only need a vote from node2.
+
+ // First send the message such that node1 has no peer address for node2 - should fail.
+
+ ChangeServersVotingStatus changeServers = new ChangeServersVotingStatus(Map.of(node1ID, true,
+ node2ID, true, "downNode1", false, "downNode2", false));
+ node1RaftActorRef.tell(changeServers, testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.NO_LEADER, reply.getStatus());
+ assertEquals("getRaftState", RaftState.Follower, node1RaftActor.getRaftState());
+
+ // Send an AppendEntries so node1 has a leaderId
+
+ long term = node1RaftActor.getRaftActorContext().getTermInformation().getCurrentTerm();
+ node1RaftActorRef.tell(new AppendEntries(term, "downNode1", -1L, -1L,
+ List.of(), 0, -1, (short)1), ActorRef.noSender());
+
+ // Wait for the ElectionTimeout to clear the leaderId. The leaderId must be null so on the next
+ // ChangeServersVotingStatus message, it will try to elect a leader.
+
+ AbstractRaftActorIntegrationTest.verifyRaftState(node1RaftActorRef,
+ rs -> assertEquals("getLeader", null, rs.getLeader()));
+
+ // Update node2's peer address and send the message again
+
+ node1RaftActor.setPeerAddress(node2ID, node2RaftActorRef.path().toString());
+
+ node1RaftActorRef.tell(changeServers, testKit.getRef());
+ reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ ApplyJournalEntries apply = MessageCollectorActor.expectFirstMatching(node1Collector,
+ ApplyJournalEntries.class);
+ assertEquals("getToIndex", 1, apply.getToIndex());
+ verifyServerConfigurationPayloadEntry(node1RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID), nonVotingServer("downNode1"),
+ nonVotingServer("downNode2"));
+ assertEquals("isVotingMember", true, node1RaftActor.getRaftActorContext().isVotingMember());
+ assertEquals("getRaftState", RaftState.Leader, node1RaftActor.getRaftState());
+
+ apply = MessageCollectorActor.expectFirstMatching(node2Collector, ApplyJournalEntries.class);
+ assertEquals("getToIndex", 1, apply.getToIndex());
+ verifyServerConfigurationPayloadEntry(node2RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID), nonVotingServer("downNode1"),
+ nonVotingServer("downNode2"));
+ assertEquals("isVotingMember", true, node2RaftActor.getRaftActorContext().isVotingMember());
+ assertEquals("getRaftState", RaftState.Follower, node2RaftActor.getRaftState());
+
+ LOG.info("testChangeToVotingWithNoLeader ending");
+ }
+
+ @Test
+ public void testChangeToVotingWithNoLeaderAndElectionTimeout() {
+ LOG.info("testChangeToVotingWithNoLeaderAndElectionTimeout starting");
+
+ final String node1ID = "node1";
+ final String node2ID = "node2";
+
+ final PeerAddressResolver peerAddressResolver = peerId -> peerId.equals(node1ID)
+ ? actorFactory.createTestActorPath(node1ID) : peerId.equals(node2ID)
+ ? actorFactory.createTestActorPath(node2ID) : null;
+
+ ServerConfigurationPayload persistedServerConfig = new ServerConfigurationPayload(List.of(
+ new ServerInfo(node1ID, false), new ServerInfo(node2ID, true)));
+ SimpleReplicatedLogEntry persistedServerConfigEntry = new SimpleReplicatedLogEntry(0, 1, persistedServerConfig);
+
+ InMemoryJournal.addEntry(node1ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node1ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node2ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node2ID, 2, persistedServerConfigEntry);
+
+ DefaultConfigParamsImpl configParams1 = new DefaultConfigParamsImpl();
+ configParams1.setHeartBeatInterval(new FiniteDuration(100, TimeUnit.MILLISECONDS));
+ configParams1.setElectionTimeoutFactor(1);
+ configParams1.setPeerAddressResolver(peerAddressResolver);
+ ActorRef node1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node1RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node1ID, Map.of(), configParams1,
+ PERSISTENT, node1Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node1ID);
+ final CollectingMockRaftActor node1RaftActor = node1RaftActorRef.underlyingActor();
+
+ DefaultConfigParamsImpl configParams2 = new DefaultConfigParamsImpl();
+ configParams2.setElectionTimeoutFactor(1000000);
+ configParams2.setPeerAddressResolver(peerAddressResolver);
+ ActorRef node2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node2RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node2ID, Map.of(), configParams2,
+ PERSISTENT, node2Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node2ID);
+ CollectingMockRaftActor node2RaftActor = node2RaftActorRef.underlyingActor();
+
+ // Send a ChangeServersVotingStatus message to node1 to change mode1 to voting. This should cause
+ // node1 to try to elect itself as leader in order to apply the new server config. But we'll drop
+ // RequestVote messages in node2 which should cause node1 to time out and revert back to the previous
+ // server config and fail with NO_LEADER. Note that node1 shouldn't forward the request to node2 b/c
+ // node2 was previously voting.
+
+ node2RaftActor.setDropMessageOfType(RequestVote.class);
+
+ ChangeServersVotingStatus changeServers = new ChangeServersVotingStatus(Map.of(node1ID, true));
+ node1RaftActorRef.tell(changeServers, testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.NO_LEADER, reply.getStatus());
+
+ assertEquals("Server config", Set.of(nonVotingServer(node1ID), votingServer(node2ID)),
+ Set.copyOf(node1RaftActor.getRaftActorContext().getPeerServerInfo(true).getServerConfig()));
+ assertEquals("getRaftState", RaftState.Follower, node1RaftActor.getRaftState());
+
+ LOG.info("testChangeToVotingWithNoLeaderAndElectionTimeout ending");
+ }
+
+ @Test
+ public void testChangeToVotingWithNoLeaderAndForwardedToOtherNodeAfterElectionTimeout() {
+ LOG.info("testChangeToVotingWithNoLeaderAndForwardedToOtherNodeAfterElectionTimeout starting");
+
+ final String node1ID = "node1";
+ final String node2ID = "node2";
+
+ final PeerAddressResolver peerAddressResolver = peerId -> peerId.equals(node1ID)
+ ? actorFactory.createTestActorPath(node1ID) : peerId.equals(node2ID)
+ ? actorFactory.createTestActorPath(node2ID) : null;
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(100, TimeUnit.MILLISECONDS));
+ configParams.setElectionTimeoutFactor(3);
+ configParams.setPeerAddressResolver(peerAddressResolver);
+
+ ServerConfigurationPayload persistedServerConfig = new ServerConfigurationPayload(List.of(
+ new ServerInfo(node1ID, false), new ServerInfo(node2ID, false)));
+ SimpleReplicatedLogEntry persistedServerConfigEntry = new SimpleReplicatedLogEntry(0, 1, persistedServerConfig);
+
+ InMemoryJournal.addEntry(node1ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node1ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node2ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node2ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node2ID, 3, new SimpleReplicatedLogEntry(1, 1,
+ new MockRaftActorContext.MockPayload("2")));
+ InMemoryJournal.addEntry(node2ID, 4, new ApplyJournalEntries(1));
+
+ ActorRef node1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node1RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node1ID, Map.of(), configParams,
+ PERSISTENT, node1Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node1ID);
+ final CollectingMockRaftActor node1RaftActor = node1RaftActorRef.underlyingActor();
+
+ ActorRef node2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node2RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node2ID, Map.of(), configParams,
+ PERSISTENT, node2Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node2ID);
+ final CollectingMockRaftActor node2RaftActor = node2RaftActorRef.underlyingActor();
+
+ // Send a ChangeServersVotingStatus message to node1 to change mode1 to voting. This should cause
+ // node1 to try to elect itself as leader in order to apply the new server config. However node1's log
+ // is behind node2's so node2 should not grant node1's vote. This should cause node1 to time out and
+ // forward the request to node2.
+
+ ChangeServersVotingStatus changeServers = new ChangeServersVotingStatus(
+ Map.of(node1ID, true, node2ID, true));
+ node1RaftActorRef.tell(changeServers, testKit.getRef());
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ MessageCollectorActor.expectFirstMatching(node2Collector, ApplyJournalEntries.class);
+ verifyServerConfigurationPayloadEntry(node2RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID));
+ assertEquals("getRaftState", RaftState.Leader, node2RaftActor.getRaftState());
+
+ MessageCollectorActor.expectFirstMatching(node1Collector, ApplyJournalEntries.class);
+ verifyServerConfigurationPayloadEntry(node1RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID));
+ assertEquals("isVotingMember", true, node1RaftActor.getRaftActorContext().isVotingMember());
+ assertEquals("getRaftState", RaftState.Follower, node1RaftActor.getRaftState());
+
+ LOG.info("testChangeToVotingWithNoLeaderAndForwardedToOtherNodeAfterElectionTimeout ending");
+ }
+
+ @Test
+ public void testChangeToVotingWithNoLeaderAndOtherLeaderElected() {
+ LOG.info("testChangeToVotingWithNoLeaderAndOtherLeaderElected starting");
+
+ DefaultConfigParamsImpl configParams = new DefaultConfigParamsImpl();
+ configParams.setHeartBeatInterval(new FiniteDuration(100, TimeUnit.MILLISECONDS));
+ configParams.setElectionTimeoutFactor(100000);
+
+ final String node1ID = "node1";
+ final String node2ID = "node2";
+
+ configParams.setPeerAddressResolver(peerId -> peerId.equals(node1ID)
+ ? actorFactory.createTestActorPath(node1ID) : peerId.equals(node2ID)
+ ? actorFactory.createTestActorPath(node2ID) : null);
+
+ ServerConfigurationPayload persistedServerConfig = new ServerConfigurationPayload(List.of(
+ new ServerInfo(node1ID, false), new ServerInfo(node2ID, true)));
+ SimpleReplicatedLogEntry persistedServerConfigEntry = new SimpleReplicatedLogEntry(0, 1, persistedServerConfig);
+
+ InMemoryJournal.addEntry(node1ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node1ID, 2, persistedServerConfigEntry);
+ InMemoryJournal.addEntry(node2ID, 1, new UpdateElectionTerm(1, "node1"));
+ InMemoryJournal.addEntry(node2ID, 2, persistedServerConfigEntry);
+
+ ActorRef node1Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node1RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node1ID, Map.of(), configParams,
+ PERSISTENT, node1Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node1ID);
+ final CollectingMockRaftActor node1RaftActor = node1RaftActorRef.underlyingActor();
+
+ ActorRef node2Collector = actorFactory.createActor(
+ MessageCollectorActor.props(), actorFactory.generateActorId("collector"));
+ TestActorRef<CollectingMockRaftActor> node2RaftActorRef = actorFactory.createTestActor(
+ CollectingMockRaftActor.props(node2ID, Map.of(), configParams,
+ PERSISTENT, node2Collector).withDispatcher(Dispatchers.DefaultDispatcherId()), node2ID);
+ CollectingMockRaftActor node2RaftActor = node2RaftActorRef.underlyingActor();
+
+ // Send a ChangeServersVotingStatus message to node1 to change node1 to voting. This should cause
+ // node1 to try to elect itself as leader in order to apply the new server config. But we'll drop
+ // RequestVote messages in node2 and make it the leader so node1 should forward the server change
+ // request to node2 when node2 is elected.
+
+ node2RaftActor.setDropMessageOfType(RequestVote.class);
+
+ ChangeServersVotingStatus changeServers = new ChangeServersVotingStatus(Map.of(node1ID, true,
+ node2ID, true));
+ node1RaftActorRef.tell(changeServers, testKit.getRef());
+
+ MessageCollectorActor.expectFirstMatching(node2Collector, RequestVote.class);
+
+ node2RaftActorRef.tell(TimeoutNow.INSTANCE, ActorRef.noSender());
+
+ ServerChangeReply reply = testKit.expectMsgClass(Duration.ofSeconds(5), ServerChangeReply.class);
+ assertEquals("getStatus", ServerChangeStatus.OK, reply.getStatus());
+
+ MessageCollectorActor.expectFirstMatching(node1Collector, ApplyJournalEntries.class);
+ verifyServerConfigurationPayloadEntry(node1RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID));
+ assertEquals("isVotingMember", true, node1RaftActor.getRaftActorContext().isVotingMember());
+ assertEquals("getRaftState", RaftState.Follower, node1RaftActor.getRaftState());
+
+ MessageCollectorActor.expectFirstMatching(node2Collector, ApplyJournalEntries.class);
+ verifyServerConfigurationPayloadEntry(node2RaftActor.getRaftActorContext().getReplicatedLog(),
+ votingServer(node1ID), votingServer(node2ID));
+ assertEquals("getRaftState", RaftState.Leader, node2RaftActor.getRaftState());
+
+ LOG.info("testChangeToVotingWithNoLeaderAndOtherLeaderElected ending");
+ }
+
+ private static void verifyRaftState(final RaftState expState, final RaftActor... raftActors) {
+ Stopwatch sw = Stopwatch.createStarted();
+ while (sw.elapsed(TimeUnit.SECONDS) <= 5) {
+ for (RaftActor raftActor : raftActors) {
+ if (raftActor.getRaftState() == expState) {
+ return;
+ }
+ }
+ }
+
+ fail("None of the RaftActors have state " + expState);
+ }
+
+ private static ServerInfo votingServer(final String id) {