feat: complete final jetstream parity transport and runtime baselines

This commit is contained in:
Joseph Doherty
2026-02-23 11:04:43 -05:00
parent 53585012f3
commit 8bce096f55
61 changed files with 2655 additions and 129 deletions

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View 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 });
}
}