// 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; /// /// Tests for PlacementEngine topology-aware peer selection. /// Go reference: jetstream_cluster.go:7212 selectPeerGroup. /// 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( () => PlacementEngine.SelectPeerGroup("fail", 5, peers)); } [Fact] public void Zero_peers_with_replicas_throws() { var group = Should.Throw( () => 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 { 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 { 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 { new() { PeerId = "p1", Cluster = "us-east" }, new() { PeerId = "p2", Cluster = "us-west" }, }; var policy = new PlacementPolicy { Cluster = "us-east" }; Should.Throw( () => 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 { 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 { 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 { 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 { new() { PeerId = "p1", Available = false }, new() { PeerId = "p2", Available = false }, }; Should.Throw( () => 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 { 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 { 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 { 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 CreatePeers(int count) { return Enumerable.Range(1, count) .Select(i => new PeerInfo { PeerId = $"peer-{i}", Available = true, AvailableStorage = long.MaxValue - i, }) .ToList(); } }