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,309 @@
|
||||
// Go parity: golang/nats-server/server/jetstream_cluster.go:7212 selectPeerGroup
|
||||
// Covers: PlacementEngine peer selection with cluster affinity, tag filtering,
|
||||
// exclude-tag filtering, unavailable peer exclusion, storage-based ordering,
|
||||
// single replica selection, and combined policy filtering.
|
||||
using NATS.Server.JetStream.Cluster;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream.Cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for PlacementEngine topology-aware peer selection.
|
||||
/// Go reference: jetstream_cluster.go:7212 selectPeerGroup.
|
||||
/// </summary>
|
||||
public class PlacementEngineTests
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// Basic selection with enough peers
|
||||
// Go reference: jetstream_cluster.go selectPeerGroup base case
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Basic_selection_with_enough_peers()
|
||||
{
|
||||
var peers = CreatePeers(5);
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("test-group", 3, peers);
|
||||
|
||||
group.Name.ShouldBe("test-group");
|
||||
group.Peers.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Selection_returns_exact_replica_count()
|
||||
{
|
||||
var peers = CreatePeers(10);
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("exact", 5, peers);
|
||||
|
||||
group.Peers.Count.ShouldBe(5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Insufficient peers throws
|
||||
// Go reference: jetstream_cluster.go not enough peers error
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Insufficient_peers_throws()
|
||||
{
|
||||
var peers = CreatePeers(2);
|
||||
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => PlacementEngine.SelectPeerGroup("fail", 5, peers));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zero_peers_with_replicas_throws()
|
||||
{
|
||||
var group = Should.Throw<InvalidOperationException>(
|
||||
() => PlacementEngine.SelectPeerGroup("empty", 1, []));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Cluster affinity filtering
|
||||
// Go reference: jetstream_cluster.go cluster affinity in placement
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Cluster_affinity_selects_only_matching_cluster()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Cluster = "us-east" },
|
||||
new() { PeerId = "p2", Cluster = "us-west" },
|
||||
new() { PeerId = "p3", Cluster = "us-east" },
|
||||
new() { PeerId = "p4", Cluster = "us-east" },
|
||||
new() { PeerId = "p5", Cluster = "eu-west" },
|
||||
};
|
||||
var policy = new PlacementPolicy { Cluster = "us-east" };
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("cluster", 3, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(3);
|
||||
group.Peers.ShouldAllBe(id => id.StartsWith("p1") || id.StartsWith("p3") || id.StartsWith("p4"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cluster_affinity_is_case_insensitive()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Cluster = "US-East" },
|
||||
new() { PeerId = "p2", Cluster = "us-east" },
|
||||
};
|
||||
var policy = new PlacementPolicy { Cluster = "us-east" };
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("ci", 2, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cluster_affinity_with_insufficient_matching_throws()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Cluster = "us-east" },
|
||||
new() { PeerId = "p2", Cluster = "us-west" },
|
||||
};
|
||||
var policy = new PlacementPolicy { Cluster = "us-east" };
|
||||
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => PlacementEngine.SelectPeerGroup("fail", 2, peers, policy));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Tag filtering (include and exclude)
|
||||
// Go reference: jetstream_cluster.go tag-based filtering
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Tag_filtering_selects_peers_with_all_required_tags()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Tags = ["ssd", "fast"] },
|
||||
new() { PeerId = "p2", Tags = ["ssd"] },
|
||||
new() { PeerId = "p3", Tags = ["ssd", "fast", "gpu"] },
|
||||
new() { PeerId = "p4", Tags = ["hdd"] },
|
||||
};
|
||||
var policy = new PlacementPolicy { Tags = ["ssd", "fast"] };
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("tags", 2, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
group.Peers.ShouldContain("p1");
|
||||
group.Peers.ShouldContain("p3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exclude_tag_filtering_removes_peers_with_excluded_tags()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Tags = ["ssd"] },
|
||||
new() { PeerId = "p2", Tags = ["ssd", "deprecated"] },
|
||||
new() { PeerId = "p3", Tags = ["ssd"] },
|
||||
};
|
||||
var policy = new PlacementPolicy { ExcludeTags = ["deprecated"] };
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("excl", 2, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
group.Peers.ShouldNotContain("p2");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Unavailable peers excluded
|
||||
// Go reference: jetstream_cluster.go offline peer filter
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Unavailable_peers_are_excluded()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Available = true },
|
||||
new() { PeerId = "p2", Available = false },
|
||||
new() { PeerId = "p3", Available = true },
|
||||
new() { PeerId = "p4", Available = false },
|
||||
};
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("avail", 2, peers);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
group.Peers.ShouldContain("p1");
|
||||
group.Peers.ShouldContain("p3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_unavailable_throws()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Available = false },
|
||||
new() { PeerId = "p2", Available = false },
|
||||
};
|
||||
|
||||
Should.Throw<InvalidOperationException>(
|
||||
() => PlacementEngine.SelectPeerGroup("fail", 1, peers));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Peers ordered by available storage
|
||||
// Go reference: jetstream_cluster.go storage-based ordering
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Peers_ordered_by_available_storage_descending()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "low", AvailableStorage = 100 },
|
||||
new() { PeerId = "high", AvailableStorage = 10000 },
|
||||
new() { PeerId = "mid", AvailableStorage = 5000 },
|
||||
};
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("storage", 2, peers);
|
||||
|
||||
// Should pick high and mid (top 2 by storage)
|
||||
group.Peers[0].ShouldBe("high");
|
||||
group.Peers[1].ShouldBe("mid");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Single replica selection
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Single_replica_selection()
|
||||
{
|
||||
var peers = CreatePeers(5);
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("single", 1, peers);
|
||||
|
||||
group.Peers.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Policy with all filters combined
|
||||
// Go reference: jetstream_cluster.go combined placement policy
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Combined_policy_filters_applied_together()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Cluster = "us-east", Tags = ["ssd"], Available = true, AvailableStorage = 5000 },
|
||||
new() { PeerId = "p2", Cluster = "us-east", Tags = ["ssd", "old"], Available = true, AvailableStorage = 8000 },
|
||||
new() { PeerId = "p3", Cluster = "us-west", Tags = ["ssd"], Available = true, AvailableStorage = 9000 },
|
||||
new() { PeerId = "p4", Cluster = "us-east", Tags = ["ssd"], Available = false, AvailableStorage = 10000 },
|
||||
new() { PeerId = "p5", Cluster = "us-east", Tags = ["ssd"], Available = true, AvailableStorage = 7000 },
|
||||
new() { PeerId = "p6", Cluster = "us-east", Tags = ["hdd"], Available = true, AvailableStorage = 12000 },
|
||||
};
|
||||
var policy = new PlacementPolicy
|
||||
{
|
||||
Cluster = "us-east",
|
||||
Tags = ["ssd"],
|
||||
ExcludeTags = ["old"],
|
||||
};
|
||||
|
||||
// After filtering: p1 (5000), p5 (7000) — p2 excluded (old tag), p3 (wrong cluster), p4 (unavailable), p6 (no ssd tag)
|
||||
var group = PlacementEngine.SelectPeerGroup("combined", 2, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
// Ordered by storage descending: p5 (7000) first, p1 (5000) second
|
||||
group.Peers[0].ShouldBe("p5");
|
||||
group.Peers[1].ShouldBe("p1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Null policy is allowed (no filtering)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Null_policy_selects_without_filtering()
|
||||
{
|
||||
var peers = CreatePeers(3);
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("nofilter", 3, peers, policy: null);
|
||||
|
||||
group.Peers.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Empty policy fields are ignored
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Empty_policy_cluster_is_ignored()
|
||||
{
|
||||
var peers = new List<PeerInfo>
|
||||
{
|
||||
new() { PeerId = "p1", Cluster = "us-east" },
|
||||
new() { PeerId = "p2", Cluster = "us-west" },
|
||||
};
|
||||
var policy = new PlacementPolicy { Cluster = "" };
|
||||
|
||||
var group = PlacementEngine.SelectPeerGroup("empty-cluster", 2, peers, policy);
|
||||
|
||||
group.Peers.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private static List<PeerInfo> CreatePeers(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => new PeerInfo
|
||||
{
|
||||
PeerId = $"peer-{i}",
|
||||
Available = true,
|
||||
AvailableStorage = long.MaxValue - i,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user