feat(batch30): implement raft group-c run loop behaviors
This commit is contained in:
222
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Codecs.cs
Normal file
222
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.Codecs.cs
Normal file
@@ -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<Entry>? 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<Entry>? 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<AppendEntry>(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<AppendEntryResponse>(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<ProposedEntry>($"{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<PeerState>(buffer) ?? new PeerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public VoteRequest DecodeVoteRequest(byte[] buffer)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(buffer);
|
||||||
|
return JsonSerializer.Deserialize<VoteRequest>(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<VoteResponse>(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;
|
||||||
|
}
|
||||||
291
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.RunLoop.cs
Normal file
291
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/RaftTypes.RunLoop.cs
Normal file
@@ -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<byte[]?>? 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<AppendEntry>($"{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<bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<byte[]?>? Handler);
|
||||||
@@ -104,6 +104,58 @@ public interface IRaftNode
|
|||||||
Exception? RecreateInternalSubsLocked();
|
Exception? RecreateInternalSubsLocked();
|
||||||
bool OutOfResources();
|
bool OutOfResources();
|
||||||
void PauseApplyLocked();
|
void PauseApplyLocked();
|
||||||
|
|
||||||
|
// Group C
|
||||||
|
void Shutdown();
|
||||||
|
string NewCatchupInbox();
|
||||||
|
string NewInbox();
|
||||||
|
object Subscribe(string subject, Action<byte[]?>? 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<Entry>? entries = null);
|
||||||
|
Entry NewEntry(EntryType type, byte[]? data = null);
|
||||||
|
AppendEntry NewAppendEntry(string leader, ulong term, ulong commit, ulong prevTerm, ulong prevIndex, IReadOnlyList<Entry>? 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.
|
/// An entry that has been proposed to the leader, with an optional reply subject.
|
||||||
/// Mirrors Go <c>proposedEntry</c> struct in server/raft.go lines 253-256.
|
/// Mirrors Go <c>proposedEntry</c> struct in server/raft.go lines 253-256.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ProposedEntry
|
public sealed class ProposedEntry
|
||||||
{
|
{
|
||||||
public Entry? Entry { get; set; }
|
public Entry? Entry { get; set; }
|
||||||
public string Reply { get; set; } = string.Empty;
|
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 ulong Index { get; set; }
|
||||||
public List<Entry> Entries { get; set; } = new();
|
public List<Entry> 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.
|
/// The main struct used to sync Raft peers.
|
||||||
/// Mirrors Go <c>appendEntry</c> struct in server/raft.go lines 2557-2568.
|
/// Mirrors Go <c>appendEntry</c> struct in server/raft.go lines 2557-2568.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AppendEntry
|
public sealed class AppendEntry
|
||||||
{
|
{
|
||||||
public string Leader { get; set; } = string.Empty;
|
public string Leader { get; set; } = string.Empty;
|
||||||
public ulong TermV { get; set; }
|
public ulong TermV { get; set; }
|
||||||
@@ -1008,6 +1072,20 @@ internal sealed class AppendEntry
|
|||||||
/// <summary>Subscription the append entry arrived on (object to avoid session dep).</summary>
|
/// <summary>Subscription the append entry arrived on (object to avoid session dep).</summary>
|
||||||
public object? Sub { get; set; }
|
public object? Sub { get; set; }
|
||||||
public byte[]? Buf { 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.
|
/// Response sent by a follower after receiving an append-entry RPC.
|
||||||
/// Mirrors Go <c>appendEntryResponse</c> struct in server/raft.go lines 2760-2766.
|
/// Mirrors Go <c>appendEntryResponse</c> struct in server/raft.go lines 2760-2766.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AppendEntryResponse
|
public sealed class AppendEntryResponse
|
||||||
{
|
{
|
||||||
public ulong TermV { get; set; }
|
public ulong TermV { get; set; }
|
||||||
public ulong Index { get; set; }
|
public ulong Index { get; set; }
|
||||||
public string Peer { get; set; } = string.Empty;
|
public string Peer { get; set; } = string.Empty;
|
||||||
public string Reply { get; set; } = string.Empty;
|
public string Reply { get; set; } = string.Empty;
|
||||||
public bool Success { get; set; }
|
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.
|
/// Encoded peer state attached to snapshots and peer-state entries.
|
||||||
/// Mirrors Go <c>peerState</c> struct in server/raft.go lines 4470-4474.
|
/// Mirrors Go <c>peerState</c> struct in server/raft.go lines 4470-4474.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class PeerState
|
public sealed class PeerState
|
||||||
{
|
{
|
||||||
public List<string> KnownPeers { get; set; } = new();
|
public List<string> KnownPeers { get; set; } = new();
|
||||||
public int ClusterSize { get; set; }
|
public int ClusterSize { get; set; }
|
||||||
@@ -1093,7 +1174,7 @@ internal sealed class PeerState
|
|||||||
/// A Raft vote request sent during leader election.
|
/// A Raft vote request sent during leader election.
|
||||||
/// Mirrors Go <c>voteRequest</c> struct in server/raft.go lines 4549-4556.
|
/// Mirrors Go <c>voteRequest</c> struct in server/raft.go lines 4549-4556.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VoteRequest
|
public sealed class VoteRequest
|
||||||
{
|
{
|
||||||
public ulong TermV { get; set; }
|
public ulong TermV { get; set; }
|
||||||
public ulong LastTerm { get; set; }
|
public ulong LastTerm { get; set; }
|
||||||
@@ -1101,6 +1182,9 @@ internal sealed class VoteRequest
|
|||||||
public string Candidate { get; set; } = string.Empty;
|
public string Candidate { get; set; } = string.Empty;
|
||||||
/// <summary>Internal use — reply subject.</summary>
|
/// <summary>Internal use — reply subject.</summary>
|
||||||
public string Reply { get; set; } = string.Empty;
|
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 <see cref="VoteRequest"/>.
|
/// A response to a <see cref="VoteRequest"/>.
|
||||||
/// Mirrors Go <c>voteResponse</c> struct in server/raft.go lines 4730-4735.
|
/// Mirrors Go <c>voteResponse</c> struct in server/raft.go lines 4730-4735.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class VoteResponse
|
public sealed class VoteResponse
|
||||||
{
|
{
|
||||||
public ulong TermV { get; set; }
|
public ulong TermV { get; set; }
|
||||||
public string Peer { get; set; } = string.Empty;
|
public string Peer { get; set; } = string.Empty;
|
||||||
public bool Granted { get; set; }
|
public bool Granted { get; set; }
|
||||||
/// <summary>Whether this peer's log is empty.</summary>
|
/// <summary>Whether this peer's log is empty.</summary>
|
||||||
public bool Empty { get; set; }
|
public bool Empty { get; set; }
|
||||||
|
|
||||||
|
public byte[] Encode() =>
|
||||||
|
System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,4 +108,98 @@ public sealed class RaftNodeCoreTests
|
|||||||
raft.Peers_.Count.ShouldBe(2);
|
raft.Peers_.Count.ShouldBe(2);
|
||||||
raft.RandCampaignTimeout().ShouldBeGreaterThan(TimeSpan.Zero);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,4 +138,35 @@ public sealed class RaftTypesTests
|
|||||||
checkpoint.Abort();
|
checkpoint.Abort();
|
||||||
File.Exists(checkpoint.SnapFile).ShouldBeFalse();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user