feat(batch30): implement raft group-c run loop behaviors

This commit is contained in:
Joseph Doherty
2026-02-28 20:17:45 -05:00
parent ed1b62d6a3
commit 8bc4dfa58c
6 changed files with 731 additions and 6 deletions

View File

@@ -108,4 +108,98 @@ public sealed class RaftNodeCoreTests
raft.Peers_.Count.ShouldBe(2);
raft.RandCampaignTimeout().ShouldBeGreaterThan(TimeSpan.Zero);
}
[Fact]
public void RunLoopHelpers_WhenInvoked_ShouldManageSubscriptionAndTimers()
{
var raft = new Raft
{
Id = "N1",
GroupName = "RG",
Active = DateTime.UtcNow,
};
var inbox = raft.NewInbox();
var catchupInbox = raft.NewCatchupInbox();
inbox.ShouldContain("_INBOX.");
catchupInbox.ShouldContain("_INBOX.CATCHUP.");
var sub = raft.Subscribe("raft.append");
raft.AeSub.ShouldNotBeNull();
raft.Unsubscribe(sub);
raft.AeSub.ShouldBeNull();
raft.CreateInternalSubs().ShouldBeNull();
raft.ResetElectionTimeout();
raft.Elect.ShouldNotBeNull();
raft.ResetElect(TimeSpan.FromMilliseconds(10));
raft.ElectTimer().ShouldBeGreaterThan(DateTime.MinValue);
}
[Fact]
public void CodecHelpers_WhenRoundTrippingEntriesAndVotes_ShouldPreserveFields()
{
var raft = new Raft
{
GroupName = "RG",
};
var entry = raft.NewEntry(EntryType.EntryNormal, [1, 2, 3]);
var proposed = raft.NewProposedEntry(entry, "reply");
proposed.Reply.ShouldBe("reply");
var appendEntry = raft.NewAppendEntry("L1", 2, 1, 1, 0, [entry]);
appendEntry.String().ShouldContain("leader=L1");
appendEntry.ShouldStore().ShouldBeTrue();
var decodedAppend = raft.DecodeAppendEntry(appendEntry.Encode());
decodedAppend.Leader.ShouldBe("L1");
var appendResponse = raft.NewAppendEntryResponse(2, 1, "N2", "_R_", success: true);
var decodedResponse = raft.DecodeAppendEntryResponse(appendResponse.Encode());
decodedResponse.Success.ShouldBeTrue();
decodedResponse.Peer.ShouldBe("N2");
var voteRequest = new VoteRequest { TermV = 4, Candidate = "N3", LastIndex = 9, LastTerm = 3, Reply = "_R_" };
var decodedVoteRequest = raft.DecodeVoteRequest(voteRequest.Encode());
decodedVoteRequest.Candidate.ShouldBe("N3");
var voteResponse = new VoteResponse { TermV = 4, Peer = "N2", Granted = true, Empty = false };
var decodedVoteResponse = raft.DecodeVoteResponse(voteResponse.Encode());
decodedVoteResponse.Granted.ShouldBeTrue();
decodedVoteResponse.Peer.ShouldBe("N2");
}
[Fact]
public void PeerStatePersistence_WhenWrittenAndRead_ShouldRoundTrip()
{
var raft = new Raft();
var storeDir = Path.Combine(Path.GetTempPath(), $"raft-peer-state-{Guid.NewGuid():N}");
try
{
var state = new PeerState
{
KnownPeers = ["A1", "A2"],
ClusterSize = 2,
DomainExt = 7,
};
raft.PeerStateBufSize(state).ShouldBeGreaterThan(0);
raft.WritePeerStateStatic(storeDir, state).ShouldBeNull();
var (readState, readError) = raft.ReadPeerState(storeDir);
readError.ShouldBeNull();
readState.ShouldNotBeNull();
readState!.KnownPeers.Count.ShouldBe(2);
raft.WriteTermVoteStatic(storeDir, 6, "A1").ShouldBeNull();
raft.Term_.ShouldBe(6UL);
raft.Vote.ShouldBe("A1");
}
finally
{
if (Directory.Exists(storeDir))
{
Directory.Delete(storeDir, recursive: true);
}
}
}
}

View File

@@ -138,4 +138,35 @@ public sealed class RaftTypesTests
checkpoint.Abort();
File.Exists(checkpoint.SnapFile).ShouldBeFalse();
}
[Fact]
public void EntryAndPoolHelpers_ShouldReturnExpectedRepresentations()
{
EntryType.EntryAddPeer.String().ShouldBe("EntryAddPeer");
var committed = new CommittedEntry { Index = 2, Entries = [new Entry { Type = EntryType.EntryNormal, Data = [1] }] };
committed.ReturnToPool();
committed.Index.ShouldBe(0UL);
committed.Entries.ShouldBeEmpty();
var proposed = new ProposedEntry { Entry = new Entry { Type = EntryType.EntryNormal, Data = [2] }, Reply = "_R_" };
proposed.ReturnToPool();
proposed.Entry.ShouldBeNull();
proposed.Reply.ShouldBeEmpty();
var appendEntry = new AppendEntry
{
Leader = "N1",
TermV = 2,
Commit = 1,
PTerm = 1,
PIndex = 0,
Entries = [new Entry { Type = EntryType.EntryNormal, Data = [3] }],
Reply = "_R_",
};
appendEntry.ReturnToPool();
appendEntry.Leader.ShouldBeEmpty();
appendEntry.Entries.ShouldBeEmpty();
}
}