Merge branch 'codex/sublist-allocation-reduction'
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using System.Diagnostics;
|
||||
using NATS.Server.Benchmark.Tests.Harness;
|
||||
using NATS.Server.Subscriptions;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace NATS.Server.Benchmark.Tests.CorePubSub;
|
||||
|
||||
public class SubListMatchBenchmarks(ITestOutputHelper output)
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", "Benchmark")]
|
||||
public void SubListExactMatch_128Subjects()
|
||||
{
|
||||
using var subList = new SubList();
|
||||
for (var i = 0; i < 128; i++)
|
||||
subList.Insert(new Subscription { Subject = $"bench.exact.{i}", Sid = i.ToString() });
|
||||
|
||||
var (result, allocatedBytes) = Measure("SubList Exact Match (128 subjects)", "DotNet", "bench.exact.64".Length, 250_000, () =>
|
||||
{
|
||||
_ = subList.Match("bench.exact.64");
|
||||
});
|
||||
|
||||
BenchmarkResultWriter.WriteSingle(output, result);
|
||||
WriteAllocationSummary(allocatedBytes, result.TotalMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Benchmark")]
|
||||
public void SubListWildcardMatch_FanIn()
|
||||
{
|
||||
using var subList = new SubList();
|
||||
subList.Insert(new Subscription { Subject = "orders.created", Sid = "1" });
|
||||
subList.Insert(new Subscription { Subject = "orders.*", Sid = "2" });
|
||||
subList.Insert(new Subscription { Subject = "orders.>", Sid = "3" });
|
||||
subList.Insert(new Subscription { Subject = "orders.created.us", Sid = "4" });
|
||||
|
||||
var (result, allocatedBytes) = Measure("SubList Wildcard Match", "DotNet", "orders.created".Length, 250_000, () =>
|
||||
{
|
||||
_ = subList.Match("orders.created");
|
||||
});
|
||||
|
||||
BenchmarkResultWriter.WriteSingle(output, result);
|
||||
WriteAllocationSummary(allocatedBytes, result.TotalMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Benchmark")]
|
||||
public void SubListQueueMatch_MergedGroups()
|
||||
{
|
||||
using var subList = new SubList();
|
||||
subList.Insert(new Subscription { Subject = "jobs.run", Queue = "workers", Sid = "1" });
|
||||
subList.Insert(new Subscription { Subject = "jobs.*", Queue = "workers", Sid = "2" });
|
||||
subList.Insert(new Subscription { Subject = "jobs.>", Queue = "audit", Sid = "3" });
|
||||
|
||||
var (result, allocatedBytes) = Measure("SubList Queue Match", "DotNet", "jobs.run".Length, 250_000, () =>
|
||||
{
|
||||
_ = subList.Match("jobs.run");
|
||||
});
|
||||
|
||||
BenchmarkResultWriter.WriteSingle(output, result);
|
||||
WriteAllocationSummary(allocatedBytes, result.TotalMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Benchmark")]
|
||||
public void SubListRemoteInterest_WildcardLookup()
|
||||
{
|
||||
using var subList = new SubList();
|
||||
for (var i = 0; i < 64; i++)
|
||||
subList.ApplyRemoteSub(new RemoteSubscription($"remote.{i}.*", null, $"r{i}", "A"));
|
||||
|
||||
var (result, allocatedBytes) = Measure("SubList Remote Interest", "DotNet", "remote.42.created".Length, 250_000, () =>
|
||||
{
|
||||
_ = subList.HasRemoteInterest("A", "remote.42.created");
|
||||
});
|
||||
|
||||
BenchmarkResultWriter.WriteSingle(output, result);
|
||||
WriteAllocationSummary(allocatedBytes, result.TotalMessages);
|
||||
}
|
||||
|
||||
private static (BenchmarkResult Result, long AllocatedBytes) Measure(string name, string serverType, int bytesPerOperation, int iterations, Action operation)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
|
||||
for (var i = 0; i < 1_000; i++)
|
||||
operation();
|
||||
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
operation();
|
||||
sw.Stop();
|
||||
var allocatedBytes = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
return (new BenchmarkResult
|
||||
{
|
||||
Name = name,
|
||||
ServerType = serverType,
|
||||
TotalMessages = iterations,
|
||||
TotalBytes = (long)iterations * bytesPerOperation,
|
||||
Duration = sw.Elapsed,
|
||||
}, allocatedBytes);
|
||||
}
|
||||
|
||||
private void WriteAllocationSummary(long allocatedBytes, long iterations)
|
||||
{
|
||||
output.WriteLine($"Allocated: {allocatedBytes:N0} B total | {allocatedBytes / (double)iterations:F2} B/op");
|
||||
output.WriteLine("");
|
||||
}
|
||||
}
|
||||
@@ -10,21 +10,14 @@ namespace NATS.Server.Clustering.Tests.Routes;
|
||||
public class RouteRemoteSubCleanupParityBatch2Tests
|
||||
{
|
||||
[Fact]
|
||||
public void Routed_sub_key_helpers_parse_account_and_queue_fields()
|
||||
public void Routed_sub_key_exposes_route_account_subject_and_queue_fields()
|
||||
{
|
||||
var key = SubList.BuildRoutedSubKey("R1", "A", "orders.*", "q1");
|
||||
var key = RoutedSubKey.FromRemoteSubscription(new RemoteSubscription("orders.*", "q1", "R1", "A"));
|
||||
|
||||
SubList.GetAccNameFromRoutedSubKey(key).ShouldBe("A");
|
||||
|
||||
var info = SubList.GetRoutedSubKeyInfo(key);
|
||||
info.ShouldNotBeNull();
|
||||
info.Value.RouteId.ShouldBe("R1");
|
||||
info.Value.Account.ShouldBe("A");
|
||||
info.Value.Subject.ShouldBe("orders.*");
|
||||
info.Value.Queue.ShouldBe("q1");
|
||||
|
||||
SubList.GetRoutedSubKeyInfo("invalid").ShouldBeNull();
|
||||
SubList.GetAccNameFromRoutedSubKey("invalid").ShouldBeNull();
|
||||
key.RouteId.ShouldBe("R1");
|
||||
key.Account.ShouldBe("A");
|
||||
key.Subject.ShouldBe("orders.*");
|
||||
key.Queue.ShouldBe("q1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -47,6 +40,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<InterestChange>();
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -208,6 +208,50 @@ public class RouteSubscriptionTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Removing_one_subject_keeps_other_remote_interest_intact()
|
||||
{
|
||||
var cluster = Guid.NewGuid().ToString("N");
|
||||
var a = await StartServerAsync(MakeClusterOpts(cluster));
|
||||
var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));
|
||||
|
||||
try
|
||||
{
|
||||
await WaitForRouteFormation(a.Server, b.Server);
|
||||
|
||||
await using var nc = new NatsConnection(new NatsOpts
|
||||
{
|
||||
Url = $"nats://127.0.0.1:{a.Server.Port}",
|
||||
});
|
||||
await nc.ConnectAsync();
|
||||
|
||||
await using var sub1 = await nc.SubscribeCoreAsync<string>("multi.one");
|
||||
await using var sub2 = await nc.SubscribeCoreAsync<string>("multi.two");
|
||||
await nc.PingAsync();
|
||||
|
||||
await WaitForCondition(() => b.Server.HasRemoteInterest("multi.one") && b.Server.HasRemoteInterest("multi.two"));
|
||||
b.Server.HasRemoteInterest("multi.one").ShouldBeTrue();
|
||||
b.Server.HasRemoteInterest("multi.two").ShouldBeTrue();
|
||||
|
||||
await sub1.DisposeAsync();
|
||||
await nc.PingAsync();
|
||||
|
||||
await WaitForCondition(() => !b.Server.HasRemoteInterest("multi.one"));
|
||||
b.Server.HasRemoteInterest("multi.one").ShouldBeFalse();
|
||||
b.Server.HasRemoteInterest("multi.two").ShouldBeTrue();
|
||||
|
||||
await sub2.DisposeAsync();
|
||||
await nc.PingAsync();
|
||||
|
||||
await WaitForCondition(() => !b.Server.HasRemoteInterest("multi.two"));
|
||||
b.Server.HasRemoteInterest("multi.two").ShouldBeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeServers(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
// Go: RS+ wire protocol parsing (low-level)
|
||||
[Fact]
|
||||
public async Task RSplus_frame_registers_remote_interest_via_wire()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Match_merges_queue_groups_from_multiple_matching_nodes_by_queue_name()
|
||||
{
|
||||
using var sl = new SubList();
|
||||
sl.Insert(new Subscription { Subject = "orders.created", Queue = "workers", Sid = "1" });
|
||||
sl.Insert(new Subscription { Subject = "orders.*", Queue = "workers", Sid = "2" });
|
||||
sl.Insert(new Subscription { Subject = "orders.>", Queue = "audit", Sid = "3" });
|
||||
|
||||
var result = sl.Match("orders.created");
|
||||
|
||||
result.PlainSubs.ShouldBeEmpty();
|
||||
result.QueueSubs.Length.ShouldBe(2);
|
||||
result.QueueSubs.Single(group => group[0].Queue == "workers").Length.ShouldBe(2);
|
||||
result.QueueSubs.Single(group => group[0].Queue == "audit").Length.ShouldBe(1);
|
||||
}
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty result is a shared singleton — two calls that yield no matches return
|
||||
/// the same object reference.
|
||||
|
||||
Reference in New Issue
Block a user