refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a dedicated NATS.Server.JetStream.Tests project. This includes root-level JetStream*.cs files, storage test files (FileStore, MemStore, StreamStoreContract), and the full JetStream/ subfolder tree (Api, Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams). Updated all namespaces, added InternalsVisibleTo, registered in the solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
// Go parity: golang/nats-server/server/jetstream_cluster.go:2290-2439
|
||||
// Covers: ProcessAddPeer, ProcessRemovePeer, RemovePeerFromStream, RemapStreamAssignment —
|
||||
// peer-driven stream reassignment in the JetStreamMetaGroup.
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
using NATS.Server.JetStream.Models;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for JetStreamMetaGroup peer management and stream reassignment.
|
||||
/// Go reference: jetstream_cluster.go:2290-2439 (processAddPeer, processRemovePeer,
|
||||
/// removePeerFromStreamLocked, remapStreamAssignment).
|
||||
/// </summary>
|
||||
public class PeerManagementTests
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// ProcessAddPeer — peer registration
|
||||
// Go reference: jetstream_cluster.go:2290 processAddPeer
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_registers_new_peer()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2290 processAddPeer — peer is tracked
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
meta.ProcessAddPeer("peer-1");
|
||||
|
||||
meta.GetKnownPeers().ShouldContain("peer-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_registers_multiple_peers_independently()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2290 — each peer is independently tracked
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
meta.ProcessAddPeer("peer-1");
|
||||
meta.ProcessAddPeer("peer-2");
|
||||
meta.ProcessAddPeer("peer-3");
|
||||
|
||||
var known = meta.GetKnownPeers();
|
||||
known.Count.ShouldBe(3);
|
||||
known.ShouldContain("peer-1");
|
||||
known.ShouldContain("peer-2");
|
||||
known.ShouldContain("peer-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_duplicate_add_is_idempotent()
|
||||
{
|
||||
// AddKnownPeer uses a HashSet so duplicates do not inflate the count.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
meta.ProcessAddPeer("peer-1");
|
||||
meta.ProcessAddPeer("peer-1");
|
||||
|
||||
meta.GetKnownPeers().Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// ProcessAddPeer — under-replication detection
|
||||
// Go reference: jetstream_cluster.go:2311-2339 missingPeers + peer append
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_triggers_rereplication_of_underreplicated_stream()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2315 sa.missingPeers() — adds new peer to group
|
||||
var meta = new JetStreamMetaGroup(3); // leader by default (selfIndex == leaderIndex == 1)
|
||||
|
||||
// Stream assigned with 2 peers but DesiredReplicas == 3 → under-replicated
|
||||
var group = new RaftGroup
|
||||
{
|
||||
Name = "orders-rg",
|
||||
Peers = ["peer-1", "peer-2"],
|
||||
DesiredReplicas = 3,
|
||||
};
|
||||
var sa = new StreamAssignment { StreamName = "ORDERS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessAddPeer("peer-3");
|
||||
|
||||
var updated = meta.GetStreamAssignment("ORDERS")!;
|
||||
updated.Group.Peers.ShouldContain("peer-3");
|
||||
updated.Group.Peers.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_does_not_add_peer_to_fully_replicated_stream()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2315 missingPeers() returns false when at desired count
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup
|
||||
{
|
||||
Name = "events-rg",
|
||||
Peers = ["peer-1", "peer-2", "peer-3"],
|
||||
DesiredReplicas = 3,
|
||||
};
|
||||
var sa = new StreamAssignment { StreamName = "EVENTS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessAddPeer("peer-4");
|
||||
|
||||
var updated = meta.GetStreamAssignment("EVENTS")!;
|
||||
updated.Group.Peers.Count.ShouldBe(3);
|
||||
updated.Group.Peers.ShouldNotContain("peer-4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_does_not_add_peer_already_in_group()
|
||||
{
|
||||
// Peer already a member — should not be added twice.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup
|
||||
{
|
||||
Name = "logs-rg",
|
||||
Peers = ["peer-1"],
|
||||
DesiredReplicas = 2,
|
||||
};
|
||||
var sa = new StreamAssignment { StreamName = "LOGS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessAddPeer("peer-1");
|
||||
|
||||
var updated = meta.GetStreamAssignment("LOGS")!;
|
||||
updated.Group.Peers.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessAddPeer_non_leader_does_not_modify_assignments()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2301 — only leader triggers re-assignment
|
||||
var meta = new JetStreamMetaGroup(3, selfIndex: 2); // not leader
|
||||
|
||||
var group = new RaftGroup
|
||||
{
|
||||
Name = "rg",
|
||||
Peers = ["peer-1"],
|
||||
DesiredReplicas = 3,
|
||||
};
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessAddPeer("peer-2");
|
||||
|
||||
// Peer is registered but stream is not modified since not leader.
|
||||
meta.GetKnownPeers().ShouldContain("peer-2");
|
||||
meta.GetStreamAssignment("S")!.Group.Peers.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// ProcessRemovePeer — stream reassignment
|
||||
// Go reference: jetstream_cluster.go:2342 processRemovePeer
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ProcessRemovePeer_reassigns_streams_away_from_peer()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2385-2392 — streams with removed peer get remapped
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
// Register three peers
|
||||
meta.AddKnownPeer("peer-1");
|
||||
meta.AddKnownPeer("peer-2");
|
||||
meta.AddKnownPeer("peer-3");
|
||||
|
||||
var group = new RaftGroup
|
||||
{
|
||||
Name = "rg",
|
||||
Peers = ["peer-1", "peer-2"],
|
||||
};
|
||||
var sa = new StreamAssignment { StreamName = "ORDERS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessRemovePeer("peer-1");
|
||||
|
||||
var updated = meta.GetStreamAssignment("ORDERS")!;
|
||||
updated.Group.Peers.ShouldNotContain("peer-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessRemovePeer_removes_peer_from_known_peers()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2342 — peer is de-registered
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
meta.AddKnownPeer("peer-1");
|
||||
|
||||
meta.ProcessRemovePeer("peer-1");
|
||||
|
||||
meta.GetKnownPeers().ShouldNotContain("peer-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessRemovePeer_unknown_peer_is_noop()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2342 — no crash when peer not known
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-2", "peer-3"] };
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
// Should not throw
|
||||
meta.ProcessRemovePeer("peer-99");
|
||||
|
||||
// Stream unaffected
|
||||
meta.GetStreamAssignment("S")!.Group.Peers.ShouldContain("peer-2");
|
||||
meta.GetStreamAssignment("S")!.Group.Peers.ShouldContain("peer-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessRemovePeer_non_leader_only_deregisters_peer()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2378 — non-leader skips re-assignment
|
||||
var meta = new JetStreamMetaGroup(3, selfIndex: 2);
|
||||
meta.AddKnownPeer("peer-1");
|
||||
meta.AddKnownPeer("peer-2");
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2"] };
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.ProcessRemovePeer("peer-1");
|
||||
|
||||
// Peer removed from known set
|
||||
meta.GetKnownPeers().ShouldNotContain("peer-1");
|
||||
|
||||
// Stream assignments are NOT modified by a non-leader
|
||||
meta.GetStreamAssignment("S")!.Group.Peers.ShouldContain("peer-1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// RemovePeerFromStream
|
||||
// Go reference: jetstream_cluster.go:2403 removePeerFromStreamLocked
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RemovePeerFromStream_removes_peer_from_group()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:2404 — peer is removed from stream group
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
meta.AddKnownPeer("peer-1");
|
||||
meta.AddKnownPeer("peer-2");
|
||||
meta.AddKnownPeer("peer-3");
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2", "peer-3"] };
|
||||
var sa = new StreamAssignment { StreamName = "EVENTS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
meta.RemovePeerFromStream("EVENTS", "peer-2");
|
||||
|
||||
var updated = meta.GetStreamAssignment("EVENTS")!;
|
||||
updated.Group.Peers.ShouldNotContain("peer-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePeerFromStream_returns_false_for_nonexistent_stream()
|
||||
{
|
||||
// RemovePeerFromStream silently returns false when stream not found.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var result = meta.RemovePeerFromStream("GHOST", "peer-1");
|
||||
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePeerFromStream_returns_false_when_peer_not_in_group()
|
||||
{
|
||||
// Peer not a member of the stream's group.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2"] };
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var result = meta.RemovePeerFromStream("S", "peer-99");
|
||||
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePeerFromStream_replaces_peer_when_replacement_available()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:7088-7094 — replacement peer picked from available pool
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
meta.AddKnownPeer("peer-1");
|
||||
meta.AddKnownPeer("peer-2");
|
||||
meta.AddKnownPeer("peer-3"); // replacement candidate
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2"] };
|
||||
var sa = new StreamAssignment { StreamName = "ORDERS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var result = meta.RemovePeerFromStream("ORDERS", "peer-1");
|
||||
|
||||
result.ShouldBeTrue();
|
||||
var updated = meta.GetStreamAssignment("ORDERS")!;
|
||||
updated.Group.Peers.ShouldNotContain("peer-1");
|
||||
updated.Group.Peers.Count.ShouldBe(2);
|
||||
updated.Group.Peers.ShouldContain("peer-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePeerFromStream_shrinks_group_when_no_replacement_available()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:7102-7110 — R>1 bare removal fallback
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
// Only peer-1 and peer-2 are known; peer-1 is in the group; no replacement
|
||||
meta.AddKnownPeer("peer-1");
|
||||
meta.AddKnownPeer("peer-2");
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2"] };
|
||||
var sa = new StreamAssignment { StreamName = "LOGS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var result = meta.RemovePeerFromStream("LOGS", "peer-1");
|
||||
|
||||
// No replacement found → group shrinks
|
||||
result.ShouldBeFalse();
|
||||
var updated = meta.GetStreamAssignment("LOGS")!;
|
||||
updated.Group.Peers.ShouldNotContain("peer-1");
|
||||
updated.Group.Peers.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// RemapStreamAssignment
|
||||
// Go reference: jetstream_cluster.go:7077 remapStreamAssignment
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void RemapStreamAssignment_selects_new_peers()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:7077 — retain existing minus removed, add candidate
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2", "peer-3"] };
|
||||
var sa = new StreamAssignment { StreamName = "EVENTS", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var available = new List<string> { "peer-1", "peer-2", "peer-3", "peer-4" };
|
||||
var result = meta.RemapStreamAssignment(sa, available, removePeer: "peer-3");
|
||||
|
||||
result.ShouldBeTrue();
|
||||
var updated = meta.GetStreamAssignment("EVENTS")!;
|
||||
updated.Group.Peers.ShouldNotContain("peer-3");
|
||||
updated.Group.Peers.Count.ShouldBe(3);
|
||||
updated.Group.Peers.ShouldContain("peer-4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemapStreamAssignment_retains_existing_peers()
|
||||
{
|
||||
// Retained peers (not removed) remain in the new assignment.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2", "peer-3"] };
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var available = new List<string> { "peer-1", "peer-2", "peer-3", "peer-4" };
|
||||
meta.RemapStreamAssignment(sa, available, removePeer: "peer-1");
|
||||
|
||||
var updated = meta.GetStreamAssignment("S")!;
|
||||
updated.Group.Peers.ShouldContain("peer-2");
|
||||
updated.Group.Peers.ShouldContain("peer-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemapStreamAssignment_returns_false_when_no_replacement()
|
||||
{
|
||||
// Go reference: jetstream_cluster.go:7098-7110 — no placement, R1 returns false
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1"] };
|
||||
var sa = new StreamAssignment { StreamName = "R1", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var available = new List<string> { "peer-1" };
|
||||
var result = meta.RemapStreamAssignment(sa, available, removePeer: "peer-1");
|
||||
|
||||
// Only peer-1 available and it is the removed one → nothing to add
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemapStreamAssignment_empty_available_shrinks_group()
|
||||
{
|
||||
// When the available-peer list is empty, the group simply loses the removed peer.
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
var group = new RaftGroup { Name = "rg", Peers = ["peer-1", "peer-2"] };
|
||||
var sa = new StreamAssignment { StreamName = "S", Group = group };
|
||||
meta.AddStreamAssignment(sa);
|
||||
|
||||
var result = meta.RemapStreamAssignment(sa, [], removePeer: "peer-1");
|
||||
|
||||
result.ShouldBeFalse();
|
||||
meta.GetStreamAssignment("S")!.Group.Peers.ShouldNotContain("peer-1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// AddKnownPeer / RemoveKnownPeer
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void AddKnownPeer_and_RemoveKnownPeer_are_consistent()
|
||||
{
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
|
||||
meta.AddKnownPeer("p1");
|
||||
meta.AddKnownPeer("p2");
|
||||
meta.RemoveKnownPeer("p1");
|
||||
|
||||
var known = meta.GetKnownPeers();
|
||||
known.ShouldNotContain("p1");
|
||||
known.ShouldContain("p2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveKnownPeer_unknown_peer_is_noop()
|
||||
{
|
||||
var meta = new JetStreamMetaGroup(3);
|
||||
meta.AddKnownPeer("p1");
|
||||
|
||||
// Should not throw
|
||||
meta.RemoveKnownPeer("p99");
|
||||
|
||||
meta.GetKnownPeers().Count.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user