feat: implement raft election and term state

This commit is contained in:
Joseph Doherty
2026-02-23 06:11:28 -05:00
parent f1d3c19594
commit 66ec378bdc
4 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
namespace NATS.Server.Raft;
public sealed class RaftNode
{
private int _votesReceived;
public string Id { get; }
public int Term => TermState.CurrentTerm;
public RaftRole Role { get; private set; } = RaftRole.Follower;
public RaftTermState TermState { get; } = new();
public long AppliedIndex { get; set; }
public RaftNode(string id)
{
Id = id;
}
public void StartElection(int clusterSize)
{
Role = RaftRole.Candidate;
TermState.CurrentTerm++;
TermState.VotedFor = Id;
_votesReceived = 1;
TryBecomeLeader(clusterSize);
}
public VoteResponse GrantVote(int term)
{
if (term < TermState.CurrentTerm)
return new VoteResponse { Granted = false };
TermState.CurrentTerm = term;
return new VoteResponse { Granted = true };
}
public void ReceiveVote(VoteResponse response, int clusterSize = 3)
{
if (!response.Granted)
return;
_votesReceived++;
TryBecomeLeader(clusterSize);
}
private void TryBecomeLeader(int clusterSize)
{
var quorum = (clusterSize / 2) + 1;
if (_votesReceived >= quorum)
Role = RaftRole.Leader;
}
public void RequestStepDown()
{
Role = RaftRole.Follower;
}
}

View File

@@ -0,0 +1,12 @@
namespace NATS.Server.Raft;
public sealed class VoteRequest
{
public int Term { get; init; }
public string CandidateId { get; init; } = string.Empty;
}
public sealed class VoteResponse
{
public bool Granted { get; init; }
}

View File

@@ -0,0 +1,14 @@
namespace NATS.Server.Raft;
public sealed class RaftTermState
{
public int CurrentTerm { get; set; }
public string? VotedFor { get; set; }
}
public enum RaftRole
{
Follower,
Candidate,
Leader,
}

View File

@@ -0,0 +1,43 @@
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<RaftNode> Nodes { get; }
private RaftTestCluster(List<RaftNode> nodes)
{
Nodes = nodes;
}
public static RaftTestCluster Create(int nodes)
{
var created = Enumerable.Range(1, nodes).Select(i => new RaftNode($"n{i}")).ToList();
return new RaftTestCluster(created);
}
public Task<RaftNode> ElectLeaderAsync()
{
var candidate = Nodes[0];
candidate.StartElection(Nodes.Count);
foreach (var voter in Nodes.Skip(1))
candidate.ReceiveVote(voter.GrantVote(candidate.Term));
return Task.FromResult(candidate);
}
}