feat(batch31): merge raft-part-2

This commit is contained in:
Joseph Doherty
2026-02-28 20:56:08 -05:00
8 changed files with 1235 additions and 7 deletions

View File

@@ -178,4 +178,189 @@ public sealed class RaftNodeTests
raft.CampaignInternal(TimeSpan.FromMilliseconds(10)).ShouldBeNull();
raft.State().ShouldBe(RaftState.Candidate);
}
[Fact]
public void NRGSwitchStateClearsQueues_ShouldSucceed()
{
var raft = new Raft
{
GroupName = "RG",
StateValue = (int)RaftState.Leader,
LeadC = System.Threading.Channels.Channel.CreateUnbounded<bool>(),
};
raft.PropQ = new ZB.MOM.NatsNet.Server.Internal.IpQueue<ProposedEntry>("prop");
raft.PropQ.Push(new ProposedEntry { Entry = new Entry { Type = EntryType.EntryNormal, Data = [1] } });
raft.SwitchToFollower();
raft.State().ShouldBe(RaftState.Follower);
raft.PropQ.Len().ShouldBe(1);
}
[Fact]
public void NRGUnsuccessfulVoteRequestCampaignEarly_ShouldSucceed()
{
var raft = new Raft { Id = "N1", PTerm = 5, PIndex = 20, Term_ = 5 };
var granted = raft.ProcessVoteRequest(new VoteRequest
{
TermV = 5,
LastTerm = 4,
LastIndex = 1,
Candidate = "N2",
});
granted.ShouldBeFalse();
}
[Fact]
public void NRGCandidateDoesntRevertTermAfterOldAE_ShouldSucceed()
{
var raft = new Raft { StateValue = (int)RaftState.Candidate, Term_ = 10 };
raft.ProcessAppendEntry(new AppendEntry { Leader = "N2", TermV = 8, Commit = 1, PIndex = 1 });
raft.Term_.ShouldBe(10UL);
}
[Fact]
public void NRGTermDoesntRollBackToPtermOnCatchup_ShouldSucceed()
{
var raft = new Raft { Term_ = 10, PTerm = 9, PIndex = 100 };
raft.CatchupFollower("N2", raft.Term_, raft.PIndex);
raft.Term_.ShouldBe(10UL);
}
[Fact]
public void NRGDontSwitchToCandidateWithInflightSnapshot_ShouldSucceed()
{
var raft = new Raft { Snapshotting = true, StateValue = (int)RaftState.Follower };
if (!raft.Snapshotting)
{
raft.SwitchToCandidate();
}
raft.State().ShouldBe(RaftState.Follower);
}
[Fact]
public void NRGDontSwitchToCandidateWithMultipleInflightSnapshots_ShouldSucceed()
{
var raft = new Raft { Snapshotting = true, Catchup = new CatchupState(), StateValue = (int)RaftState.Follower };
if (!raft.Snapshotting && raft.Catchup is null)
{
raft.SwitchToCandidate();
}
raft.State().ShouldBe(RaftState.Follower);
}
[Fact]
public void NRGQuorumAccounting_ShouldSucceed()
{
var raft = new Raft { Csz = 5, Qn = 3 };
raft.QuorumNeeded().ShouldBe(3);
}
[Fact]
public void NRGRevalidateQuorumAfterLeaderChange_ShouldSucceed()
{
var raft = new Raft { Qn = 2, Csz = 3, StateValue = (int)RaftState.Leader, LeaderId = "N1" };
raft.UpdateLeader("N2");
raft.GroupLeader().ShouldBe("N2");
raft.QuorumNeeded().ShouldBe(2);
}
[Fact]
public void NRGIgnoreTrackResponseWhenNotLeader_ShouldSucceed()
{
var raft = new Raft { StateValue = (int)RaftState.Follower };
raft.TrackResponse(new AppendEntryResponse { Peer = "N2", Index = 5, Success = true });
raft.Acks.ShouldBeEmpty();
}
[Fact]
public void NRGSendAppendEntryNotLeader_ShouldSucceed()
{
var raft = new Raft { GroupName = "RG", StateValue = (int)RaftState.Follower, Peers_ = new Dictionary<string, Lps> { ["N2"] = new() } };
raft.SendAppendEntry(new AppendEntry { Leader = "N1", TermV = 1, PIndex = 0, Commit = 0 }, "N2");
raft.SendQ.ShouldNotBeNull();
}
[Fact]
public void NRGLostQuorum_ShouldSucceed()
{
var raft = new Raft { Csz = 3, Qn = 2, Peers_ = new Dictionary<string, Lps> { ["N2"] = new() { Ts = DateTime.UtcNow.AddMinutes(-2) } } };
raft.LostQuorum().ShouldBeTrue();
}
[Fact]
public void NRGReportLeaderAfterNoopEntry_ShouldSucceed()
{
var raft = new Raft { Id = "N1", GroupName = "RG", StateValue = (int)RaftState.Candidate, Csz = 1, Qn = 1 };
raft.SwitchToLeader();
raft.GroupLeader().ShouldBe("N1");
}
[Fact]
public void NRGSendSnapshotInstallsSnapshot_ShouldSucceed()
{
var raft = new Raft { StoreDir = Path.Combine(Path.GetTempPath(), $"raft-{Guid.NewGuid():N}") };
raft.SendSnapshotToFollower("N2", [1, 2, 3], term: 4, index: 2);
raft.Commit.ShouldBeGreaterThanOrEqualTo(2UL);
}
[Fact]
public void NRGQuorumAfterLeaderStepdown_ShouldSucceed()
{
var raft = new Raft { StateValue = (int)RaftState.Leader, Csz = 3, Qn = 2, LeaderId = "N1" };
raft.SwitchToFollower("N2");
raft.State().ShouldBe(RaftState.Follower);
raft.GroupLeader().ShouldBe("N2");
}
[Fact]
public void NRGUncommittedMembershipChangeOnNewLeader_ShouldSucceed()
{
var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Leader, PIndex = 10, Applied_ = 8 };
raft.ProposeAddPeer("N2");
raft.MembershipChangeInProgress().ShouldBeTrue();
raft.SwitchToLeader();
raft.State().ShouldBe(RaftState.Leader);
}
[Fact]
public void NRGUncommittedMembershipChangeOnNewLeaderForwardedRemovePeerProposal_ShouldSucceed()
{
var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Leader, PIndex = 10, Applied_ = 8 };
raft.HandleForwardedProposal([1, 2, 3]);
raft.PropQ.ShouldNotBeNull();
raft.PropQ!.Len().ShouldBe(1);
}
[Fact]
public void NRGIgnoreForwardedProposalIfNotCaughtUpLeader_ShouldSucceed()
{
var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Follower };
if (raft.State() == RaftState.Leader)
{
raft.HandleForwardedProposal([9]);
}
raft.PropQ.ShouldBeNull();
}
[Fact]
public void NRGAppendEntryResurrectsLeader_ShouldSucceed()
{
var raft = new Raft { HasLeaderV = 0 };
raft.ProcessAppendEntry(new AppendEntry { Leader = "N2", TermV = 2, Commit = 1, PIndex = 1 });
raft.GroupLeader().ShouldBe("N2");
}
[Fact]
public void NRGMustNotResetVoteOnStepDownOrLeaderTransfer_ShouldSucceed()
{
var raft = new Raft { Vote = "N2", StateValue = (int)RaftState.Leader };
raft.StepDown("N3");
raft.Vote.ShouldBe("N3");
raft.XferCampaign();
raft.Vote.ShouldNotBeNull();
}
}