diff --git a/tests/NATS.Server.Clustering.Tests/Routes/RouteRemoteSubCleanupParityBatch2Tests.cs b/tests/NATS.Server.Clustering.Tests/Routes/RouteRemoteSubCleanupParityBatch2Tests.cs index 684a69c..f363389 100644 --- a/tests/NATS.Server.Clustering.Tests/Routes/RouteRemoteSubCleanupParityBatch2Tests.cs +++ b/tests/NATS.Server.Clustering.Tests/Routes/RouteRemoteSubCleanupParityBatch2Tests.cs @@ -47,6 +47,23 @@ public class RouteRemoteSubCleanupParityBatch2Tests sl.HasRemoteInterest("B", "orders.created").ShouldBeTrue(); } + [Fact] + public void Applying_same_remote_subscription_twice_is_idempotent_for_interest_tracking() + { + using var sl = new SubList(); + var changes = new List(); + sl.InterestChanged += changes.Add; + + var sub = new RemoteSubscription("orders.*", "workers", "r1", "A"); + + sl.ApplyRemoteSub(sub); + sl.ApplyRemoteSub(sub); + + sl.HasRemoteInterest("A", "orders.created").ShouldBeTrue(); + sl.MatchRemote("A", "orders.created").Count.ShouldBe(1); + changes.Count(change => change.Kind == InterestChangeKind.RemoteAdded).ShouldBe(1); + } + [Fact] public async Task Route_disconnect_cleans_remote_interest_without_explicit_rs_minus() { diff --git a/tests/NATS.Server.Core.Tests/Subscriptions/SubListAllocationGuardTests.cs b/tests/NATS.Server.Core.Tests/Subscriptions/SubListAllocationGuardTests.cs new file mode 100644 index 0000000..b48277c --- /dev/null +++ b/tests/NATS.Server.Core.Tests/Subscriptions/SubListAllocationGuardTests.cs @@ -0,0 +1,48 @@ +using System.Reflection; +using NATS.Server.Subscriptions; + +namespace NATS.Server.Core.Tests; + +public class SubListAllocationGuardTests +{ + [Fact] + public void Remote_subscription_dictionary_uses_dedicated_routed_sub_key_type() + { + var field = typeof(SubList).GetField("_remoteSubs", BindingFlags.Instance | BindingFlags.NonPublic); + + field.ShouldNotBeNull(); + field.FieldType.IsGenericType.ShouldBeTrue(); + field.FieldType.GetGenericArguments()[0].Name.ShouldBe("RoutedSubKey"); + } + + [Fact] + public void Has_remote_interest_supports_exact_and_wildcard_subjects_per_account() + { + using var sl = new SubList(); + sl.ApplyRemoteSub(new RemoteSubscription("orders.created", null, "r1", "A")); + sl.ApplyRemoteSub(new RemoteSubscription("orders.*", null, "r2", "A")); + sl.ApplyRemoteSub(new RemoteSubscription("payments.*", null, "r3", "B")); + + sl.HasRemoteInterest("A", "orders.created").ShouldBeTrue(); + sl.HasRemoteInterest("A", "orders.updated").ShouldBeTrue(); + sl.HasRemoteInterest("A", "payments.created").ShouldBeFalse(); + sl.HasRemoteInterest("B", "payments.posted").ShouldBeTrue(); + sl.HasRemoteInterest("B", "orders.created").ShouldBeFalse(); + } + + [Fact] + public void Match_remote_reflects_queue_weight_updates_for_existing_remote_queue_sub() + { + using var sl = new SubList(); + var sub = new RemoteSubscription("orders.*", "workers", "r1", "A", QueueWeight: 1); + sl.ApplyRemoteSub(sub); + + sl.MatchRemote("A", "orders.created").Count.ShouldBe(1); + + sl.UpdateRemoteQSub(sub with { QueueWeight = 4 }); + + var matches = sl.MatchRemote("A", "orders.created"); + matches.Count.ShouldBe(4); + matches.ShouldAllBe(match => match.Queue == "workers"); + } +} diff --git a/tests/NATS.Server.Core.Tests/Subscriptions/SubListGoParityTests.cs b/tests/NATS.Server.Core.Tests/Subscriptions/SubListGoParityTests.cs index ff8ccdf..022ad6a 100644 --- a/tests/NATS.Server.Core.Tests/Subscriptions/SubListGoParityTests.cs +++ b/tests/NATS.Server.Core.Tests/Subscriptions/SubListGoParityTests.cs @@ -199,6 +199,33 @@ public class SubListGoParityTests sl.Match("foo.bar").PlainSubs.Length.ShouldBe(3); } + [Fact] + public void Cache_generation_bump_rebuilds_match_result_after_insert_and_remove() + { + var sl = new SubList(); + var exact = MakeSub("foo.bar", sid: "1"); + var wildcard = MakeSub("foo.*", sid: "2"); + + sl.Insert(exact); + + var first = sl.Match("foo.bar"); + var second = sl.Match("foo.bar"); + ReferenceEquals(first, second).ShouldBeTrue(); + first.PlainSubs.Select(sub => sub.Sid).ShouldBe(["1"]); + + sl.Insert(wildcard); + + var afterInsert = sl.Match("foo.bar"); + ReferenceEquals(afterInsert, first).ShouldBeFalse(); + afterInsert.PlainSubs.Select(sub => sub.Sid).OrderBy(x => x).ToArray().ShouldBe(["1", "2"]); + + sl.Remove(wildcard); + + var afterRemove = sl.Match("foo.bar"); + ReferenceEquals(afterRemove, afterInsert).ShouldBeFalse(); + afterRemove.PlainSubs.Select(sub => sub.Sid).ShouldBe(["1"]); + } + /// /// Empty result is a shared singleton — two calls that yield no matches return /// the same object reference.