From 14e79f33eec0df1cca408b287c7d8a4c2f54ff11 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:41:21 -0500 Subject: [PATCH 1/6] batch31: implement raft group A catchup foundations --- .../JetStream/RaftTypes.AppendProcessing.cs | 239 +++++++++++++ .../JetStream/RaftTypes.Catchup.cs | 329 ++++++++++++++++++ .../JetStream/RaftTypes.Elections.cs | 305 ++++++++++++++++ .../JetStream/RaftTypes.cs | 55 ++- .../ImplBacklog/RaftNodeTests.Impltests.cs | 185 ++++++++++ .../JetStream/RaftTypesTests.cs | 117 +++++++ porting.db | Bin 6696960 -> 6705152 bytes 7 files changed, 1229 insertions(+), 1 deletion(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.AppendProcessing.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Catchup.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Elections.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.AppendProcessing.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.AppendProcessing.cs new file mode 100644 index 0000000..fed8779 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.AppendProcessing.cs @@ -0,0 +1,239 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Text; +using System.Text.Json; + +namespace ZB.MOM.NatsNet.Server; + +internal sealed partial class Raft +{ + public void ProcessAppendEntry(AppendEntry appendEntry) + { + ArgumentNullException.ThrowIfNull(appendEntry); + + ProcessAppendEntries(appendEntry); + if (appendEntry.ShouldStore()) + { + StoreToWAL(appendEntry); + } + } + + public void ResetInitializing() + { + Initializing = false; + } + + public void ProcessPeerState(Entry entry) + { + ArgumentNullException.ThrowIfNull(entry); + if (entry.Type != EntryType.EntryPeerState || entry.Data.Length == 0) + { + return; + } + + var peerState = DecodePeerState(entry.Data); + _lock.EnterWriteLock(); + try + { + ProposeKnownPeers(peerState.KnownPeers); + Csz = Math.Max(peerState.ClusterSize, 1); + Qn = (Csz / 2) + 1; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ProcessAppendEntryResponse(AppendEntryResponse response) + { + ArgumentNullException.ThrowIfNull(response); + TrackResponse(response); + } + + public void HandleAppendEntryResponse(AppendEntryResponse response) + { + ProcessAppendEntryResponse(response); + } + + public AppendEntry BuildAppendEntry(string peer) + { + _lock.EnterReadLock(); + try + { + var appendEntry = NewAppendEntry(Id, Term_, Commit, PTerm, PIndex); + appendEntry.Reply = AReply; + if (Pae.TryGetValue(PIndex, out var pending)) + { + appendEntry.Entries.AddRange(pending.Entries.Select(entry => NewEntry(entry.Type, entry.Data))); + } + + if (!string.IsNullOrWhiteSpace(peer)) + { + appendEntry.Reply = $"{AReply}.{peer}"; + } + + return appendEntry; + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool StoreToWAL(AppendEntry appendEntry) + { + ArgumentNullException.ThrowIfNull(appendEntry); + if (!appendEntry.ShouldStore()) + { + return false; + } + + _lock.EnterWriteLock(); + try + { + var index = appendEntry.PIndex; + foreach (var entry in appendEntry.Entries) + { + index++; + var stored = NewAppendEntry(appendEntry.Leader, appendEntry.TermV, appendEntry.Commit, appendEntry.PTerm, index, [entry]); + CachePendingEntry(stored, index); + } + + PIndex = Math.Max(PIndex, index); + PTerm = appendEntry.TermV; + Commit = Math.Max(Commit, appendEntry.Commit); + WalBytes += (ulong)appendEntry.Encode().Length; + return true; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SendAppendEntry(AppendEntry appendEntry, string? peer = null) + { + ArgumentNullException.ThrowIfNull(appendEntry); + + _lock.EnterWriteLock(); + try + { + if (!string.IsNullOrWhiteSpace(peer)) + { + SendAppendEntryLocked(peer, appendEntry); + return; + } + + foreach (var peerName in PeerNames()) + { + SendAppendEntryLocked(peerName, appendEntry); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SendAppendEntryLocked(string peer, AppendEntry appendEntry) + { + ArgumentException.ThrowIfNullOrWhiteSpace(peer); + ArgumentNullException.ThrowIfNull(appendEntry); + + CachePendingEntry(appendEntry); + TrackPeer(peer, appendEntry.PIndex); + SendRPC($"{GroupName}.append.{peer}", appendEntry.Encode()); + } + + public void CachePendingEntry(AppendEntry appendEntry, ulong? index = null) + { + ArgumentNullException.ThrowIfNull(appendEntry); + var cacheIndex = index ?? (appendEntry.PIndex > 0 ? appendEntry.PIndex : PIndex + 1); + Pae[cacheIndex] = appendEntry; + } + + public IReadOnlyList PeerNames() + { + _lock.EnterReadLock(); + try + { + return [.. Peers_.Keys.OrderBy(static key => key, StringComparer.Ordinal)]; + } + finally + { + _lock.ExitReadLock(); + } + } + + public PeerState CurrentPeerState() + { + _lock.EnterReadLock(); + try + { + return CurrentPeerStateLocked(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public PeerState CurrentPeerStateLocked() + { + return new PeerState + { + KnownPeers = [.. PeerNames()], + ClusterSize = ClusterSize(), + }; + } + + public void SendPeerState() + { + var state = CurrentPeerState(); + Wps = EncodePeerState(state); + var entry = NewEntry(EntryType.EntryPeerState, Wps); + var appendEntry = NewAppendEntry(Id, Term_, Commit, PTerm, PIndex, [entry]); + SendAppendEntry(appendEntry); + } + + public void SendHeartbeat() + { + var heartbeat = NewAppendEntry(Id, Term_, Commit, PTerm, PIndex); + SendAppendEntry(heartbeat); + } + + public Exception? WritePeerState() + { + return WritePeerStateStatic(StoreDir, CurrentPeerState()); + } + + public (ulong Term, string Vote, Exception? Error) ReadTermVote() + { + try + { + var path = Path.Combine(StoreDir, "tav.idx"); + if (!File.Exists(path)) + { + return (0, string.Empty, new FileNotFoundException("term vote file not found", path)); + } + + var payload = File.ReadAllBytes(path); + var state = JsonSerializer.Deserialize(payload); + if (state is null) + { + return (0, string.Empty, new InvalidDataException("term vote file is invalid")); + } + + Term_ = state.Term; + Vote = state.Vote; + Wtv = payload; + return (state.Term, state.Vote, null); + } + catch (Exception ex) + { + return (0, string.Empty, ex); + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Catchup.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Catchup.cs new file mode 100644 index 0000000..04e2028 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Catchup.cs @@ -0,0 +1,329 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Threading; + +namespace ZB.MOM.NatsNet.Server; + +internal sealed partial class Raft +{ + public void SendSnapshotToFollower(string peer, byte[] snapshot, ulong term = 0, ulong index = 0) + { + ArgumentException.ThrowIfNullOrWhiteSpace(peer); + ArgumentNullException.ThrowIfNull(snapshot); + + _lock.EnterWriteLock(); + try + { + var catchup = CatchupFollower(peer, term == 0 ? Term_ : term, index == 0 ? Commit : index); + catchup.Signal = false; + InstallSnapshot(snapshot, force: true); + Commit = Math.Max(Commit, catchup.CIndex); + Applied_ = Math.Max(Applied_, catchup.CIndex); + TrackPeer(peer, catchup.CIndex); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public CatchupState CatchupFollower(string peer, ulong term, ulong index) + { + ArgumentException.ThrowIfNullOrWhiteSpace(peer); + + _lock.EnterWriteLock(); + try + { + var catchup = CreateCatchup(); + catchup.CTerm = term; + catchup.CIndex = index; + catchup.PTerm = PTerm; + catchup.PIndex = PIndex; + catchup.Sub = peer; + catchup.Active = DateTime.UtcNow; + catchup.Signal = true; + return catchup; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public Entry? LoadEntry(ulong index) + { + _lock.EnterReadLock(); + try + { + if (index == 0 || !Pae.TryGetValue(index, out var appendEntry) || appendEntry.Entries.Count == 0) + { + return null; + } + + return appendEntry.Entries[0]; + } + finally + { + _lock.ExitReadLock(); + } + } + + public CommittedEntry? ApplyCommit(ulong commitIndex) + { + _lock.EnterWriteLock(); + try + { + if (commitIndex <= Applied_) + { + return null; + } + + var committed = NewCommittedEntry(commitIndex); + for (var index = Applied_ + 1; index <= commitIndex; index++) + { + if (Pae.TryGetValue(index, out var appendEntry)) + { + committed.Entries.AddRange(appendEntry.Entries); + } + } + + Applied_ = commitIndex; + Commit = Math.Max(Commit, commitIndex); + ApplyQ_ ??= new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-apply"); + ApplyQ_.Push(committed); + return committed; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool TryCommit(ulong commitIndex) => ApplyCommit(commitIndex) is not null; + + public void TrackResponse(AppendEntryResponse response) + { + ArgumentNullException.ThrowIfNull(response); + + _lock.EnterWriteLock(); + try + { + if (State() != RaftState.Leader) + { + return; + } + + TrackPeer(response.Peer, response.Index); + if (response.Success) + { + if (!Acks.TryGetValue(response.Index, out var acks)) + { + acks = new Dictionary(StringComparer.Ordinal); + Acks[response.Index] = acks; + } + + acks[response.Peer] = true; + TryCommit(response.Index); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void AdjustClusterSizeAndQuorum() + { + _lock.EnterWriteLock(); + try + { + Csz = Math.Max(Peers_.Count + 1, 1); + Qn = (Csz / 2) + 1; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void TrackPeer(string peer, ulong index) + { + ArgumentException.ThrowIfNullOrWhiteSpace(peer); + + _lock.EnterWriteLock(); + try + { + if (!Peers_.TryGetValue(peer, out var state)) + { + state = new Lps(); + Peers_[peer] = state; + } + + state.Li = Math.Max(state.Li, index); + state.Ts = DateTime.UtcNow; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RunAsCandidate() + { + SwitchToCandidate(); + RequestVote(); + } + + public void HandleAppendEntry(AppendEntry appendEntry) + { + ProcessAppendEntries(appendEntry); + } + + public void CancelCatchup() + { + _lock.EnterWriteLock(); + try + { + Catchup = null; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool CatchupStalled(TimeSpan? threshold = null) + { + _lock.EnterReadLock(); + try + { + if (Catchup is null) + { + return false; + } + + var timeout = threshold ?? TimeSpan.FromSeconds(10); + return DateTime.UtcNow - Catchup.Active > timeout; + } + finally + { + _lock.ExitReadLock(); + } + } + + public CatchupState CreateCatchup() + { + _lock.EnterWriteLock(); + try + { + Catchup ??= new CatchupState(); + Catchup.Active = DateTime.UtcNow; + return Catchup; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool SendCatchupSignal() + { + _lock.EnterWriteLock(); + try + { + var catchup = CreateCatchup(); + catchup.Signal = true; + catchup.Active = DateTime.UtcNow; + return true; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void CancelCatchupSignal() + { + _lock.EnterWriteLock(); + try + { + if (Catchup is not null) + { + Catchup.Signal = false; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void TruncateWAL(ulong index) + { + _lock.EnterWriteLock(); + try + { + foreach (var key in Pae.Keys.Where(key => key > index).ToArray()) + { + Pae.Remove(key); + } + + PIndex = Math.Min(PIndex, index); + Commit = Math.Min(Commit, index); + Applied_ = Math.Min(Applied_, index); + Processed_ = Math.Min(Processed_, index); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ResetWAL() + { + _lock.EnterWriteLock(); + try + { + Pae.Clear(); + Acks.Clear(); + PIndex = 0; + PTerm = 0; + Commit = 0; + Applied_ = 0; + Processed_ = 0; + WalBytes = 0; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void UpdateLeader(string leaderId) + { + _lock.EnterWriteLock(); + try + { + LeaderId = leaderId ?? string.Empty; + if (!string.IsNullOrWhiteSpace(LeaderId) && + !string.Equals(LeaderId, Id, StringComparison.Ordinal) && + State() == RaftState.Leader) + { + StateValue = (int)RaftState.Follower; + } + + Interlocked.Exchange(ref HasLeaderV, string.IsNullOrWhiteSpace(LeaderId) ? 0 : 1); + if (!string.IsNullOrWhiteSpace(LeaderId)) + { + Interlocked.Exchange(ref PLeaderV, 1); + } + + Active = DateTime.UtcNow; + } + finally + { + _lock.ExitWriteLock(); + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Elections.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Elections.cs new file mode 100644 index 0000000..bb7b803 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Elections.cs @@ -0,0 +1,305 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Threading; +using System.Threading.Channels; + +namespace ZB.MOM.NatsNet.Server; + +internal sealed partial class Raft +{ + public void SetWriteErrLocked(Exception? error) + { + if (error is not null && WriteErr is null) + { + WriteErr = error; + } + } + + public bool IsClosed() => State() == RaftState.Closed; + + public void SetWriteErr(Exception? error) + { + _lock.EnterWriteLock(); + try + { + SetWriteErrLocked(error); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public Exception? WriteTermVote() => WriteTermVoteStatic(StoreDir, Term_, Vote); + + public void HandleVoteResponse(VoteResponse response) + { + ArgumentNullException.ThrowIfNull(response); + + _lock.EnterWriteLock(); + try + { + if (response.TermV > Term_) + { + Term_ = response.TermV; + SwitchToFollowerLocked(string.Empty); + return; + } + + if (State() != RaftState.Candidate) + { + return; + } + + Votes_ ??= new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-votes"); + Votes_.Push(response); + if (WonElection()) + { + SwitchToLeader(); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool ProcessVoteRequest(VoteRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + _lock.EnterWriteLock(); + try + { + if (request.TermV < Term_) + { + return false; + } + + if (request.TermV > Term_) + { + Term_ = request.TermV; + Vote = string.Empty; + SwitchToFollowerLocked(string.Empty); + } + + var upToDate = request.LastTerm > PTerm || (request.LastTerm == PTerm && request.LastIndex >= PIndex); + if (!upToDate) + { + return false; + } + + if (string.IsNullOrWhiteSpace(Vote) || string.Equals(Vote, request.Candidate, StringComparison.Ordinal)) + { + Vote = request.Candidate; + _ = WriteTermVote(); + Active = DateTime.UtcNow; + return true; + } + + return false; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void HandleVoteRequest(VoteRequest request) + { + ArgumentNullException.ThrowIfNull(request); + + var granted = ProcessVoteRequest(request); + if (!string.IsNullOrWhiteSpace(request.Reply)) + { + var response = new VoteResponse + { + TermV = Term_, + Peer = Id, + Granted = granted, + Empty = PIndex == 0, + }; + SendReply(request.Reply, response.Encode()); + } + } + + public void RequestVote() + { + _lock.EnterWriteLock(); + try + { + var request = new VoteRequest + { + TermV = Term_, + LastTerm = PTerm, + LastIndex = PIndex, + Candidate = Id, + Reply = VReply, + }; + + Reqs ??= new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-requests"); + Reqs.Push(request); + SendRPC(VSubj, request.Encode()); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SendRPC(string subject, byte[] payload) + { + ArgumentException.ThrowIfNullOrWhiteSpace(subject); + ArgumentNullException.ThrowIfNull(payload); + + _lock.EnterWriteLock(); + try + { + var pending = SendQ as List<(string Subject, byte[] Payload)>; + if (pending is null) + { + pending = []; + SendQ = pending; + } + + pending.Add((subject, [.. payload])); + Active = DateTime.UtcNow; + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SendReply(string subject, byte[] payload) + { + SendRPC(subject, payload); + } + + public bool WonElection() + { + if (State() != RaftState.Candidate) + { + return false; + } + + var votes = 1; // self-vote + if (Votes_ is not null) + { + votes += Votes_.Len(); + } + + return votes >= QuorumNeeded(); + } + + public int QuorumNeeded() + { + return Qn > 0 ? Qn : (Math.Max(ClusterSize(), 1) / 2) + 1; + } + + public void UpdateLeadChange(bool isLeader) + { + _lock.EnterWriteLock(); + try + { + LeadC ??= Channel.CreateUnbounded(); + LeadC.Writer.TryWrite(isLeader); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SwitchState(RaftState newState) + { + switch (newState) + { + case RaftState.Follower: + SwitchToFollower(); + break; + case RaftState.Candidate: + SwitchToCandidate(); + break; + case RaftState.Leader: + SwitchToLeader(); + break; + default: + _lock.EnterWriteLock(); + try + { + StateValue = (int)newState; + } + finally + { + _lock.ExitWriteLock(); + } + break; + } + } + + public void SwitchToFollower(string newLeader = "") + { + _lock.EnterWriteLock(); + try + { + SwitchToFollowerLocked(newLeader); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SwitchToFollowerLocked(string newLeader = "") + { + StateValue = (int)RaftState.Follower; + if (!string.IsNullOrWhiteSpace(newLeader)) + { + LeaderId = newLeader; + } + + Interlocked.Exchange(ref HasLeaderV, string.IsNullOrWhiteSpace(LeaderId) ? 0 : 1); + UpdateLeadChange(false); + ResetElectionTimeoutWithLock(); + } + + public void SwitchToCandidate() + { + _lock.EnterWriteLock(); + try + { + StateValue = (int)RaftState.Candidate; + Term_++; + Vote = Id; + Votes_ ??= new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-votes"); + Votes_.Push(new VoteResponse { TermV = Term_, Peer = Id, Granted = true }); + UpdateLeadChange(false); + ResetElectionTimeoutWithLock(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void SwitchToLeader() + { + _lock.EnterWriteLock(); + try + { + StateValue = (int)RaftState.Leader; + LeaderId = Id; + Lsut = DateTime.UtcNow; + Interlocked.Exchange(ref HasLeaderV, 1); + Interlocked.Exchange(ref PLeaderV, 1); + UpdateLeadChange(true); + SendHeartbeat(); + } + finally + { + _lock.ExitWriteLock(); + } + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs index 8919e55..4b38f12 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs @@ -126,6 +126,24 @@ public interface IRaftNode void SetObserverLocked(bool isObserver); void ProcessAppendEntries(AppendEntry appendEntry); void RunAsFollower(); + void SendSnapshotToFollower(string peer, byte[] snapshot, ulong term = 0, ulong index = 0); + CatchupState CatchupFollower(string peer, ulong term, ulong index); + Entry? LoadEntry(ulong index); + CommittedEntry? ApplyCommit(ulong commitIndex); + bool TryCommit(ulong commitIndex); + void TrackResponse(AppendEntryResponse response); + void AdjustClusterSizeAndQuorum(); + void TrackPeer(string peer, ulong index); + void RunAsCandidate(); + void HandleAppendEntry(AppendEntry appendEntry); + void CancelCatchup(); + bool CatchupStalled(TimeSpan? threshold = null); + CatchupState CreateCatchup(); + bool SendCatchupSignal(); + void CancelCatchupSignal(); + void TruncateWAL(ulong index); + void ResetWAL(); + void UpdateLeader(string leaderId); // Group D CommittedEntry NewCommittedEntry(ulong index, IReadOnlyList? entries = null); @@ -156,6 +174,41 @@ public interface IRaftNode (PeerState? State, Exception? Error) ReadPeerState(string storeDir); Exception? WriteTermVoteStatic(string storeDir, ulong term, string vote); VoteResponse DecodeVoteResponse(byte[] buffer); + void ProcessAppendEntry(AppendEntry appendEntry); + void ResetInitializing(); + void ProcessPeerState(Entry entry); + void ProcessAppendEntryResponse(AppendEntryResponse response); + void HandleAppendEntryResponse(AppendEntryResponse response); + AppendEntry BuildAppendEntry(string peer); + bool StoreToWAL(AppendEntry appendEntry); + void SendAppendEntry(AppendEntry appendEntry, string? peer = null); + void SendAppendEntryLocked(string peer, AppendEntry appendEntry); + void CachePendingEntry(AppendEntry appendEntry, ulong? index = null); + IReadOnlyList PeerNames(); + PeerState CurrentPeerState(); + PeerState CurrentPeerStateLocked(); + void SendPeerState(); + void SendHeartbeat(); + Exception? WritePeerState(); + (ulong Term, string Vote, Exception? Error) ReadTermVote(); + void SetWriteErrLocked(Exception? error); + bool IsClosed(); + void SetWriteErr(Exception? error); + Exception? WriteTermVote(); + void HandleVoteResponse(VoteResponse response); + bool ProcessVoteRequest(VoteRequest request); + void HandleVoteRequest(VoteRequest request); + void RequestVote(); + void SendRPC(string subject, byte[] payload); + void SendReply(string subject, byte[] payload); + bool WonElection(); + int QuorumNeeded(); + void UpdateLeadChange(bool isLeader); + void SwitchState(RaftState newState); + void SwitchToFollower(string newLeader = ""); + void SwitchToFollowerLocked(string newLeader = ""); + void SwitchToCandidate(); + void SwitchToLeader(); } // ============================================================================ @@ -904,7 +957,7 @@ public sealed class ProposedEntry /// Tracks the state of a follower catch-up operation. /// Mirrors Go catchupState struct in server/raft.go lines 259-268. /// -internal sealed class CatchupState +public sealed class CatchupState { /// Subscription that catchup messages arrive on (object to avoid session dep). public object? Sub { get; set; } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs index b38726b..9563cfa 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs @@ -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(), + }; + + raft.PropQ = new ZB.MOM.NatsNet.Server.Internal.IpQueue("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 { ["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 { ["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 NRGUncommittedMembershipChangeOnNewLeaderForwarded_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(); + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs index 72e32be..6132480 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs @@ -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(), + }; + + 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 + { + ["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"); + } } diff --git a/porting.db b/porting.db index 9bac02c4ea30fea02842a6c8a396f4e4b4c78086..56235a3446b8bab9d52438d3e315ec7d2d11b69f 100644 GIT binary patch delta 5823 zcmcJReQZw^6JLu|AiUVH zw$ez_cB3j;ThO%A22=%#rj#U(^LYt4Sy!T}Ql&xZ#6N?sAhc?&whqcBt!mlc>yut= z+1iA`fBdZT__d$sew=gO)A!k%vc93WWN)mJAEPJ*f5g;4ADifCtf1RoI#D5eb1NjP&IKMNyX|H4kc0Qc+N|k~#-!SW;&}4N7Vb z)PSVUfI28C0aQRzvy_*jIwd&+#ch(B2DMRAQ=lA@ngmrYsR>X8l8S)3d!OS%=@;~; zE%b~aw=#Qaob?#fiX)W_65gw1K0@>x!hoGYjEVh%n)UI1zNd%xvEy2X8kSTT)SwV? zFfOK7^D;Fc73QFDP*P_=1tcYa>Xg(hsBMy(0ku(5)1VxZngUfWsYy@;lA6%U7)p;9 zR9U$Sbu*Oo_!T$vbGk`Tc$gfzs6FnV^Sbu9e?~#+1y>z yYChYN8pJ}J7Q{lF62xK_M#Ms#3B+Pr7!ET#i&dwzX82CQ3)+iJGmW&>S~I@p z%4-$g{+M~4)k4WPxCK7D25pC&R~W&0MRwtQp~8rUWLIU`%qDs}bxJp)?bEcW7gbSJ zmi{|s11>hv2XW^xdI~c=G9$kDTcpPS`6p7~zYe1!+&7I>a+}p4~ z7y)uX0f<@!r~wwx09rr?=m7(e1^fV51}q1%fgB(gSOMe#D}j7q6;J>efkL1NFagED zL%?d`PPl~aUv04HZtFH{TlBAK4zoAeBJ}~)pOse>$K`IcOtyoWqMxVmwVTL^gEd?f zzqkX%ZbyT++qt;M=)kYoxvS)EV=wA(Etl{t->>Di;v*l!JF+>rHE|CE?{;uL-1BVq z((Ae9;2w%E7dyH0_|lpc5NnX<<2EO!#w9LpW!!IsRu`wD@%4{POCHa;xze~g9&vMD z;-0bZCsEXs81bct8zt8^Uzd1Of3M@3$P6$oA{~CRo-2&^{Y5?3hEIO3Gva^LbCvNG zO9L0cO}|>cGvU3TgGYLvaP_qeYCP7UnsHp@snV=>Tlp&HJ z$A#mgzU4Ank0ay0T!vl1_NyIh4d)VmboQ4K-)w|O0a+fk3oA{2W48Q;d=om2bg}^R z31g?1W-c-KG#93fSu0)Q-f9Qa>SWUy(X<866?nC2 znpP#7PLnI8Px9$rX=SqMRLqpF!4n1E>@=^{nru5suJy<}X>MG^`woNieY`Ytvhz4`X5Q~i_ob~*HVwy2Y3u#Gv~|h0WBBGLyoYw)wYA*a zVi5XE{k>|N4vso2r~%5*rT;*`RyUyiO53iP(d4l{bwu^8YP0g!iq915<S=4F=u#r{%uvT5nIN+q=i)@7}|wSiTMjY)uh(b}6tb6M>JU2%K9Atjt8<<`jWv zVuARZfAeg~gM6BsQe+8YmdEVO#Jz1ykw><4vm+CEEh+M1yL}zL@C~nepwF^7MHXoS zW_u>?Z9|GY(jUw50z96@T~$mmXXo+r-v zUxRa+M-b83mFygiIny?r=4ni{btc=Ari@*$G3@=E#vIAcr2FE(iOy+03h`WCo9s;5 zEFO9toL~4L?GvOu*_rfH#&%z4vYKRH(mUhNWbS)AlZl5hneGN<*s6b1hl{O6=LM^Y z&en-%t+o^kip+3}?SXd~Cj>s4B5-NqA5{7u+(|^t+L|JZECn3?*S(y2ivIUb;ruUt Ct7~`w delta 1751 zcmY+^e{2(F7zgmX>)Y$~uD=8}U|mPI@s?4LbsLOzY;M2+89(H=2nbu(4wp$F2pEHr zEn-FpF-QUrFoRpi+{l_SMLmKu;+Ts$rHKcAcT}3)-rYlJM%=9JFYi7ELw8cyp zgjzv}nDKKoSC}b*G}lZ$NF`=EffO`TH&UjVx{&@`XQv+cSsa=pexX}K(k4+>63Q`U zKh=CMj#9MU&SGbtGlQB6q{;f(0_h?X&*=M#B_`2%#S_%elt>|o$`sFGdUV(0qEZDP z$7V@)iwRk4(YDFya(SeOI3l-?_? zEKeSZF(P>!#)#xm7$cI$U_8(TRK|$p?i(W#dUvn1E?bE^!nh{a>5j`%l}O?hX|aB6 zsk>Ee3E!0T@J-95bD7R>9qYAL^@?h>rzsJe$9jjIvP_h+#qF*~qzO)=N# zPMqZ!H>7p21{&BY`>5{{Q>n9qO)?ro9V|vays6-hg z@Iof|APdHUAF?3_a`gwj0|LU6NSe@)IG)ZhVxg)eXU2Wwr49Z*aqZ1 z)`Qu8FNOSKmi~JwcUvhM;{B8}U>#n}yaLXRRlZWdZyBrf#F?F5E#yICuRbW`3n>2e zNFSl0QlN+jjK-QGUPn@+E4A$|`l*Qfj8P+|@^ch%_?p0jK_L8%oougr$-O3@P3$2FdF_+zL}OS!a{K9PI5D=LZ6lL+|&~ss$r~! zRdNWx1jvJl5QIrE8J>hGkPjg!fI=vOsW1(SVLFsR7)oIVltDQ>1r<;UGhr6YhANl? zb73Al4fCNI7QjMS1dHJrSOQC787zktuo70mv#=VTgEg=g*1_|z9yUM?)ItR6bXFOC zw85+G7JiXjf?OjnldEj$aY(LoBLQCjZ5D#I$cT zS$(Ko(E7DrtxY?ig>)8=HrZVcNyz;7%gzV|8$215=!vdk?u2lZuHUQWboEYckT&0| zU9NX0qBe`Tg8EKI(`j|1CzBG59yi@+^mtmtzNjwt#VlJaF1eX({(9fRLho7~ueBuW K&4Kz|_J05&Ktq%O From a0868e900bac06aa43168aff785a762c95b6b8e6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:42:40 -0500 Subject: [PATCH 2/6] batch31: verify raft feature group B statuses --- porting.db | Bin 6705152 -> 6709248 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index 56235a3446b8bab9d52438d3e315ec7d2d11b69f..476fb760fca70065432e3b0e53b38e1d30131307 100644 GIT binary patch delta 4320 zcmb7`4Nz3q702Iudw2KkTXtC$@MCush>Bp;c5O=tKm|Sd2ueHK}#d)Ui&-cAB(z_sLr}yJ5&M zcjo-=J?DS#Iq%-Hlvcg1DQ$XNQ(@H}<~W`IVB#$P@Q1G1Q~2Unk4}+~PT^0T5Dev5 zaEni|@OqQRF1+L`F7g-oywW@Pi(CBSWZl#KXHA7nW=#JXW3T+bTYLkFZPK@M=E7|+ zz!u+jpLA9}bB9N=>pI7ASJl~WMy)D!F*>PIC!<=GIv7=|bcNA=mD(A-qS9qXpwdM~ z9+lcwGhD6W1x5>1I?u?dQY)i0mCiA;sno(Klv=Yq{vLmG0spbAbqI$ve41RDDQtd{ zoE7$Sa+*sx#7i;WzCZbxTlf{v{}ksJ3eys$CO*&W^ZSa6S%I%JnyXSbqb#f}6tala z#Qhgtddu+KPGt4i&RPO5a7Q7w-53R!+;fjZyD$fME)Mypji&uF1a zt&E&1onw@yQVS!SO3jSSa+6m`oh2DG@j}V=9UiGu{%4Kwg(3I6xqzD)oM~lNM$kIP ztO-G@g;}G6Rx`8Wf>sl=q@Z;c6B>jyxq+LYNCa+yA`!R&ibUY+D-wY(uSm2pOOd!B z*ER?RN#c7kDQr&Xmt#7FMZ6G`f@L`)7Rg6Cg}suFE%+)u<&$~@c~Xz&M$H6Wv34!7 z8vkwFAf3{H&?@NoST0|(8g|4!B_7iMM{hMdO*>)^V^ShN5ohlu`8e;_MmxIRB_{mC zUXqL*HN-&Dr)Op0!@cAP-hN0@@YHK$TyQ}QER;=ub2&E8CBsp-kIV^pjK}*oh#sfz zBe7`vL^raZ?}Rs(;jVd#uLZmIF`??lzJT%j!!}&ApRC2*8;Bh{%vuSr?k7p1{jbV0dyh0?@zD`m@i#X$T@>?7lKlE`Xi3dnpztwdI$TGZriEV2;z>fIo0iql+ zn5UdyN}6cdt`>q$Gl(Z%Zak9w;XfAYcOYoOeZx(~=hJlM%r z&2?nxx2yVY6*;Uc|E7Vkp=Bdr>)8<5Ft8!9VPqqQ4HFw?`J0AV&4)zyCHJ|k?REcE zNA!0&Gv5Eg{fI~-SWhTD=53S@-*xZgi8+H?DBJG2M-m=zcDHM|y!0#gM?3`!g&`D% zQW!=djzT3N{LM3Zp5Ep^!)+i9#}kCn$`iFpk1_3MmvOP)Mba zMqwg_Cn-##Fqy&>3JwbC6f!7eQpln(mBKU%*%X`nN$;LP_Q5-4~{lD~gjXLR({!{6m)FZX)Khgi**kG)}ycL!N zc}tadC68Yn^(M%=YVTStF`nT(=sND*yhys<|HoA74x_Fx>I|cfFuH;bT`p_v<*><% zc)Q1y&9~yH&s^4=maw(vFlq{;>M%NrBks9Z%A?-*Rtto6(KNZK&ikdt9OiTuPoMBE zE+_ZhY&@CUJ(1;>V&{|Gch6 zXVbn!I!K!4HQ}0&%x~pR2m12QMwB@!JrZXZ;&<80;xqj|_x2sdz7nYkn_m@X2-!~j zv%{Zn3-)41r8|n}*~gyQVV#}aal>hnLUUU)<=e4R8q(| z>KjZ7>mmi(9`8SO5c{u<48PD<0rk=jT!A%_66gCQ)N4Ah#Og?i);Sby@s(W@e( z&-FzQ@*=K`lxPV`2+qM=#1)a@%}RK7&LBQQUSzn^jj(sLdoY(^d89z7NqzS_3jC+x zknb$@+k_MoYswbV)z?uSF*}WI#yH7ks23lJ^YsFGlfHyIHTpUl3?}nPs+0_^vLMj(>CbS1E3e%572fl=dU!q^mn; zu${L?@eFmD{>}b{D4t3~5u7<;np>iHDxE`chW~*4D2ivOLG<5!a}-ad7YNR2VY?Sa z@eDos{+iE6@l@Wo;0%4AK$V`0;;BC4zV8!o^ha@29;@KY2s@@Pil_201!wpt@<#Ct Uy}$km6-Mz4J+**m@ZabE18&*3*#H0l delta 1912 zcmY+Fd2Cd55Xav!d%f+xK1wN+w$NRm98#%VmZP@ZSlS}DLY3WimjF>50f|9^Ev17b z2#CONNm@!vjj$|7`vfYMBg#KE!~hz(4FZCOM)?E&5dG~wT;Y#-na^)#zcas?x4RXs z+trGi?drxf+fGTchd#~Bmv=4C#>eT1 zpxihe7Su0JhXkd^DI)0qs~t46?#b82$xWs$N7<~(>E@0>%EJCx=aqF5v_KeYU=yr{ zWiTIRLlGo{U45k9Q?ID4>c{GG)9F_>%jU^|QZ36{d2G2dlv5l=I;d?@Ki*NU3|%3%K4VR$mmJs17%Ir=u5k|v+wyAETh zc`2xL%-1~UC2h&l_sg^s=3jNnZCBnAy;K?*J2@=8fidrp@cP8Oi150_yo16^j(K6> zSsDj8C9KTIi!RHOh%U;Kh%U*Jh%U&Ih%U#HXcC?!anx)KE2UoN-h^y%W)GPO=aq>v z-yGhh(A+&Gaf7xd`-);_Ur{gD^-i3nkMKygVX$3&oG`=vg?8Wd&{d$$QJS19#VcIY3P4D z^4XgyuQbaVVV9ljYhXE7j)mSlz7~A3D&k&at$S^(jovFdIWct?->-$fR)gL>P-pcK z3#=H}s{@zp0VjX5_h~a`-#X|iN0qbqV7(Yhq&`h!tP?Wv96quaE^$(kBZF(|(+qB| zw>t3ful3Nzr$4l|I>@SN?1P8wqfW8pt~{aJINjOnGJ8iFYw8v0^vl$ zR|yLVClMAA788~bP9}Vfa0=m6!fAxl31<+_B%DR~I^i3HvkB)AmJ+^6_!i+@LVq&` z6jbj@zneC{amUWiYkm(uo@YD&eW29H6}JNsbJu)hg$&VOEHizfkqUAd7yRV!Y!)vv zj>yy=p!Txn_HsGmNZc?Y{(O+G2IZ*Ez6|*VP9f@Y;r8 z!0h^4V2{F!8iHx&kM{!?RbA{=|L-5s(}Ryc2o$b?NN^uSLK|JG=a!UMq;K=EH>dlV O=+=`x=B?s+>B_&q8j2tQ From d063961f00a67cc92cd580129440bd44752f0a2c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:44:19 -0500 Subject: [PATCH 3/6] batch31: verify raft feature group C statuses --- porting.db | Bin 6709248 -> 6717440 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index 476fb760fca70065432e3b0e53b38e1d30131307..a3bdcbf48096db72534983b7d8a414a4ea4bf3ea 100644 GIT binary patch delta 3583 zcmZvedr%Zt7RI}K?$A9wJ;SIx!~p|{81ca{4n&O!2ndMyNF=*4nxq?NI*^!{Aexw$ zV6t(mc1_F!j{!oEtTC$zVc9KvcgtAO+HxxrWOUk`-_v26Q3N?lJYs%RX`WQLjvSl#(ms1o){Y_W5P@U1$ zB&rW|HG!&4SL3LDtE-!+UewhXswZ_dimFamBh*%ks@3HU9IVjQFshZh8bXz)t3g!h zx*9+=v$L?0J4yHNW%)j;hg_mSJRV#FCA~}sFcNbXx+G@4`a$5F#7rLG`Fqe{@#O}>x*udc>WP3meCRajResJ_(I4OE}%Y8X|Yu7>#g`sm;3au5fP z>S_ShuXS}@EtIapIJQey{iq(&)iu6Pd44BT#1e|4`;;?}GBssM4qGy_r)h5;Kjq#* z@sYo7DJeb7w^`gt!HegYP_GDIG1W8?vIH-jFPdAfbpDHZffsP-k#-M$%5D1(EebQr zqOgq)I_Z0;)9$~xQ(Ynh1-vxXos|`2VPrsCC#^Mf^bARbUsaGLP8P;CjDndauW_NTw zSq1cmxc0{wT$@yaKNGcDLEJIqwt=r(;(T!>qF z<8b2c%Y+@LrWCvA`6n%K2e~Fr=I-aJxi!k5R+Tqo( z=%t6CBP=e?2x~!6TXc%3x(4TNO5YNpgUW;6acKp7b6Ir5s<4<&PblsQsfS6Aiyn6+RZ{3E$ad=&J~@}$F&UZz6X)D^sF19wA6fO*5x z68Qa;^!6(!f@e;I$Z1mlMnO*ch=;iobRwlhIwZ+@E-$7+C$pqRH$&CbaH%y z&=+8 zD9-A(u#Uy7g>hDwg>^J$4a8Xm3+qV4s&>nuPu>wr)oJ0p5u4N>Hz{vn)mjkysUXhk zu&^3h&v;gxg;j4mqmi{RYAr{vG%TLpo}e1B)r^IT^jZN-_~fmzo!c$u(^`y6+W( zYfrLh5v}w#f5FVvC`9WV3#*~zv5r@*vn{NKa>uLzw9c}yYNf`zsa=oAe`x;$vAf4F delta 1802 zcmY+^eNYs29LMo}_}%TXcN`}w4+07(QmB9rp;ihUPX~e>Y7Z*f?QwU5q{*OGHkC9^ zGR;Us27J>f(TTFrh-${28V9i)GbfeI981!Zd_aT7$fj|0#y;F$Tl>dr=HC0;{r$eT ze{4g;F0r9^r`VieYUem};LYS*zWq~AUKU@!bx)SMCyW2%6aF1~>k|JAA5`yO;=5Q> zuXKvDSFc}Jx7NSWFCSJ9UE$wmTR4t8qhCCaI;z)M)Iq(zNA1(A54B6L)2QuwokG2$ z*GW{JUf-g;dL8GMa@;aKzrkX$UdK@L^g4>l(CcedqF!I2{`+S~)TuZ53!5TECwExb z&WUTq)uNkvf=UeC;vJ0YE_)VIUboOrL62~VdKBU2mPj#4@c9G&`g*@lzG0|ALtQu2 zHA7uB)D=VhYN!E2U5=z-v%^*zG+ezTzR212nJyaE`VDo#P(RDP>Pyu^jzo1{!KU`q z3WWvZ>{Z;j4I4LkfjqW|MmEGGQExsQLuvVJ61jh{+G$BXtE76L zIkYfayVm1mD=E5M3g2ugV0OA5Ev8b(6lSKE3)o_sm17I9-YH-yp~u7-tu>Uq$Pr7< zLY7OZKgNgdh@y`R*+gyQp9)zuMc<7GEkw}$zp!0R5u2>7?kr-P^^GHFUNKA4HjZ!g zw;g1$^ja~SM$skzK7Ov4P0?-`+v=l>i*a&l2}>vM5mV>@nd(Zyv(fGnw!pZ+NOG|R z?c{71E7Iob`+!;LT^CE$7EZeGb_U+Cga;5-nc18Hjs7eLv z&DH*S@D7v7)x?suIdWT<)9(E&zHvtr0~44*0t-Zd3|6o~B-qs*O;PL5F!K<1RW*4W zr+J&~mYwn(d8!;|Z&8Da!)9XAAZMpr0mof_A2nQ4x*OYT954o=VJwV;@el*C5C`!v z0TLh)l3*fCf@Da6RG17?APuHMIy?XmLIzBO=`aH_Aq%o$Cd`7_Fb5uj9GDC9U_Lwy zkHDj_02V?ncCsX~U3D19gOSkFt{))UfRsYTi*y&%;}Ria7wTeu-ypzQa3bJd~&CDz2Ohd3{p zJAJF@mtjvNow%+Hu+c9Lbq^}D$zS7`t4_F~?8W)`LO`VUqljo<(P From 09c407781a7dfc0c1db1d18a6401291418cc7e58 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:47:07 -0500 Subject: [PATCH 4/6] batch31: verify raft test wave t1 statuses --- porting.db | Bin 6717440 -> 6717440 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/porting.db b/porting.db index a3bdcbf48096db72534983b7d8a414a4ea4bf3ea..f2cff1cb4f7d57499db578c909104f3107a61ae9 100644 GIT binary patch delta 2897 zcmc)LT})GF7zgn41@!c^F9(H#mV&2p@Pk=7Ed?qnG90K-Me$=!H)#tk?IZ=;`jLfc zD-A3-#VoSNIJ1G-g%{iO!tLy8A2%lZik*pGm_^+rlVn*k&6dnX%^n++s@)A2?Bdrv z=RD_}_kZ5@eH)9NVq@t1Xa)U#|5(M~SOtw3#3Bx5p=^|cbcjcKBp?GaBBG5|);)jeO44gp>afZ`) zW}iV^n3tr?Pi_eqlN8ZDF^KDFT7mmUanTa8A+7Jiqp9Iqo*J&148TSrR&(S$K6;4Y zu;dzL)-Dk7%&#kWwt$I5_j>gS-F3>Ist#kdC{-Q8sw7qI#wtHm1+dbmsxIxFzr?Mq z(3m?ud8R?T`?tvE2ri16JkyfMOUz2lPUI&FE=U)9A$+c^6vSFVZYS$H{}Nmr&~>Z4#F+@0_5D1H$mIlvSK*iE=h3YmTXJAIX*6LXQ1ufg2}f? z#-vrLw(ij!#C;{Hd8@%GWI1Jt0rd z7D*^AY2)}4N-e`sHaIh&SmDkwr5}Fz?(wz5M-&0f!^#}^c34S&wVwu+35GG=)-Dby z7M7zZ+Nk|@RLQ1UEaCQ1B@TOTND7SJXq^oo98(&nMjU)Nte`Tq04+p|&|*}MUO-Eb z6)i;-NJTbeN0rEds!%nmK~A&`)uQES1zL&f&?;1qUPKLOHF^m(q9(KktwrllGipID zv>t6ht!N|Kgxb(%v;}QN+tABsJ9-7}Ks(W^=ryzpwQCQD-P!f@11&4*UQCzpXZcf1 zIb-JE<_|JN-@y0sUVe+-!8h?vz8suCx;2fx>?S$18QvOqD~vt?=R;x<+zyEZjs)>z zU5{vn{@tQExAD>H-RBDj{XyJ18sK;UFD&g6w+xnDb0ZJ(q7Kxlm0k1Q7?BBAN9DT6 zN#nRtHM9tReVjk7({e6n|DLVOD&^`li%griqPXY15`nTX#qqp?czfmEP%zpa^+lo) z%tM!ZmcVF&;oy&@{z9jHS>>Ze9`!_aTdU+?$l4q3=?eQI5jh(6gd)LcuqPz<`ogl; z6YU7d*p2pd^mH$W_-ub+>;7POx9s)FVW0K!o5;~XxMy!yK<@J3kapExZB;9+s!djH zwT`M%EPWS6IWeymCJ0E$U`t4QQW&$H?Qs!8V&%|=t)&F5B6l4z90G2++1S)i-?Iy6Qds-9cn9fG0sc-s| F%)cmz_wxV% delta 1216 zcmY+>ZA@EL7zglsPfJ^FdrvPfJ8W}|+i@8%heb-kGRDhLgi#oCo8nZ~ZWIoU;g|fM zljl75<~i~BC0iV3-}0-IqtpKMv|j}VHn2kuIKT-m$OR4D-~k=Doc4Q<&D+1S%_+1P zVI?JYzQVuevwVh6@)SSKpXV*ShFe^4m;Q_XBUMLPkk0oS?y1v85jB6roD^A#6ny-) zk*~^Ys&U=LJd7@A#)zrgirGMHFn01}Y(Pt{t43EVs&yON?b+1tHr`?SJlQ?Q3u`AY zdyMkD{|`96+9*Ht7~QJcLx1Z=#m;pj!|ST9b=6+}8TE3bZf*2Ae*d%b3O8ncYV>B5 z#ur~YbyVv>s>!BSq>5~6M%tQ9yO9dBX%~`~O%3w(Ka4{*_Z$D6OvyWc8aAiIdR%UAzLmsQ`H3%Raqil+X>Y)+{yn*T8ReA(Pjeaq$$ z{mM11$knENlCP*rw;s^DnOfXd9#sMX7hTVoCViSQ6Ecx8`xsr0i!+X3FcF)wQR09o zq}qhILGe&sPP%=?1RrdZ?JK2tdj6aKv)mlFzCiElig?oMT3gXj(h4!AHqlJVs-n=C zl}BRC!v5YbE1}^r>o6@A4%+jyFV%(uMP;3dU_kF=>ISMjYt_pi($)%NiXyL`vwSxD zS;u5p-aci~6MFjPK6M2`W zrw2+T|2ATCs_)TQCNfKJ9yh#HI%FJ7m)(p&6pq3%7?5Q*W4ER}cD-8hhWQ0OqgP9x NshYa1esyr3{R@yL)qMZ} From 57be6da558bf88ec5464638a7b77939279c517da Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:49:05 -0500 Subject: [PATCH 5/6] batch31: verify raft test wave t2 statuses --- .../ImplBacklog/RaftNodeTests.Impltests.cs | 2 +- porting.db | Bin 6717440 -> 6721536 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs index 9563cfa..03b9612 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ImplBacklog/RaftNodeTests.Impltests.cs @@ -326,7 +326,7 @@ public sealed class RaftNodeTests } [Fact] - public void NRGUncommittedMembershipChangeOnNewLeaderForwarded_ShouldSucceed() + public void NRGUncommittedMembershipChangeOnNewLeaderForwardedRemovePeerProposal_ShouldSucceed() { var raft = new Raft { Id = "N1", StateValue = (int)RaftState.Leader, PIndex = 10, Applied_ = 8 }; raft.HandleForwardedProposal([1, 2, 3]); diff --git a/porting.db b/porting.db index f2cff1cb4f7d57499db578c909104f3107a61ae9..9e936b5aa227ac1eea70cc3e1e3050474f2b4e7d 100644 GIT binary patch delta 4443 zcmc(he^3v_HH?0101J&f`F$dcPs)@jr>SWYAbL?ZQ+RC0SA&?;KGj@ zGfnXnB{LF)H0MKM2Cze>ai%{E)3}qFPK!?4nD`@@X{#l)feE42Of5Q1$2Lye=MDsF zH#7Pd%zU`_dH30UzVG`y`|Nw@P@m?|P@ksT95qN$(e*vjR(kL)PeCSq=E$kcj#HVm zaw?Ol9@4!T6HkR5l$bH5gl1ym!Mib`NEx`w93atnVSoSX4)X+qXb={OLZT5ZqC+@@ zAbKQ5F;DRCjL>%YU6-Mf9#^tX7?OwS3MiG0Pg{;@E>p?Xjn4=Cz7`)jik4x)a>FlW zW8OAiV__EN`q(-uZ^7zC%eG+kptbm{ZnPGk)un97Hokk2eOoh1!K8;-6-pPw2~E~7rsZ+ zJ7FM3csq8CN?y3h&5uzArG^*o{W*)%#52t;tyQE?_a`bNGU~cX*vfk3GC9lmvk$F&^jv@gN-DVx3p?;{nmNM3H__ zY=%3E-U8kGL=pb_k)(&84~n^qF3M=Pn8BYMV%ZLMNJJh(5|BhB2{9wdhy_`RtWww^ zi4>T0d!i1~|1#X-XZ7!JB@9DNg{7TSrY5B{hGByW_lfi(ouDhG?wTl?3D$+8!@oOt zncutd!5jZ>g5b2&2vbKz85SMF8$P$2 z4IvgLJ5uA6Lo?E^=~!yT3W}y&M_Xmy#W0rQbO?MZwa(=$a6pWO#2Kk*ap@f;HzdT0 ztVU9hRAdd3hNL4I$XdjPtV7l#c4PyRi8v4^l7+}fHnI`PL2{8iWD}B)dcAyR}CBTpb-MYbVNB2OV-L$)JdN4|mVKuVBOrQ}AL@_VB;pV1AI?S{Ybm$dmY zrxnaXnii@qxz0n;uz5c7omX6D7`o(2gWk)oxq_Ul+2ISWYm9!Y^74DG^K=45(a+)@ zGKbBf*zj*7W~(urtkS&3o{gI?-uoi;@csw$BBR4^jqEr%nK#lgGUh^dB2Oc`kln~0 zSl!>%fZyge!|`%~S31|YPq4HN zl5K7oo*DO;q0;8I!0FwBp(A8-BbCTA$X;Zh60&*xZ)iyw^*Ljp6w{c;PCJu10~g?` zxLqWh+s5T_HtjL(FGw0m&|T2oA$JMk?s2!cN$x}LSL7ylmQ29jtDaX^oFuQ1mvk41 zAG=WGkJl*CA9^~Y;H*zD!Y039fS+DNMQ{3Wbp4v=9z(jIVaA&Tv)4TahIGTkO|RML z4tL;h`#;jmD7HU%(r6Z+*a^GFJ+DJ_onV0jbpj8Yr@cmKo%SXxTR!r<%d#J9 zu267hOh^D>iq23Rlb&N5a17V*bQ$==dr|k9*8t`l_%Af>33MEp_8?xQ3aLhFlq1u= z;Bk$KdLOibT8mEOW!mW}WeZM9GGS=m|Y5guVh ztJEWaHMZ*iR+J?>)Z?W;zLw@=zlZjU}e8!UZo|tV`+B#!1^5~2|h3{-S{?nWA6P^%UrUhd9%K; zL8{*Gt9~xfSl`eh1-#A8z8Wc`s+6Q8vsLRCU${0oYvJs=9C=5j>(!NZsh6p9S>xwQk>r-F`XMCYAT6&#Qglo37gRdF=Y?AE!t7 z7^bNQRO{P3_HFfekMMb4qaJUr^YYkv)gwIgigNo?N%&Y)`oIo$L*YNGoXr0ogC0S5 Tin?yqKF7<>JoRNf?5W|uKM&c? delta 1651 zcmY+@e^3-v902g$y}i5LcXxZs4_D$2SrLIQ4dlouQxKID5m=PWPHBNV`~l{LXzGlG zik{_f^7tC#5Y{#QSEEzDai%eJDu1MO;v}X{;Yb8pj^j*@)6kFa9fbPFXXpEC-h1=j zT~kM!p{c3Ou-{>9XBg9p18N@IepJuTWZSnL$qXFHWa*JiuBJoy)aqa&1JoMkN?15G zHDNYw*X)}~z;S~^2E-#HG9fb(kcc8P7G$N48@3b2*fRKWue6lyrrG->XNa8%rEdGe zq*8u4Y6R9Va;!x{Mdueb` z?xN>d`GJwWOFPZ7%7LkllT<<&$?L22H8l9Vi7aLad1Ic$9)Bpj0#wrJ;0`fu2S# z^bDGWCZlIjCdxunP&RU-9P}KTik?Tg=mqp5nucCN)6oo+hw{-(RDfoo*=PIEJB{&uOPw zers|t2K)`m7&v`F+cQdh5Wo}cW>gi#jt4S8P zhLyB`yytC$L=qTjy{}#YH4XEEID^7fB-tgos1p zU9nf}7QYdXL+vGfPw-m{c!2;PW2c0UKN%39k0&L-631 zF2Sb$Dhcjg#b^~aOymmw*PXDlPfw*&f6^0KD6W&OP~!49VZ$}u54C-I5@hyOIbj`s z==AIQ8J@q)uV<**tAA#IES2qqtdQ3h*w&|`DpZYXke6=j^KIE>koXy3_SYsw-a3(| KGa748TmA#rf?kRM From 3bbb6efddb215ec21275011de1a0fa639d641e78 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 20:50:37 -0500 Subject: [PATCH 6/6] batch31: complete raft part 2 batch --- porting.db | Bin 6721536 -> 6721536 bytes reports/current.md | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/porting.db b/porting.db index 9e936b5aa227ac1eea70cc3e1e3050474f2b4e7d..4e827a7626776085291cb8f22349974bbf1d063b 100644 GIT binary patch delta 390 zcmXxcNpBKi0Dxh>89KmFmQtmtrPxKGP>~il>Q=<9xRh?vRzHBWN=AAb4> z(9axtc1_?7ngyRe|!YE^$;3QFEj1wop1gA)HniP{vk>(6%ImdY}kYSpO zT;eiUxXKJ!u94$9H@L|yZZk`sJKW_S_nD)>JPRzc#4-;kvcf8BJmk?AshHxep_($P zc;6dwQ-<5Krp$Olz9E}?r&8P4u5ITA`bd{W%P^zDkgk?hC;?U${g@9D;{KQQv4pzFnm+cugNLWGUK w-(|vkkf&2GN}F|~^g{;QuR8Lj{w~**mdBPSmZz4IW!+M?Y_xOT%GYoI0M9##TmS$7 delta 382 zcmXZUNiRcD06^j1zK*Y`c`B`WENY%9rD~pM*DN9tkx0Z6If)fZPi(xcjSbS3q!K$D ziJxKT7kH8`zT~XVxqh#l|3hF#!GaYVb{sfSagjhGNw`TSg;df=Cxc9~$R>weJmis& z7as)_QbaK&lu|}H6;x71HGXQSrH%mgG|)&B&9u-;8|`$^Nf+Jp&`Xd$`Ux?>AYq1x zFw6*}j4{pxlT0zq471EJ&jO1qvCImqti4FHE^ahMm*tvYm-*r{8qsA(QnpL+h~vGZ z-GifjpWACnh_^R-|0EAnD$Y#KQ^&Vs(0*$V+8%6`)<