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

@@ -19,6 +19,20 @@ public sealed class RaftLog
return entry;
}
/// <summary>
/// Appends an entry with an explicit timestamp. Used in tests and by the
/// <see cref="CompactionPolicy.ByAge"/> policy to set controlled creation times.
/// </summary>
public RaftLogEntry AppendWithTimestamp(int term, string command, DateTime timestamp)
{
var entry = new RaftLogEntry(_baseIndex + _entries.Count + 1, term, command)
{
Timestamp = timestamp,
};
_entries.Add(entry);
return entry;
}
public void AppendReplicated(RaftLogEntry entry)
{
if (_entries.Any(e => e.Index == entry.Index))
@@ -79,4 +93,15 @@ public sealed class RaftLog
}
}
public sealed record RaftLogEntry(long Index, int Term, string Command);
/// <summary>
/// A single RAFT log entry. Timestamp records when the entry was created (UTC) and
/// is used by the <see cref="CompactionPolicy.ByAge"/> compaction policy.
/// </summary>
public sealed record RaftLogEntry(long Index, int Term, string Command)
{
/// <summary>
/// UTC creation time. Defaults to <see cref="DateTime.UtcNow"/> at append time.
/// Stored as a JSON-serializable property so it survives persistence round-trips.
/// </summary>
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}