feat: implement raft log replication and apply
This commit is contained in:
25
src/NATS.Server/Raft/RaftLog.cs
Normal file
25
src/NATS.Server/Raft/RaftLog.cs
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/NATS.Server/Raft/RaftReplicator.cs
Normal file
16
src/NATS.Server/Raft/RaftReplicator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user