feat: implement raft election and term state
This commit is contained in:
56
src/NATS.Server/Raft/RaftNode.cs
Normal file
56
src/NATS.Server/Raft/RaftNode.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/NATS.Server/Raft/RaftRpcContracts.cs
Normal file
12
src/NATS.Server/Raft/RaftRpcContracts.cs
Normal 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; }
|
||||||
|
}
|
||||||
14
src/NATS.Server/Raft/RaftTermState.cs
Normal file
14
src/NATS.Server/Raft/RaftTermState.cs
Normal 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,
|
||||||
|
}
|
||||||
43
tests/NATS.Server.Tests/RaftElectionTests.cs
Normal file
43
tests/NATS.Server.Tests/RaftElectionTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user