- Add SnapshotChunkEnumerator: IEnumerable<byte[]> that splits snapshot data into fixed-size chunks (default 65536 bytes) and computes CRC32 over the full payload for integrity validation during streaming transfer - Add RaftInstallSnapshotChunkWire: 24-byte header + variable data wire type encoding [snapshotIndex:8][snapshotTerm:4][chunkIndex:4][totalChunks:4][crc32:4][data:N] - Extend InstallSnapshotFromChunksAsync with optional expectedCrc32 parameter; validates assembled data against CRC32 before applying snapshot state, throwing InvalidDataException on mismatch to prevent corrupt state installation - Fix stub IRaftTransport implementations in test files missing SendTimeoutNowAsync - Fix incorrect role assertion in RaftLeadershipTransferTests (single-node quorum = 1) - 15 new tests in RaftSnapshotStreamingTests covering enumeration, reassembly, CRC correctness, validation success/failure, and wire format roundtrip
48 lines
2.0 KiB
C#
48 lines
2.0 KiB
C#
using NATS.Server.Raft;
|
|
|
|
namespace NATS.Server.Tests.Raft;
|
|
|
|
public class RaftStrictConsensusRuntimeTests
|
|
{
|
|
[Fact]
|
|
public async Task Quorum_and_nextindex_rules_gate_commit_visibility_and_snapshot_catchup_convergence()
|
|
{
|
|
var voter = new RaftNode("v1");
|
|
voter.GrantVote(2, "cand-a").Granted.ShouldBeTrue();
|
|
voter.GrantVote(2, "cand-b").Granted.ShouldBeFalse();
|
|
|
|
var transport = new RejectingRaftTransport();
|
|
var leader = new RaftNode("n1", transport);
|
|
var followerA = new RaftNode("n2", transport);
|
|
var followerB = new RaftNode("n3", transport);
|
|
var cluster = new[] { leader, followerA, followerB };
|
|
foreach (var node in cluster)
|
|
node.ConfigureCluster(cluster);
|
|
|
|
leader.StartElection(cluster.Length);
|
|
leader.ReceiveVote(new VoteResponse { Granted = true }, cluster.Length);
|
|
leader.IsLeader.ShouldBeTrue();
|
|
|
|
_ = await leader.ProposeAsync("cmd-1", default);
|
|
leader.AppliedIndex.ShouldBe(0);
|
|
followerA.AppliedIndex.ShouldBe(0);
|
|
followerB.AppliedIndex.ShouldBe(0);
|
|
}
|
|
|
|
private sealed class RejectingRaftTransport : IRaftTransport
|
|
{
|
|
public Task<IReadOnlyList<AppendResult>> AppendEntriesAsync(string leaderId, IReadOnlyList<string> followerIds, RaftLogEntry entry, CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<AppendResult>>(
|
|
followerIds.Select(id => new AppendResult { FollowerId = id, Success = false }).ToArray());
|
|
|
|
public Task<VoteResponse> RequestVoteAsync(string candidateId, string voterId, VoteRequest request, CancellationToken ct)
|
|
=> Task.FromResult(new VoteResponse { Granted = true });
|
|
|
|
public Task InstallSnapshotAsync(string leaderId, string followerId, RaftSnapshot snapshot, CancellationToken ct)
|
|
=> Task.CompletedTask;
|
|
|
|
public Task SendTimeoutNowAsync(string leaderId, string targetId, ulong term, CancellationToken ct)
|
|
=> Task.CompletedTask;
|
|
}
|
|
}
|