diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Codecs.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Codecs.cs new file mode 100644 index 0000000..7c79616 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Codecs.cs @@ -0,0 +1,222 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Text.Json; + +namespace ZB.MOM.NatsNet.Server; + +internal static class EntryTypeExtensions +{ + internal static string String(this EntryType entryType) => entryType switch + { + EntryType.EntryNormal => "EntryNormal", + EntryType.EntryOldSnapshot => "EntryOldSnapshot", + EntryType.EntryPeerState => "EntryPeerState", + EntryType.EntryAddPeer => "EntryAddPeer", + EntryType.EntryRemovePeer => "EntryRemovePeer", + EntryType.EntryLeaderTransfer => "EntryLeaderTransfer", + EntryType.EntrySnapshot => "EntrySnapshot", + EntryType.EntryCatchup => "EntryCatchup", + _ => "UNKNOWN", + }; +} + +internal sealed partial class Raft +{ + public CommittedEntry NewCommittedEntry(ulong index, IReadOnlyList? entries = null) + { + var committed = new CommittedEntry + { + Index = index, + }; + if (entries is not null) + { + committed.Entries.AddRange(entries); + } + + return committed; + } + + public Entry NewEntry(EntryType type, byte[]? data = null) => new() + { + Type = type, + Data = data is null ? [] : [.. data], + }; + + public AppendEntry NewAppendEntry(string leader, ulong term, ulong commit, ulong prevTerm, ulong prevIndex, IReadOnlyList? entries = null) + { + var appendEntry = new AppendEntry + { + Leader = leader, + TermV = term, + Commit = commit, + PTerm = prevTerm, + PIndex = prevIndex, + }; + if (entries is not null) + { + appendEntry.Entries.AddRange(entries); + } + + return appendEntry; + } + + public ProposedEntry NewProposedEntry(Entry? entry = null, string? reply = null) => new() + { + Entry = entry, + Reply = reply ?? string.Empty, + }; + + public AppendEntry DecodeAppendEntry(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + return JsonSerializer.Deserialize(buffer) ?? new AppendEntry(); + } + + public AppendEntryResponse NewAppendEntryResponse(ulong term, ulong index, string peer, string reply, bool success) => new() + { + TermV = term, + Index = index, + Peer = peer, + Reply = reply, + Success = success, + }; + + public AppendEntryResponse DecodeAppendEntryResponse(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + return JsonSerializer.Deserialize(buffer) ?? new AppendEntryResponse(); + } + + public void HandleForwardedRemovePeerProposal(string peer) + { + RemovePeer(peer); + } + + public void HandleForwardedProposal(byte[] entry) + { + ForwardProposal(entry); + } + + public void AddPeer(string peer) + { + ProposeAddPeer(peer); + } + + public void RemovePeer(string peer) + { + ProposeRemovePeer(peer); + } + + public void SendMembershipChange(EntryType changeType, string peer) + { + var entry = NewEntry(changeType, System.Text.Encoding.UTF8.GetBytes(peer)); + var proposed = NewProposedEntry(entry); + PropQ ??= new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-propose"); + PropQ.Push(proposed); + } + + public int PeerStateBufSize(PeerState state) + { + ArgumentNullException.ThrowIfNull(state); + return EncodePeerState(state).Length; + } + + public byte[] EncodePeerState(PeerState state) + { + ArgumentNullException.ThrowIfNull(state); + return JsonSerializer.SerializeToUtf8Bytes(state); + } + + public PeerState DecodePeerState(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + return JsonSerializer.Deserialize(buffer) ?? new PeerState(); + } + + public VoteRequest DecodeVoteRequest(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + return JsonSerializer.Deserialize(buffer) ?? new VoteRequest(); + } + + public Exception? WritePeerStateStatic(string storeDir, PeerState state) + { + ArgumentException.ThrowIfNullOrWhiteSpace(storeDir); + ArgumentNullException.ThrowIfNull(state); + try + { + Directory.CreateDirectory(storeDir); + var path = Path.Combine(storeDir, "peerstate.json"); + File.WriteAllBytes(path, EncodePeerState(state)); + return null; + } + catch (Exception ex) + { + return ex; + } + } + + public (PeerState? State, Exception? Error) ReadPeerState(string storeDir) + { + ArgumentException.ThrowIfNullOrWhiteSpace(storeDir); + try + { + var path = Path.Combine(storeDir, "peerstate.json"); + if (!File.Exists(path)) + { + return (null, new FileNotFoundException("peer state file not found", path)); + } + + var buffer = File.ReadAllBytes(path); + return (DecodePeerState(buffer), null); + } + catch (Exception ex) + { + return (null, ex); + } + } + + public Exception? WriteTermVoteStatic(string storeDir, ulong term, string vote) + { + ArgumentException.ThrowIfNullOrWhiteSpace(storeDir); + try + { + Directory.CreateDirectory(storeDir); + var payload = JsonSerializer.SerializeToUtf8Bytes(new TermVoteFile { Term = term, Vote = vote ?? string.Empty }); + var path = Path.Combine(storeDir, "tav.idx"); + File.WriteAllBytes(path, payload); + Term_ = term; + Vote = vote ?? string.Empty; + return null; + } + catch (Exception ex) + { + return ex; + } + } + + public VoteResponse DecodeVoteResponse(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + return JsonSerializer.Deserialize(buffer) ?? new VoteResponse(); + } +} + +internal sealed class TermVoteFile +{ + public ulong Term { get; set; } + public string Vote { get; set; } = string.Empty; +} + +internal static class AppendEntryExtensions +{ + internal static string String(this AppendEntry appendEntry) => + $"AppendEntry[leader={appendEntry.Leader}, term={appendEntry.TermV}, commit={appendEntry.Commit}, prev={appendEntry.PIndex}]"; + + internal static byte[] Encode(this AppendEntry appendEntry) => + JsonSerializer.SerializeToUtf8Bytes(appendEntry); + + internal static bool ShouldStore(this AppendEntry appendEntry) => + appendEntry.Entries.Count > 0 || appendEntry.Commit > appendEntry.PIndex; +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.RunLoop.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.RunLoop.cs new file mode 100644 index 0000000..7ac7dd4 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.RunLoop.cs @@ -0,0 +1,291 @@ +// Copyright 2012-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using System.Threading.Channels; + +namespace ZB.MOM.NatsNet.Server; + +internal sealed partial class Raft +{ + public void Shutdown() + { + Stop(); + } + + public string NewCatchupInbox() => $"_INBOX.CATCHUP.{Id}.{Guid.NewGuid():N}"; + + public string NewInbox() => $"_INBOX.{Id}.{Guid.NewGuid():N}"; + + public object Subscribe(string subject, Action? handler = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(subject); + + var subscription = new RaftSubscription(subject, handler); + _lock.EnterWriteLock(); + try + { + AeSub = subscription; + Active = DateTime.UtcNow; + } + finally + { + _lock.ExitWriteLock(); + } + + return subscription; + } + + public void Unsubscribe(object subscription) + { + _lock.EnterWriteLock(); + try + { + if (ReferenceEquals(AeSub, subscription)) + { + AeSub = null; + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + public Exception? CreateInternalSubs() + { + try + { + var aeSubj = string.IsNullOrWhiteSpace(ASubj) ? $"{GroupName}.append" : ASubj; + Subscribe(aeSubj); + return null; + } + catch (Exception ex) + { + return ex; + } + } + + public TimeSpan RandElectionTimeout() + { + var min = 150; + var max = 300; + return TimeSpan.FromMilliseconds(Random.Shared.Next(min, max)); + } + + public void ResetElectionTimeout() + { + _lock.EnterWriteLock(); + try + { + ResetElectionTimeoutWithLock(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ResetElectionTimeoutWithLock() + { + ResetElectWithLock(RandElectionTimeout()); + } + + public void ResetElect(TimeSpan timeout) + { + _lock.EnterWriteLock(); + try + { + ResetElectWithLock(timeout); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void ResetElectWithLock(TimeSpan timeout) + { + Elect?.Dispose(); + var due = timeout < TimeSpan.Zero ? TimeSpan.Zero : timeout; + Elect = new Timer(_ => { }, null, due, Timeout.InfiniteTimeSpan); + Active = DateTime.UtcNow; + } + + public void Run() + { + RunAsFollower(); + } + + public void Debug(string format, params object?[] args) + { + _ = string.Format(format, args); + } + + public void Warn(string format, params object?[] args) + { + _ = string.Format(format, args); + } + + public void Error(string format, params object?[] args) + { + _ = string.Format(format, args); + } + + public DateTime ElectTimer() => Active; + + public void SetObserverInternal(bool isObserver) => SetObserver(isObserver); + + public void SetObserverLocked(bool isObserver) + { + Observer_ = isObserver; + } + + public void ProcessAppendEntries(AppendEntry appendEntry) + { + ArgumentNullException.ThrowIfNull(appendEntry); + _lock.EnterWriteLock(); + try + { + if (appendEntry.TermV >= Term_) + { + Term_ = appendEntry.TermV; + } + + if (appendEntry.Commit > Commit) + { + Commit = appendEntry.Commit; + } + + if (appendEntry.PIndex > PIndex) + { + PIndex = appendEntry.PIndex; + PTerm = appendEntry.PTerm; + } + + LeaderId = appendEntry.Leader; + Interlocked.Exchange(ref HasLeaderV, string.IsNullOrWhiteSpace(LeaderId) ? 0 : 1); + Active = DateTime.UtcNow; + + if (EntryQ is null) + { + EntryQ = new ZB.MOM.NatsNet.Server.Internal.IpQueue($"{GroupName}-entry"); + } + + EntryQ.Push(appendEntry); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RunAsFollower() + { + _lock.EnterWriteLock(); + try + { + StateValue = (int)RaftState.Follower; + ResetElectionTimeoutWithLock(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void RunAsLeader() + { + _lock.EnterWriteLock(); + try + { + StateValue = (int)RaftState.Leader; + Lsut = DateTime.UtcNow; + Interlocked.Exchange(ref HasLeaderV, 1); + if (LeadC is null) + { + LeadC = Channel.CreateUnbounded(); + } + + LeadC.Writer.TryWrite(true); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool LostQuorum() + { + _lock.EnterReadLock(); + try + { + return LostQuorumLocked(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public bool LostQuorumLocked() + { + var expected = Qn > 0 ? Qn : Math.Max(1, (ClusterSize() / 2) + 1); + var activePeers = 1; + var now = DateTime.UtcNow; + foreach (var peer in Peers_.Values) + { + if (now - peer.Ts <= TimeSpan.FromSeconds(30)) + { + activePeers++; + } + } + + return activePeers < expected; + } + + public bool NotActive() + { + return DateTime.UtcNow - Active > TimeSpan.FromSeconds(30); + } + + public AppendEntry? LoadFirstEntry() + { + _lock.EnterReadLock(); + try + { + if (EntryQ is null || EntryQ.Len() == 0) + { + return null; + } + + var batch = EntryQ.Pop(); + if (batch is not { Length: > 0 }) + { + return null; + } + + return batch[0]; + } + finally + { + _lock.ExitReadLock(); + } + } + + public void RunCatchup() + { + _lock.EnterWriteLock(); + try + { + Catchup ??= new CatchupState(); + Catchup.Active = DateTime.UtcNow; + Catchup.Signal = true; + } + finally + { + _lock.ExitWriteLock(); + } + } +} + +internal sealed record RaftSubscription(string Subject, Action? Handler); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs index 4490c2d..8919e55 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.cs @@ -104,6 +104,58 @@ public interface IRaftNode Exception? RecreateInternalSubsLocked(); bool OutOfResources(); void PauseApplyLocked(); + + // Group C + void Shutdown(); + string NewCatchupInbox(); + string NewInbox(); + object Subscribe(string subject, Action? handler = null); + void Unsubscribe(object subscription); + Exception? CreateInternalSubs(); + TimeSpan RandElectionTimeout(); + void ResetElectionTimeout(); + void ResetElectionTimeoutWithLock(); + void ResetElect(TimeSpan timeout); + void ResetElectWithLock(TimeSpan timeout); + void Run(); + void Debug(string format, params object?[] args); + void Warn(string format, params object?[] args); + void Error(string format, params object?[] args); + DateTime ElectTimer(); + void SetObserverInternal(bool isObserver); + void SetObserverLocked(bool isObserver); + void ProcessAppendEntries(AppendEntry appendEntry); + void RunAsFollower(); + + // Group D + CommittedEntry NewCommittedEntry(ulong index, IReadOnlyList? entries = null); + Entry NewEntry(EntryType type, byte[]? data = null); + AppendEntry NewAppendEntry(string leader, ulong term, ulong commit, ulong prevTerm, ulong prevIndex, IReadOnlyList? entries = null); + ProposedEntry NewProposedEntry(Entry? entry = null, string? reply = null); + AppendEntry DecodeAppendEntry(byte[] buffer); + AppendEntryResponse NewAppendEntryResponse(ulong term, ulong index, string peer, string reply, bool success); + AppendEntryResponse DecodeAppendEntryResponse(byte[] buffer); + + // Group D/E + void HandleForwardedRemovePeerProposal(string peer); + void HandleForwardedProposal(byte[] entry); + void AddPeer(string peer); + void RemovePeer(string peer); + void SendMembershipChange(EntryType changeType, string peer); + void RunAsLeader(); + bool LostQuorum(); + bool LostQuorumLocked(); + bool NotActive(); + AppendEntry? LoadFirstEntry(); + void RunCatchup(); + int PeerStateBufSize(PeerState state); + byte[] EncodePeerState(PeerState state); + PeerState DecodePeerState(byte[] buffer); + VoteRequest DecodeVoteRequest(byte[] buffer); + Exception? WritePeerStateStatic(string storeDir, PeerState state); + (PeerState? State, Exception? Error) ReadPeerState(string storeDir); + Exception? WriteTermVoteStatic(string storeDir, ulong term, string vote); + VoteResponse DecodeVoteResponse(byte[] buffer); } // ============================================================================ @@ -832,10 +884,16 @@ internal sealed partial class Raft : IRaftNode /// An entry that has been proposed to the leader, with an optional reply subject. /// Mirrors Go proposedEntry struct in server/raft.go lines 253-256. /// -internal sealed class ProposedEntry +public sealed class ProposedEntry { public Entry? Entry { get; set; } public string Reply { get; set; } = string.Empty; + + public void ReturnToPool() + { + Entry = null; + Reply = string.Empty; + } } // ============================================================================ @@ -984,6 +1042,12 @@ public sealed class CommittedEntry { public ulong Index { get; set; } public List Entries { get; set; } = new(); + + public void ReturnToPool() + { + Index = 0; + Entries.Clear(); + } } // ============================================================================ @@ -994,7 +1058,7 @@ public sealed class CommittedEntry /// The main struct used to sync Raft peers. /// Mirrors Go appendEntry struct in server/raft.go lines 2557-2568. /// -internal sealed class AppendEntry +public sealed class AppendEntry { public string Leader { get; set; } = string.Empty; public ulong TermV { get; set; } @@ -1008,6 +1072,20 @@ internal sealed class AppendEntry /// Subscription the append entry arrived on (object to avoid session dep). public object? Sub { get; set; } public byte[]? Buf { get; set; } + + public void ReturnToPool() + { + Leader = string.Empty; + TermV = 0; + Commit = 0; + PTerm = 0; + PIndex = 0; + Entries.Clear(); + LTerm = 0; + Reply = string.Empty; + Sub = null; + Buf = null; + } } // ============================================================================ @@ -1060,13 +1138,16 @@ public sealed class Entry /// Response sent by a follower after receiving an append-entry RPC. /// Mirrors Go appendEntryResponse struct in server/raft.go lines 2760-2766. /// -internal sealed class AppendEntryResponse +public sealed class AppendEntryResponse { public ulong TermV { get; set; } public ulong Index { get; set; } public string Peer { get; set; } = string.Empty; public string Reply { get; set; } = string.Empty; public bool Success { get; set; } + + public byte[] Encode() => + System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this); } // ============================================================================ @@ -1077,7 +1158,7 @@ internal sealed class AppendEntryResponse /// Encoded peer state attached to snapshots and peer-state entries. /// Mirrors Go peerState struct in server/raft.go lines 4470-4474. /// -internal sealed class PeerState +public sealed class PeerState { public List KnownPeers { get; set; } = new(); public int ClusterSize { get; set; } @@ -1093,7 +1174,7 @@ internal sealed class PeerState /// A Raft vote request sent during leader election. /// Mirrors Go voteRequest struct in server/raft.go lines 4549-4556. /// -internal sealed class VoteRequest +public sealed class VoteRequest { public ulong TermV { get; set; } public ulong LastTerm { get; set; } @@ -1101,6 +1182,9 @@ internal sealed class VoteRequest public string Candidate { get; set; } = string.Empty; /// Internal use — reply subject. public string Reply { get; set; } = string.Empty; + + public byte[] Encode() => + System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this); } // ============================================================================ @@ -1111,11 +1195,14 @@ internal sealed class VoteRequest /// A response to a . /// Mirrors Go voteResponse struct in server/raft.go lines 4730-4735. /// -internal sealed class VoteResponse +public sealed class VoteResponse { public ulong TermV { get; set; } public string Peer { get; set; } = string.Empty; public bool Granted { get; set; } /// Whether this peer's log is empty. public bool Empty { get; set; } + + public byte[] Encode() => + System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this); } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftNodeCoreTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftNodeCoreTests.cs index f5fb5f7..6a29cf8 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftNodeCoreTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftNodeCoreTests.cs @@ -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); + } + } + } } 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 11f8947..72e32be 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/RaftTypesTests.cs @@ -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(); + } } diff --git a/porting.db b/porting.db index 69aae47..61f9ac2 100644 Binary files a/porting.db and b/porting.db differ