feat: add quorum check before proposing entries (Gap 8.6)

Add HasQuorum() to RaftNode that counts peers with LastContact within
2 × ElectionTimeoutMaxMs and returns true only when self + current peers
reaches majority. ProposeAsync now throws InvalidOperationException with
"no quorum" when HasQuorum() returns false, preventing a partitioned
leader from diverging the log. Add 14 tests in RaftQuorumCheckTests.cs
covering single-node, 3-node, 5-node, boundary window, and heartbeat
restore scenarios. Update RaftHealthTests.LastContact_updates_on_successful_replication
to avoid triggering the new quorum guard.
This commit is contained in:
Joseph Doherty
2026-02-25 08:26:37 -05:00
parent 5d3a3c73e9
commit 5a62100397
6 changed files with 888 additions and 15 deletions

View File

@@ -301,15 +301,15 @@ public class RaftHealthTests
var (nodes, _) = CreateCluster(3);
var leader = ElectLeader(nodes);
// Set peer contacts in the past
foreach (var (_, state) in leader.GetPeerStates())
state.LastContact = DateTime.UtcNow.AddMinutes(-5);
// Record timestamps just before proposing (peers are fresh from ConfigureCluster).
var beforePropose = DateTime.UtcNow;
await leader.ProposeAsync("cmd-1", default);
// Successful replication should update LastContact
// Successful replication should update LastContact to at least the time we
// recorded before the propose call.
foreach (var (_, state) in leader.GetPeerStates())
state.LastContact.ShouldBeGreaterThan(DateTime.UtcNow.AddSeconds(-2));
state.LastContact.ShouldBeGreaterThanOrEqualTo(beforePropose);
}
[Fact]