feat: complete final jetstream parity transport and runtime baselines
This commit is contained in:
@@ -27,6 +27,36 @@ public sealed class RaftLog
|
||||
_entries.Clear();
|
||||
_baseIndex = snapshot.LastIncludedIndex;
|
||||
}
|
||||
|
||||
public async Task PersistAsync(string path, CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
var model = new PersistedLog
|
||||
{
|
||||
BaseIndex = _baseIndex,
|
||||
Entries = [.. _entries],
|
||||
};
|
||||
await File.WriteAllTextAsync(path, System.Text.Json.JsonSerializer.Serialize(model), ct);
|
||||
}
|
||||
|
||||
public static async Task<RaftLog> LoadAsync(string path, CancellationToken ct)
|
||||
{
|
||||
var log = new RaftLog();
|
||||
if (!File.Exists(path))
|
||||
return log;
|
||||
|
||||
var json = await File.ReadAllTextAsync(path, ct);
|
||||
var model = System.Text.Json.JsonSerializer.Deserialize<PersistedLog>(json) ?? new PersistedLog();
|
||||
log._baseIndex = model.BaseIndex;
|
||||
log._entries.AddRange(model.Entries);
|
||||
return log;
|
||||
}
|
||||
|
||||
private sealed class PersistedLog
|
||||
{
|
||||
public long BaseIndex { get; set; }
|
||||
public List<RaftLogEntry> Entries { get; set; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record RaftLogEntry(long Index, int Term, string Command);
|
||||
|
||||
@@ -6,6 +6,8 @@ public sealed class RaftNode
|
||||
private readonly List<RaftNode> _cluster = [];
|
||||
private readonly RaftReplicator _replicator = new();
|
||||
private readonly RaftSnapshotStore _snapshotStore = new();
|
||||
private readonly IRaftTransport? _transport;
|
||||
private readonly string? _persistDirectory;
|
||||
|
||||
public string Id { get; }
|
||||
public int Term => TermState.CurrentTerm;
|
||||
@@ -13,11 +15,13 @@ public sealed class RaftNode
|
||||
public RaftRole Role { get; private set; } = RaftRole.Follower;
|
||||
public RaftTermState TermState { get; } = new();
|
||||
public long AppliedIndex { get; set; }
|
||||
public RaftLog Log { get; } = new();
|
||||
public RaftLog Log { get; private set; } = new();
|
||||
|
||||
public RaftNode(string id)
|
||||
public RaftNode(string id, IRaftTransport? transport = null, string? persistDirectory = null)
|
||||
{
|
||||
Id = id;
|
||||
_transport = transport;
|
||||
_persistDirectory = persistDirectory;
|
||||
}
|
||||
|
||||
public void ConfigureCluster(IEnumerable<RaftNode> peers)
|
||||
@@ -60,7 +64,8 @@ public sealed class RaftNode
|
||||
|
||||
var entry = Log.Append(TermState.CurrentTerm, command);
|
||||
var followers = _cluster.Where(n => n.Id != Id).ToList();
|
||||
var acknowledgements = _replicator.Replicate(entry, followers);
|
||||
var results = await _replicator.ReplicateAsync(Id, entry, followers, _transport, ct);
|
||||
var acknowledgements = results.Count(r => r.Success);
|
||||
|
||||
var quorum = (_cluster.Count / 2) + 1;
|
||||
if (acknowledgements + 1 >= quorum)
|
||||
@@ -68,9 +73,14 @@ public sealed class RaftNode
|
||||
AppliedIndex = entry.Index;
|
||||
foreach (var node in _cluster)
|
||||
node.AppliedIndex = Math.Max(node.AppliedIndex, entry.Index);
|
||||
|
||||
foreach (var node in _cluster.Where(n => n._persistDirectory != null))
|
||||
await node.PersistAsync(ct);
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
if (_persistDirectory != null)
|
||||
await PersistAsync(ct);
|
||||
|
||||
return entry.Index;
|
||||
}
|
||||
|
||||
@@ -120,4 +130,29 @@ public sealed class RaftNode
|
||||
if (_votesReceived >= quorum)
|
||||
Role = RaftRole.Leader;
|
||||
}
|
||||
|
||||
public async Task PersistAsync(CancellationToken ct)
|
||||
{
|
||||
var dir = _persistDirectory ?? Path.Combine(Path.GetTempPath(), "natsdotnet-raft", Id);
|
||||
Directory.CreateDirectory(dir);
|
||||
await Log.PersistAsync(Path.Combine(dir, "log.json"), ct);
|
||||
await File.WriteAllTextAsync(Path.Combine(dir, "term.txt"), TermState.CurrentTerm.ToString(), ct);
|
||||
await File.WriteAllTextAsync(Path.Combine(dir, "applied.txt"), AppliedIndex.ToString(), ct);
|
||||
}
|
||||
|
||||
public async Task LoadPersistedStateAsync(CancellationToken ct)
|
||||
{
|
||||
var dir = _persistDirectory ?? Path.Combine(Path.GetTempPath(), "natsdotnet-raft", Id);
|
||||
Log = await RaftLog.LoadAsync(Path.Combine(dir, "log.json"), ct);
|
||||
|
||||
var termPath = Path.Combine(dir, "term.txt");
|
||||
if (File.Exists(termPath) && int.TryParse(await File.ReadAllTextAsync(termPath, ct), out var term))
|
||||
TermState.CurrentTerm = term;
|
||||
|
||||
var appliedPath = Path.Combine(dir, "applied.txt");
|
||||
if (File.Exists(appliedPath) && long.TryParse(await File.ReadAllTextAsync(appliedPath, ct), out var applied))
|
||||
AppliedIndex = applied;
|
||||
else if (Log.Entries.Count > 0)
|
||||
AppliedIndex = Log.Entries[^1].Index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,24 @@ public sealed class RaftReplicator
|
||||
|
||||
return acknowledgements;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AppendResult>> ReplicateAsync(
|
||||
string leaderId,
|
||||
RaftLogEntry entry,
|
||||
IReadOnlyList<RaftNode> followers,
|
||||
IRaftTransport? transport,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (transport != null)
|
||||
return await transport.AppendEntriesAsync(leaderId, followers.Select(f => f.Id).ToArray(), entry, ct);
|
||||
|
||||
var results = new List<AppendResult>(followers.Count);
|
||||
foreach (var follower in followers)
|
||||
{
|
||||
follower.ReceiveReplicatedEntry(entry);
|
||||
results.Add(new AppendResult { FollowerId = follower.Id, Success = true });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,9 @@ public sealed class VoteResponse
|
||||
{
|
||||
public bool Granted { get; init; }
|
||||
}
|
||||
|
||||
public sealed class AppendResult
|
||||
{
|
||||
public string FollowerId { get; init; } = string.Empty;
|
||||
public bool Success { get; init; }
|
||||
}
|
||||
|
||||
44
src/NATS.Server/Raft/RaftTransport.cs
Normal file
44
src/NATS.Server/Raft/RaftTransport.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace NATS.Server.Raft;
|
||||
|
||||
public interface IRaftTransport
|
||||
{
|
||||
Task<IReadOnlyList<AppendResult>> AppendEntriesAsync(string leaderId, IReadOnlyList<string> followerIds, RaftLogEntry entry, CancellationToken ct);
|
||||
Task<VoteResponse> RequestVoteAsync(string candidateId, string voterId, VoteRequest request, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class InMemoryRaftTransport : IRaftTransport
|
||||
{
|
||||
private readonly Dictionary<string, RaftNode> _nodes = new(StringComparer.Ordinal);
|
||||
|
||||
public void Register(RaftNode node)
|
||||
{
|
||||
_nodes[node.Id] = node;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AppendResult>> AppendEntriesAsync(string leaderId, IReadOnlyList<string> followerIds, RaftLogEntry entry, CancellationToken ct)
|
||||
{
|
||||
var results = new List<AppendResult>(followerIds.Count);
|
||||
foreach (var followerId in followerIds)
|
||||
{
|
||||
if (_nodes.TryGetValue(followerId, out var node))
|
||||
{
|
||||
node.ReceiveReplicatedEntry(entry);
|
||||
results.Add(new AppendResult { FollowerId = followerId, Success = true });
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(new AppendResult { FollowerId = followerId, Success = false });
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<AppendResult>>(results);
|
||||
}
|
||||
|
||||
public Task<VoteResponse> RequestVoteAsync(string candidateId, string voterId, VoteRequest request, CancellationToken ct)
|
||||
{
|
||||
if (_nodes.TryGetValue(voterId, out var node))
|
||||
return Task.FromResult(node.GrantVote(request.Term));
|
||||
|
||||
return Task.FromResult(new VoteResponse { Granted = false });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user