Files
natsdotnet/tests/NATS.Server.Tests/Raft/RaftStrictConsensusRuntimeTests.cs
Joseph Doherty ae4cc6d613 feat: add randomized election timeout jitter (Gap 8.8)
Add RandomizedElectionTimeout() method to RaftNode returning TimeSpan in
[ElectionTimeoutMinMs, ElectionTimeoutMaxMs) using TotalMilliseconds (not
.Milliseconds component) to prevent synchronized elections after partitions.
Make Random injectable for deterministic testing. Fix SendHeartbeatAsync stub
in NatsRaftTransport and test-local transport implementations to satisfy the
IRaftTransport interface added in Gap 8.7.
2026-02-25 08:30:38 -05:00

51 lines
2.2 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;
public Task SendHeartbeatAsync(string leaderId, IReadOnlyList<string> followerIds, int term, Action<string> onAck, CancellationToken ct)
=> Task.CompletedTask;
}
}