feat(raft): add membership proposals, snapshot checkpoints, and log compaction (B4+B5+B6)

- ProposeAddPeerAsync/ProposeRemovePeerAsync: single-change-at-a-time membership
  changes through RAFT consensus (Go ref: raft.go:961-1019)
- RaftLog.Compact: removes entries up to given index for log compaction
- CreateSnapshotCheckpointAsync: creates snapshot and compacts log in one operation
- DrainAndReplaySnapshotAsync: drains commit queue, installs snapshot, resets indices
- Pre-vote protocol skipped (Go NATS doesn't implement it either)
- 23 new tests in RaftMembershipAndSnapshotTests
This commit is contained in:
Joseph Doherty
2026-02-24 17:06:16 -05:00
parent 5b706c969d
commit 824e0b3607
10 changed files with 1712 additions and 2 deletions

View File

@@ -0,0 +1,49 @@
namespace NATS.Server.JetStream.Cluster;
/// <summary>
/// RAFT group describing which peers own a replicated asset (stream or consumer).
/// Go reference: jetstream_cluster.go:154-163 raftGroup struct.
/// </summary>
public sealed class RaftGroup
{
public required string Name { get; init; }
public List<string> Peers { get; init; } = [];
public string StorageType { get; set; } = "file";
public string Cluster { get; set; } = string.Empty;
public string Preferred { get; set; } = string.Empty;
public int QuorumSize => (Peers.Count / 2) + 1;
public bool HasQuorum(int ackCount) => ackCount >= QuorumSize;
}
/// <summary>
/// Assignment of a stream to a RAFT group of peers.
/// Go reference: jetstream_cluster.go:166-184 streamAssignment struct.
/// </summary>
public sealed class StreamAssignment
{
public required string StreamName { get; init; }
public required RaftGroup Group { get; init; }
public DateTime Created { get; init; } = DateTime.UtcNow;
public string ConfigJson { get; set; } = "{}";
public string SyncSubject { get; set; } = string.Empty;
public bool Responded { get; set; }
public bool Recovering { get; set; }
public bool Reassigning { get; set; }
public Dictionary<string, ConsumerAssignment> Consumers { get; } = new(StringComparer.Ordinal);
}
/// <summary>
/// Assignment of a consumer to a RAFT group within a stream's cluster.
/// Go reference: jetstream_cluster.go:250-266 consumerAssignment struct.
/// </summary>
public sealed class ConsumerAssignment
{
public required string ConsumerName { get; init; }
public required string StreamName { get; init; }
public required RaftGroup Group { get; init; }
public DateTime Created { get; init; } = DateTime.UtcNow;
public string ConfigJson { get; set; } = "{}";
public bool Responded { get; set; }
public bool Recovering { get; set; }
}