using NATS.Server.Raft; namespace NATS.Server.Tests; public class RaftElectionTests { [Fact] public async Task Candidate_becomes_leader_after_majority_votes() { var cluster = RaftTestCluster.Create(3); var leader = await cluster.ElectLeaderAsync(); leader.Role.ShouldBe(RaftRole.Leader); leader.Term.ShouldBe(1); } } internal sealed class RaftTestCluster { public List Nodes { get; } public RaftNode Leader { get; private set; } public RaftNode LaggingFollower { get; private set; } private RaftTestCluster(List nodes) { Nodes = nodes; Leader = nodes[0]; LaggingFollower = nodes[^1]; } public static RaftTestCluster Create(int nodes) { var created = Enumerable.Range(1, nodes).Select(i => new RaftNode($"n{i}")).ToList(); foreach (var node in created) node.ConfigureCluster(created); return new RaftTestCluster(created); } public Task ElectLeaderAsync() { var candidate = Nodes[0]; candidate.StartElection(Nodes.Count); foreach (var voter in Nodes.Skip(1)) candidate.ReceiveVote(voter.GrantVote(candidate.Term)); Leader = candidate; return Task.FromResult(candidate); } public async Task WaitForAppliedAsync(long index) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(2)); while (!timeout.IsCancellationRequested) { if (Nodes.All(n => n.AppliedIndex >= index)) return; await Task.Delay(20, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); } } public async Task GenerateCommittedEntriesAsync(int count) { var leader = await ElectLeaderAsync(); for (int i = 0; i < count; i++) _ = await leader.ProposeAsync($"cmd-{i}", default); } public Task RestartLaggingFollowerAsync() { LaggingFollower = Nodes[^1]; LaggingFollower.AppliedIndex = 0; return Task.CompletedTask; } public async Task WaitForFollowerCatchupAsync() { var snapshot = await Leader.CreateSnapshotAsync(default); await LaggingFollower.InstallSnapshotAsync(snapshot, default); } }