FileStore basics (4), MemStore/retention (10), RAFT election/append (16), config reload parity (3), monitoring endpoints varz/connz/healthz (6). 972 total tests passing, 0 failures.
140 lines
4.8 KiB
C#
140 lines
4.8 KiB
C#
using NATS.Server.Raft;
|
|
|
|
namespace NATS.Server.Tests.Raft;
|
|
|
|
/// <summary>
|
|
/// Ported from Go: TestNRGSimple in golang/nats-server/server/raft_test.go
|
|
/// Validates basic RAFT election mechanics and state convergence after proposals.
|
|
/// </summary>
|
|
public class RaftElectionBasicTests
|
|
{
|
|
[Fact]
|
|
public async Task Three_node_group_elects_leader()
|
|
{
|
|
// Reference: TestNRGSimple — create 3-node RAFT group, wait for leader election.
|
|
var cluster = RaftTestCluster.Create(3);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
|
|
// Verify exactly 1 leader among the 3 nodes.
|
|
leader.IsLeader.ShouldBeTrue();
|
|
leader.Role.ShouldBe(RaftRole.Leader);
|
|
leader.Term.ShouldBe(1);
|
|
|
|
// The other 2 nodes should not be leaders.
|
|
var followers = cluster.Nodes.Where(n => n.Id != leader.Id).ToList();
|
|
followers.Count.ShouldBe(2);
|
|
foreach (var follower in followers)
|
|
{
|
|
follower.IsLeader.ShouldBeFalse();
|
|
}
|
|
|
|
// Verify the cluster has exactly 1 leader total.
|
|
cluster.Nodes.Count(n => n.IsLeader).ShouldBe(1);
|
|
cluster.Nodes.Count(n => !n.IsLeader).ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task State_converges_after_proposals()
|
|
{
|
|
// Reference: TestNRGSimple — propose entries and verify all nodes converge.
|
|
var cluster = RaftTestCluster.Create(3);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
|
|
// Propose multiple entries like the Go test does with proposeDelta.
|
|
var index1 = await leader.ProposeAsync("delta-22", default);
|
|
var index2 = await leader.ProposeAsync("delta-minus-11", default);
|
|
var index3 = await leader.ProposeAsync("delta-minus-10", default);
|
|
|
|
// Wait for all members to have applied the entries.
|
|
await cluster.WaitForAppliedAsync(index3);
|
|
|
|
// All nodes should have converged to the same applied index.
|
|
cluster.Nodes.All(n => n.AppliedIndex >= index3).ShouldBeTrue();
|
|
|
|
// The leader's log should contain all 3 entries.
|
|
leader.Log.Entries.Count.ShouldBe(3);
|
|
leader.Log.Entries[0].Command.ShouldBe("delta-22");
|
|
leader.Log.Entries[1].Command.ShouldBe("delta-minus-11");
|
|
leader.Log.Entries[2].Command.ShouldBe("delta-minus-10");
|
|
|
|
// Verify log indices are sequential.
|
|
leader.Log.Entries[0].Index.ShouldBe(1);
|
|
leader.Log.Entries[1].Index.ShouldBe(2);
|
|
leader.Log.Entries[2].Index.ShouldBe(3);
|
|
|
|
// All entries should carry the current term.
|
|
foreach (var entry in leader.Log.Entries)
|
|
{
|
|
entry.Term.ShouldBe(leader.Term);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Candidate_receives_majority_to_become_leader()
|
|
{
|
|
// Validates the vote-counting mechanics in detail.
|
|
var node1 = new RaftNode("n1");
|
|
var node2 = new RaftNode("n2");
|
|
var node3 = new RaftNode("n3");
|
|
var allNodes = new[] { node1, node2, node3 };
|
|
foreach (var n in allNodes)
|
|
n.ConfigureCluster(allNodes);
|
|
|
|
// n1 starts an election.
|
|
node1.StartElection(clusterSize: 3);
|
|
node1.Role.ShouldBe(RaftRole.Candidate);
|
|
node1.Term.ShouldBe(1);
|
|
node1.TermState.VotedFor.ShouldBe("n1");
|
|
|
|
// With only 1 vote (self), not yet leader.
|
|
node1.IsLeader.ShouldBeFalse();
|
|
|
|
// n2 grants vote.
|
|
var voteFromN2 = node2.GrantVote(node1.Term, "n1");
|
|
voteFromN2.Granted.ShouldBeTrue();
|
|
node1.ReceiveVote(voteFromN2, clusterSize: 3);
|
|
|
|
// With 2 out of 3 votes (majority), should now be leader.
|
|
node1.IsLeader.ShouldBeTrue();
|
|
node1.Role.ShouldBe(RaftRole.Leader);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Leader_steps_down_on_request()
|
|
{
|
|
var cluster = RaftTestCluster.Create(3);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
leader.IsLeader.ShouldBeTrue();
|
|
|
|
leader.RequestStepDown();
|
|
leader.IsLeader.ShouldBeFalse();
|
|
leader.Role.ShouldBe(RaftRole.Follower);
|
|
}
|
|
|
|
[Fact]
|
|
public void Follower_steps_down_to_higher_term_on_heartbeat()
|
|
{
|
|
// When a follower receives a heartbeat with a higher term, it updates its term.
|
|
var node = new RaftNode("n1");
|
|
node.StartElection(clusterSize: 1);
|
|
node.IsLeader.ShouldBeTrue();
|
|
node.Term.ShouldBe(1);
|
|
|
|
// Receiving heartbeat with higher term causes step-down.
|
|
node.ReceiveHeartbeat(term: 5);
|
|
node.Role.ShouldBe(RaftRole.Follower);
|
|
node.Term.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Five_node_group_elects_leader_with_quorum()
|
|
{
|
|
var cluster = RaftTestCluster.Create(5);
|
|
var leader = await cluster.ElectLeaderAsync();
|
|
|
|
leader.IsLeader.ShouldBeTrue();
|
|
cluster.Nodes.Count(n => n.IsLeader).ShouldBe(1);
|
|
cluster.Nodes.Count(n => !n.IsLeader).ShouldBe(4);
|
|
}
|
|
}
|