batch31: implement raft group A catchup foundations

This commit is contained in:
Joseph Doherty
2026-02-28 20:41:21 -05:00
parent db5240331a
commit 14e79f33ee
7 changed files with 1229 additions and 1 deletions

View File

@@ -169,4 +169,121 @@ public sealed class RaftTypesTests
appendEntry.Leader.ShouldBeEmpty();
appendEntry.Entries.ShouldBeEmpty();
}
[Fact]
public void CatchupAndCommitMethods_ShouldTrackProgress()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
StateValue = (int)RaftState.Leader,
Term_ = 4,
PIndex = 10,
Commit = 10,
Peers_ = new Dictionary<string, Lps>(),
};
var entry = raft.NewAppendEntry("N1", 4, 11, 4, 10, [raft.NewEntry(EntryType.EntryNormal, [1, 2])]);
raft.StoreToWAL(entry).ShouldBeTrue();
raft.LoadEntry(11).ShouldNotBeNull();
raft.TrackPeer("N2", 11);
raft.CatchupFollower("N2", 4, 11).Signal.ShouldBeTrue();
raft.SendCatchupSignal().ShouldBeTrue();
raft.CancelCatchupSignal();
raft.Catchup!.Signal.ShouldBeFalse();
raft.TryCommit(11).ShouldBeTrue();
raft.ApplyQ_.ShouldNotBeNull();
raft.ApplyQ_!.Len().ShouldBeGreaterThan(0);
raft.UpdateLeader("N2");
raft.GroupLeader().ShouldBe("N2");
raft.TruncateWAL(10);
raft.LoadEntry(11).ShouldBeNull();
raft.ResetWAL();
raft.PIndex.ShouldBe(0UL);
}
[Fact]
public void AppendProcessingMethods_ShouldHandlePeerStateAndHeartbeat()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
StoreDir = Path.Combine(Path.GetTempPath(), $"raft-{Guid.NewGuid():N}"),
Qn = 2,
Csz = 3,
PIndex = 5,
PTerm = 2,
Commit = 4,
Peers_ = new Dictionary<string, Lps>
{
["N2"] = new() { Kp = true, Li = 5, Ts = DateTime.UtcNow },
},
};
var appendEntry = raft.BuildAppendEntry("N2");
appendEntry.Leader.ShouldBe("N1");
raft.SendAppendEntry(appendEntry, "N2");
raft.SendQ.ShouldNotBeNull();
var peerStateEntry = raft.NewEntry(EntryType.EntryPeerState, raft.EncodePeerState(new PeerState
{
KnownPeers = ["N2", "N3"],
ClusterSize = 3,
}));
raft.ProcessPeerState(peerStateEntry);
raft.PeerNames().ShouldContain("N2");
raft.PeerNames().ShouldContain("N3");
raft.CurrentPeerState().ClusterSize.ShouldBe(3);
raft.SendPeerState();
raft.SendHeartbeat();
raft.WritePeerState().ShouldBeNull();
raft.WriteTermVoteStatic(raft.StoreDir, 7, "N2").ShouldBeNull();
var termVote = raft.ReadTermVote();
termVote.Error.ShouldBeNull();
termVote.Term.ShouldBe(7UL);
termVote.Vote.ShouldBe("N2");
}
[Fact]
public void ElectionMethods_ShouldTransitionStatesBasedOnVotes()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
StoreDir = Path.Combine(Path.GetTempPath(), $"raft-{Guid.NewGuid():N}"),
StateValue = (int)RaftState.Follower,
Csz = 3,
Qn = 2,
PIndex = 9,
PTerm = 3,
VSubj = "raft.vote",
VReply = "raft.vote.reply",
};
raft.SwitchToCandidate();
raft.State().ShouldBe(RaftState.Candidate);
raft.QuorumNeeded().ShouldBe(2);
raft.RequestVote();
raft.SendQ.ShouldNotBeNull();
raft.ProcessVoteRequest(new VoteRequest
{
TermV = raft.Term_,
LastTerm = raft.PTerm,
LastIndex = raft.PIndex,
Candidate = "N2",
Reply = "reply-subject",
}).ShouldBeFalse();
raft.HandleVoteResponse(new VoteResponse { TermV = raft.Term_, Peer = "N2", Granted = true });
raft.State().ShouldBe(RaftState.Leader);
raft.State().ShouldBe(RaftState.Leader);
raft.IsClosed().ShouldBeFalse();
raft.SetWriteErr(new InvalidOperationException("write"));
raft.WriteErr.ShouldNotBeNull();
raft.SwitchToFollower("N2");
raft.State().ShouldBe(RaftState.Follower);
raft.GroupLeader().ShouldBe("N2");
}
}