206 lines
6.5 KiB
C#
206 lines
6.5 KiB
C#
// Copyright 2012-2026 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
using Shouldly;
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
|
|
|
public sealed class RaftNodeCoreTests
|
|
{
|
|
[Fact]
|
|
public void SnapshotHelpers_WhenEncodedAndLoaded_ShouldRoundTrip()
|
|
{
|
|
var storeDir = Path.Combine(Path.GetTempPath(), $"raft-node-core-{Guid.NewGuid():N}");
|
|
var raft = new Raft
|
|
{
|
|
StoreDir = storeDir,
|
|
Term_ = 3,
|
|
Applied_ = 9,
|
|
PApplied = 7,
|
|
Wps = [1, 2, 3],
|
|
};
|
|
|
|
try
|
|
{
|
|
var checkpoint = raft.CreateSnapshotCheckpointLocked(force: true);
|
|
checkpoint.ShouldNotBeNull();
|
|
|
|
var snapshot = new Snapshot
|
|
{
|
|
LastTerm = 3,
|
|
LastIndex = 9,
|
|
PeerState = [7, 8],
|
|
Data = [9, 10, 11],
|
|
};
|
|
|
|
raft.InstallSnapshotInternal(snapshot).ShouldBeNull();
|
|
var (loaded, error) = raft.LoadLastSnapshot();
|
|
error.ShouldBeNull();
|
|
loaded.ShouldNotBeNull();
|
|
loaded!.LastTerm.ShouldBe(3UL);
|
|
loaded.LastIndex.ShouldBe(9UL);
|
|
loaded.PeerState.ShouldBe([7, 8]);
|
|
loaded.Data.ShouldBe([9, 10, 11]);
|
|
|
|
raft.SetupLastSnapshot().ShouldBeNull();
|
|
raft.PIndex.ShouldBe(9UL);
|
|
raft.PTerm.ShouldBe(3UL);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(storeDir))
|
|
{
|
|
Directory.Delete(storeDir, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LeadershipHelpers_WhenSteppingDownAndSelectingLeader_ShouldUpdateState()
|
|
{
|
|
var raft = new Raft
|
|
{
|
|
Id = "N1",
|
|
StateValue = (int)RaftState.Candidate,
|
|
Peers_ = new Dictionary<string, Lps>
|
|
{
|
|
["N2"] = new() { Li = 3 },
|
|
["N3"] = new() { Li = 7 },
|
|
},
|
|
};
|
|
|
|
raft.StepdownLocked("N3");
|
|
raft.State().ShouldBe(RaftState.Follower);
|
|
raft.LeaderId.ShouldBe("N3");
|
|
raft.SelectNextLeader().ShouldBe("N3");
|
|
}
|
|
|
|
[Fact]
|
|
public void CampaignHelpers_WhenLeaderOrFollower_ShouldReturnExpectedOutcome()
|
|
{
|
|
var raft = new Raft
|
|
{
|
|
StateValue = (int)RaftState.Leader,
|
|
};
|
|
|
|
raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldNotBeNull();
|
|
raft.XferCampaign().ShouldNotBeNull();
|
|
|
|
raft.StateValue = (int)RaftState.Follower;
|
|
raft.CampaignInternal(TimeSpan.FromMilliseconds(200)).ShouldBeNull();
|
|
raft.State().ShouldBe(RaftState.Candidate);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProgressHelpers_WhenCatchupAndKnownPeersChange_ShouldTrackFlags()
|
|
{
|
|
var raft = new Raft
|
|
{
|
|
Commit = 10,
|
|
Applied_ = 8,
|
|
Catchup = new CatchupState(),
|
|
};
|
|
|
|
raft.IsCatchingUp().ShouldBeTrue();
|
|
raft.IsCurrent(includeForwardProgress: true).ShouldBeFalse();
|
|
raft.Catchup = null;
|
|
raft.UpdateKnownPeersLocked(["N2", "N3"]);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|