feat: implement raft log replication and apply

This commit is contained in:
Joseph Doherty
2026-02-23 06:12:18 -05:00
parent 66ec378bdc
commit ecc4752c07
5 changed files with 113 additions and 4 deletions

View File

@@ -0,0 +1,25 @@
namespace NATS.Server.Raft;
public sealed class RaftLog
{
private readonly List<RaftLogEntry> _entries = [];
public IReadOnlyList<RaftLogEntry> Entries => _entries;
public RaftLogEntry Append(int term, string command)
{
var entry = new RaftLogEntry(_entries.Count + 1, term, command);
_entries.Add(entry);
return entry;
}
public void AppendReplicated(RaftLogEntry entry)
{
if (_entries.Any(e => e.Index == entry.Index))
return;
_entries.Add(entry);
}
}
public sealed record RaftLogEntry(long Index, int Term, string Command);

View File

@@ -3,18 +3,27 @@ namespace NATS.Server.Raft;
public sealed class RaftNode
{
private int _votesReceived;
private readonly List<RaftNode> _cluster = [];
private readonly RaftReplicator _replicator = new();
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 RaftLog Log { get; } = new();
public RaftNode(string id)
{
Id = id;
}
public void ConfigureCluster(IEnumerable<RaftNode> peers)
{
_cluster.Clear();
_cluster.AddRange(peers);
}
public void StartElection(int clusterSize)
{
Role = RaftRole.Candidate;
@@ -42,15 +51,41 @@ public sealed class RaftNode
TryBecomeLeader(clusterSize);
}
private void TryBecomeLeader(int clusterSize)
public async ValueTask<long> ProposeAsync(string command, CancellationToken ct)
{
var quorum = (clusterSize / 2) + 1;
if (_votesReceived >= quorum)
Role = RaftRole.Leader;
if (Role != RaftRole.Leader)
throw new InvalidOperationException("Only leader can propose entries.");
var entry = Log.Append(TermState.CurrentTerm, command);
var followers = _cluster.Where(n => n.Id != Id).ToList();
var acknowledgements = _replicator.Replicate(entry, followers);
var quorum = (_cluster.Count / 2) + 1;
if (acknowledgements + 1 >= quorum)
{
AppliedIndex = entry.Index;
foreach (var node in _cluster)
node.AppliedIndex = Math.Max(node.AppliedIndex, entry.Index);
}
await Task.CompletedTask;
return entry.Index;
}
public void ReceiveReplicatedEntry(RaftLogEntry entry)
{
Log.AppendReplicated(entry);
}
public void RequestStepDown()
{
Role = RaftRole.Follower;
}
private void TryBecomeLeader(int clusterSize)
{
var quorum = (clusterSize / 2) + 1;
if (_votesReceived >= quorum)
Role = RaftRole.Leader;
}
}

View File

@@ -0,0 +1,16 @@
namespace NATS.Server.Raft;
public sealed class RaftReplicator
{
public int Replicate(RaftLogEntry entry, IReadOnlyList<RaftNode> followers)
{
var acknowledgements = 0;
foreach (var follower in followers)
{
follower.ReceiveReplicatedEntry(entry);
acknowledgements++;
}
return acknowledgements;
}
}