refactor: rename remaining tests to NATS.Server.Core.Tests

- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests
- Update solution file, InternalsVisibleTo, and csproj references
- Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests)
- Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.*
- Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls
- Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
This commit is contained in:
Joseph Doherty
2026-03-12 16:14:02 -04:00
parent 78b4bc2486
commit 7fbffffd05
114 changed files with 576 additions and 1121 deletions

View File

@@ -0,0 +1,670 @@
// Go parity: golang/nats-server/server/norace_2_test.go
// Covers: concurrent stream creation, parallel publish to clustered streams,
// concurrent consumer creation and fetch, leader stepdown under load,
// create-delete-recreate cycles, mixed concurrent operations, and large
// batch fetch under concurrent publish — all using ClusterFixture.
using System.Collections.Concurrent;
using System.Text;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Api;
using NATS.Server.JetStream.Cluster;
using NATS.Server.JetStream.Consumers;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Publish;
using ClusterFixture = NATS.Server.TestUtilities.JetStreamClusterFixture;
using NATS.Server.TestUtilities;
namespace NATS.Server.Core.Tests.Stress;
/// <summary>
/// Stress tests for clustered JetStream operations under concurrency.
/// Uses JetStreamClusterFixture (in-process meta-group) to simulate cluster behaviour
/// consistent with how Tasks 6-10 are tested.
///
/// Go ref: norace_2_test.go — cluster stress tests.
/// </summary>
public class ClusterStressTests
{
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster100ConcurrentStreamCreates norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_100_concurrent_stream_creates_all_succeed()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
const int count = 100;
var errors = new ConcurrentBag<Exception>();
var created = new ConcurrentBag<string>();
await Parallel.ForEachAsync(Enumerable.Range(0, count), async (i, _) =>
{
try
{
var resp = await fx.CreateStreamAsync(
$"CONCS{i}",
[$"concs{i}.>"],
1);
if (resp.Error is null)
created.Add($"CONCS{i}");
}
catch (Exception ex) { errors.Add(ex); }
await Task.CompletedTask;
});
errors.ShouldBeEmpty();
created.Count.ShouldBe(count);
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster50ConcurrentPublishes norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_50_concurrent_publishes_to_same_stream_all_stored()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("CONCPUB", ["concpub.>"], 1);
const int publishes = 50;
var sequences = new ConcurrentBag<ulong>();
var errors = new ConcurrentBag<Exception>();
// Publish must be sequential because the in-process store serialises writes.
// The concurrency in Go's norace tests comes from multiple goroutines being
// scheduled — here we verify the sequential publish path is correct.
for (var i = 0; i < publishes; i++)
{
try
{
var ack = await fx.PublishAsync($"concpub.event.{i}", $"payload-{i}");
sequences.Add(ack.Seq);
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
sequences.Count.ShouldBe(publishes);
var state = await fx.GetStreamStateAsync("CONCPUB");
state.Messages.ShouldBe((ulong)publishes);
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster20StreamsConcurrentPublish norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_20_streams_with_concurrent_publish_each_stores_correct_count()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
const int streamCount = 20;
const int msgsPerStream = 10;
for (var i = 0; i < streamCount; i++)
await fx.CreateStreamAsync($"MULTI{i}", [$"multi{i}.>"], 1);
var errors = new ConcurrentBag<Exception>();
// Independent streams publish in parallel — each has its own store.
await Parallel.ForEachAsync(Enumerable.Range(0, streamCount), async (i, _) =>
{
try
{
for (var j = 0; j < msgsPerStream; j++)
await fx.PublishAsync($"multi{i}.event", $"msg-{i}-{j}");
}
catch (Exception ex) { errors.Add(ex); }
await Task.CompletedTask;
});
errors.ShouldBeEmpty();
for (var i = 0; i < streamCount; i++)
{
var state = await fx.GetStreamStateAsync($"MULTI{i}");
state.Messages.ShouldBe((ulong)msgsPerStream,
$"stream MULTI{i} should have {msgsPerStream} messages");
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterLeaderStepdownConcurrentPublish norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_leader_stepdown_during_concurrent_publishes_does_not_lose_data()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("STEPUB", ["stepub.>"], 3);
const int publishCount = 20;
var errors = new ConcurrentBag<Exception>();
for (var i = 0; i < publishCount; i++)
{
try
{
if (i == 5)
await fx.StepDownStreamLeaderAsync("STEPUB");
await fx.PublishAsync($"stepub.event.{i}", $"msg-{i}");
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
var state = await fx.GetStreamStateAsync("STEPUB");
state.Messages.ShouldBe((ulong)publishCount);
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster100ConcurrentConsumerCreates norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_100_concurrent_consumer_creates_all_succeed()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("CONCON", ["concon.>"], 1);
const int count = 100;
var errors = new ConcurrentBag<Exception>();
await Parallel.ForEachAsync(Enumerable.Range(0, count), async (i, _) =>
{
try
{
await fx.CreateConsumerAsync("CONCON", $"consumer{i}");
}
catch (Exception ex) { errors.Add(ex); }
await Task.CompletedTask;
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster50ConcurrentFetches norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_50_sequential_fetches_on_same_consumer_all_succeed()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("CONFETCH", ["confetch.>"], 1);
await fx.CreateConsumerAsync("CONFETCH", "fetcher");
for (var i = 0; i < 100; i++)
await fx.PublishAsync("confetch.event", $"msg-{i}");
var errors = new ConcurrentBag<Exception>();
for (var i = 0; i < 50; i++)
{
try
{
var batch = await fx.FetchAsync("CONFETCH", "fetcher", 1);
batch.ShouldNotBeNull();
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterPublishFetchInterleave norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_concurrent_publish_and_fetch_interleaving_delivers_all_messages()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("INTERLEAVE", ["inter.>"], 1);
await fx.CreateConsumerAsync("INTERLEAVE", "reader");
const int rounds = 10;
const int msgsPerRound = 5;
var errors = new ConcurrentBag<Exception>();
var totalFetched = 0;
for (var r = 0; r < rounds; r++)
{
try
{
for (var m = 0; m < msgsPerRound; m++)
await fx.PublishAsync("inter.event", $"round-{r}-msg-{m}");
var batch = await fx.FetchAsync("INTERLEAVE", "reader", msgsPerRound);
Interlocked.Add(ref totalFetched, batch.Messages.Count);
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
totalFetched.ShouldBe(rounds * msgsPerRound);
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterMetaStepdownDuringStreamCreate norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void Cluster_meta_stepdown_during_stream_creation_does_not_corrupt_state()
{
var meta = new JetStreamMetaGroup(5);
var consumerManager = new ConsumerManager(meta);
var streamManager = new StreamManager(meta, consumerManager: consumerManager);
var errors = new ConcurrentBag<Exception>();
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < 30; i++)
{
streamManager.CreateOrUpdate(new StreamConfig
{
Name = $"METACD{i}",
Subjects = [$"mcd{i}.>"],
Replicas = 1,
});
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 5; i++)
{
meta.StepDown();
Thread.Sleep(2);
}
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamCluster10ConcurrentStreamDeletes norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_10_concurrent_stream_deletes_complete_without_error()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
const int count = 10;
for (var i = 0; i < count; i++)
await fx.CreateStreamAsync($"DEL{i}", [$"del{i}.>"], 1);
var errors = new ConcurrentBag<Exception>();
await Parallel.ForEachAsync(Enumerable.Range(0, count), async (i, _) =>
{
try
{
var resp = await fx.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DEL{i}", "{}");
resp.ShouldNotBeNull();
}
catch (Exception ex) { errors.Add(ex); }
await Task.CompletedTask;
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterConcurrentAckAll norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_concurrent_ackall_operations_advance_consumer_correctly()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("ACKALL", ["ackall.>"], 1);
await fx.CreateConsumerAsync("ACKALL", "acker", ackPolicy: AckPolicy.All);
const int msgCount = 50;
for (var i = 0; i < msgCount; i++)
await fx.PublishAsync("ackall.event", $"msg-{i}");
var errors = new ConcurrentBag<Exception>();
for (ulong seq = 1; seq <= msgCount; seq += 5)
{
try
{
fx.AckAll("ACKALL", "acker", seq);
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterMultiConsumerConcurrentFetch norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_multiple_consumers_each_see_all_messages_independently()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("MULTICONSUMER", ["mc.>"], 1);
const int consumers = 5;
const int msgCount = 10;
for (var c = 0; c < consumers; c++)
await fx.CreateConsumerAsync("MULTICONSUMER", $"reader{c}");
for (var i = 0; i < msgCount; i++)
await fx.PublishAsync("mc.event", $"msg-{i}");
var errors = new ConcurrentBag<Exception>();
await Parallel.ForEachAsync(Enumerable.Range(0, consumers), async (c, _) =>
{
try
{
var batch = await fx.FetchAsync("MULTICONSUMER", $"reader{c}", msgCount);
batch.Messages.Count.ShouldBe(msgCount,
$"consumer reader{c} should see all {msgCount} messages");
}
catch (Exception ex) { errors.Add(ex); }
await Task.CompletedTask;
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterRapidCreateDeleteRecreate norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_rapid_create_delete_recreate_cycle_50_iterations_correct()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
const int iterations = 50;
var errors = new ConcurrentBag<Exception>();
for (var i = 0; i < iterations; i++)
{
try
{
var createResp = await fx.CreateStreamAsync("RECYCLE", ["recycle.>"], 1);
if (createResp.Error is null)
{
await fx.PublishAsync("recycle.event", $"msg-{i}");
await fx.RequestAsync($"{JetStreamApiSubjects.StreamDelete}RECYCLE", "{}");
}
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterMixedConcurrentOperations norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_mixed_create_publish_fetch_delete_concurrently_does_not_corrupt()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("MIXEDBASE", ["mixed.>"], 1);
await fx.CreateConsumerAsync("MIXEDBASE", "mixedreader");
const int opsPerTask = 20;
var errors = new ConcurrentBag<Exception>();
await Task.WhenAll(
Task.Run(async () =>
{
try
{
for (var i = 0; i < opsPerTask; i++)
await fx.CreateStreamAsync($"MXNEW{i}", [$"mxnew{i}.>"], 1);
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < opsPerTask; i++)
await fx.PublishAsync("mixed.event", $"msg-{i}");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < opsPerTask; i++)
_ = await fx.FetchAsync("MIXEDBASE", "mixedreader", 1);
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < opsPerTask; i++)
_ = await fx.GetStreamInfoAsync("MIXEDBASE");
}
catch (Exception ex) { errors.Add(ex); }
}));
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterConcurrentStreamInfo norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_concurrent_stream_info_queries_during_publishes_are_safe()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("INFOLOAD", ["infoload.>"], 1);
const int ops = 50;
var errors = new ConcurrentBag<Exception>();
await Task.WhenAll(
Task.Run(async () =>
{
try
{
for (var i = 0; i < ops; i++)
await fx.PublishAsync("infoload.event", $"msg-{i}");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < ops * 2; i++)
_ = await fx.GetStreamInfoAsync("INFOLOAD");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < ops * 2; i++)
_ = await fx.GetStreamStateAsync("INFOLOAD");
}
catch (Exception ex) { errors.Add(ex); }
}));
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterLargeBatchFetch norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_large_batch_fetch_500_messages_under_concurrent_publish()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("LARGEBATCH", ["lb.>"], 1);
await fx.CreateConsumerAsync("LARGEBATCH", "batchreader");
const int totalMsgs = 500;
for (var i = 0; i < totalMsgs; i++)
await fx.PublishAsync("lb.event", $"payload-{i}");
var errors = new ConcurrentBag<Exception>();
var fetchedCount = 0;
await Task.WhenAll(
Task.Run(async () =>
{
try
{
var batch = await fx.FetchAsync("LARGEBATCH", "batchreader", totalMsgs);
Interlocked.Add(ref fetchedCount, batch.Messages.Count);
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < 50; i++)
await fx.PublishAsync("lb.event", $"extra-{i}");
}
catch (Exception ex) { errors.Add(ex); }
}));
errors.ShouldBeEmpty();
fetchedCount.ShouldBe(totalMsgs);
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterConsumerDeleteConcurrent norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_concurrent_consumer_delete_and_create_is_thread_safe()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("CONDEL", ["condel.>"], 1);
const int initialCount = 20;
for (var i = 0; i < initialCount; i++)
await fx.CreateConsumerAsync("CONDEL", $"c{i}");
var errors = new ConcurrentBag<Exception>();
await Task.WhenAll(
Task.Run(async () =>
{
try
{
for (var i = 0; i < initialCount / 2; i++)
await fx.RequestAsync(
$"{JetStreamApiSubjects.ConsumerDelete}CONDEL.c{i}", "{}");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = initialCount; i < initialCount + 10; i++)
await fx.CreateConsumerAsync("CONDEL", $"c{i}");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
for (var i = 0; i < 30; i++)
_ = await fx.GetStreamInfoAsync("CONDEL");
}
catch (Exception ex) { errors.Add(ex); }
}));
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceJetStreamClusterStreamPurgeConcurrentFetch norace_2_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Cluster_stream_purge_concurrent_with_fetch_does_not_deadlock()
{
await using var fx = await ClusterFixture.StartAsync(nodes: 3);
await fx.CreateStreamAsync("PURGELOAD", ["pl.>"], 1);
await fx.CreateConsumerAsync("PURGELOAD", "purgereader");
for (var i = 0; i < 100; i++)
await fx.PublishAsync("pl.event", $"msg-{i}");
var errors = new ConcurrentBag<Exception>();
await Task.WhenAll(
Task.Run(async () =>
{
try
{
await fx.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGELOAD", "{}");
}
catch (Exception ex) { errors.Add(ex); }
}),
Task.Run(async () =>
{
try
{
_ = await fx.FetchAsync("PURGELOAD", "purgereader", 50);
}
catch (Exception ex) { errors.Add(ex); }
}));
errors.ShouldBeEmpty();
}
}

View File

@@ -0,0 +1,915 @@
// Go parity: golang/nats-server/server/norace_1_test.go
// Covers: concurrent publish/subscribe thread safety, SubList trie integrity
// under high concurrency, wildcard routing under load, queue group balancing,
// cache invalidation safety, and subject tree concurrent insert/remove.
using System.Collections.Concurrent;
using NATS.Server.Subscriptions;
namespace NATS.Server.Core.Tests.Stress;
/// <summary>
/// Stress tests for concurrent pub/sub operations on the in-process SubList and SubjectMatch
/// classes. All tests use Parallel.For / Task.WhenAll to exercise thread safety directly
/// without spinning up a real NatsServer.
///
/// Go ref: norace_1_test.go — concurrent subscription and matching operations.
/// </summary>
public class ConcurrentPubSubStressTests
{
// ---------------------------------------------------------------
// Go: TestNoRaceSublistConcurrent100Subscribers norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_100_concurrent_subscribers_all_inserted_without_error()
{
// 100 concurrent goroutines each Subscribe to the same subject and then Match.
using var subList = new SubList();
const int count = 100;
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, count, i =>
{
try
{
subList.Insert(new Subscription { Subject = "stress.concurrent", Sid = $"s{i}" });
var result = subList.Match("stress.concurrent");
result.PlainSubs.Length.ShouldBeGreaterThan(0);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
subList.Count.ShouldBe((uint)count);
}
// ---------------------------------------------------------------
// Go: TestNoRace50ConcurrentPublishers norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_50_concurrent_publishers_produce_correct_match_counts()
{
// 50 goroutines each publish 100 times to their own subject.
// Verifies that Match never throws even under heavy concurrent write/read.
using var subList = new SubList();
const int publishers = 50;
const int messagesEach = 100;
var errors = new ConcurrentBag<Exception>();
// Pre-insert one subscription per publisher subject
for (var i = 0; i < publishers; i++)
{
subList.Insert(new Subscription
{
Subject = $"pub.stress.{i}",
Sid = $"pre-{i}",
});
}
Parallel.For(0, publishers, i =>
{
try
{
for (var j = 0; j < messagesEach; j++)
{
var result = subList.Match($"pub.stress.{i}");
result.PlainSubs.Length.ShouldBe(1);
}
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubUnsubConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_concurrent_subscribe_and_unsubscribe_does_not_crash()
{
using var subList = new SubList();
const int ops = 300;
var subs = new ConcurrentBag<Subscription>();
var errors = new ConcurrentBag<Exception>();
// Concurrent inserts and removes — neither side holds a reference the other
// side needs, so any interleaving is valid as long as it doesn't throw.
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < ops; i++)
{
var sub = new Subscription { Subject = $"unsub.{i % 30}", Sid = $"ins-{i}" };
subList.Insert(sub);
subs.Add(sub);
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
foreach (var sub in subs.Take(ops / 2))
subList.Remove(sub);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceConcurrentMatchOperations norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_concurrent_match_operations_are_thread_safe()
{
using var subList = new SubList();
for (var i = 0; i < 50; i++)
{
subList.Insert(new Subscription
{
Subject = $"match.safe.{i % 10}",
Sid = $"m{i}",
});
}
var errors = new ConcurrentBag<Exception>();
// 200 threads all calling Match simultaneously
Parallel.For(0, 200, i =>
{
try
{
var result = subList.Match($"match.safe.{i % 10}");
result.ShouldNotBeNull();
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRace1000ConcurrentSubscriptions norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_handles_1000_concurrent_subscriptions_without_error()
{
using var subList = new SubList();
const int count = 1000;
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, count, i =>
{
try
{
subList.Insert(new Subscription
{
Subject = $"big.load.{i % 100}",
Sid = $"big-{i}",
});
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
subList.Count.ShouldBe((uint)count);
}
// ---------------------------------------------------------------
// Go: TestNoRace10000SubscriptionsWithConcurrentMatch norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_handles_10000_subscriptions_with_concurrent_matches()
{
using var subList = new SubList();
const int count = 10_000;
// Sequential insert to avoid any write-write contention noise
for (var i = 0; i < count; i++)
{
subList.Insert(new Subscription
{
Subject = $"huge.{i % 200}.data",
Sid = $"h{i}",
});
}
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 500, i =>
{
try
{
var result = subList.Match($"huge.{i % 200}.data");
// Each subject bucket has count/200 = 50 subscribers
result.PlainSubs.Length.ShouldBe(50);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceWildcardConcurrentPub norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_wildcard_subjects_routed_correctly_under_concurrent_match()
{
using var subList = new SubList();
subList.Insert(new Subscription { Subject = "wc.*", Sid = "pwc" });
subList.Insert(new Subscription { Subject = "wc.>", Sid = "fwc" });
subList.Insert(new Subscription { Subject = "wc.specific", Sid = "lit" });
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 400, i =>
{
try
{
var subject = (i % 3) switch
{
0 => "wc.specific",
1 => "wc.anything",
_ => "wc.deep.nested",
};
var result = subList.Match(subject);
// wc.* matches single-token, wc.> matches all
result.PlainSubs.Length.ShouldBeGreaterThan(0);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceQueueGroupBalancingUnderLoad norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_queue_group_balancing_correct_under_concurrent_load()
{
using var subList = new SubList();
const int memberCount = 20;
for (var i = 0; i < memberCount; i++)
{
subList.Insert(new Subscription
{
Subject = "queue.load",
Queue = "workers",
Sid = $"q{i}",
});
}
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 200, i =>
{
try
{
var result = subList.Match("queue.load");
result.QueueSubs.Length.ShouldBe(1);
result.QueueSubs[0].Length.ShouldBe(memberCount);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRace100ConcurrentPubsSameSubject norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_100_concurrent_publishes_to_same_subject_all_processed()
{
using var subList = new SubList();
subList.Insert(new Subscription { Subject = "same.subject", Sid = "single" });
var matchCount = 0;
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 100, _ =>
{
try
{
var result = subList.Match("same.subject");
result.PlainSubs.Length.ShouldBe(1);
Interlocked.Increment(ref matchCount);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
matchCount.ShouldBe(100);
}
// ---------------------------------------------------------------
// Go: TestNoRaceConcurrentIdenticalSubjects norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_concurrent_subscribe_with_identical_subjects_all_inserted()
{
using var subList = new SubList();
const int count = 100;
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, count, i =>
{
try
{
subList.Insert(new Subscription
{
Subject = "identical.subject",
Sid = $"ident-{i}",
});
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
var result = subList.Match("identical.subject");
result.PlainSubs.Length.ShouldBe(count);
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubscribePublishInterleaving norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_subscribe_publish_interleaving_does_not_lose_messages()
{
using var subList = new SubList();
var errors = new ConcurrentBag<Exception>();
var totalMatches = 0;
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < 100; i++)
{
subList.Insert(new Subscription
{
Subject = $"interleave.{i % 10}",
Sid = $"il-{i}",
});
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 200; i++)
{
var result = subList.Match($"interleave.{i % 10}");
Interlocked.Add(ref totalMatches, result.PlainSubs.Length);
}
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
// We cannot assert a fixed count because of race between sub insert and match,
// but no exception is the primary invariant.
totalMatches.ShouldBeGreaterThanOrEqualTo(0);
}
// ---------------------------------------------------------------
// Go: TestNoRaceCacheInvalidationConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_cache_invalidation_is_thread_safe_under_concurrent_modifications()
{
using var subList = new SubList();
// Fill the cache
for (var i = 0; i < 100; i++)
{
var sub = new Subscription { Subject = $"cache.inv.{i}", Sid = $"ci-{i}" };
subList.Insert(sub);
_ = subList.Match($"cache.inv.{i}");
}
subList.CacheCount.ShouldBeGreaterThan(0);
var errors = new ConcurrentBag<Exception>();
// Concurrent reads (cache hits) and writes (cache invalidation)
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < 200; i++)
_ = subList.Match($"cache.inv.{i % 100}");
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 100; i < 150; i++)
{
subList.Insert(new Subscription
{
Subject = $"cache.inv.{i}",
Sid = $"cinew-{i}",
});
}
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRacePurgeAndMatchConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_concurrent_batch_remove_and_match_do_not_deadlock()
{
using var subList = new SubList();
var inserted = new List<Subscription>();
var errors = new ConcurrentBag<Exception>();
for (var i = 0; i < 200; i++)
{
var sub = new Subscription { Subject = $"purge.match.{i % 20}", Sid = $"pm-{i}" };
subList.Insert(sub);
inserted.Add(sub);
}
Parallel.Invoke(
() =>
{
try
{
subList.RemoveBatch(inserted.Take(100));
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 100; i++)
_ = subList.Match($"purge.match.{i % 20}");
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRace1000Subjects10SubscribersEach norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_1000_subjects_10_subscribers_each_concurrent_match_correct()
{
using var subList = new SubList();
const int subjects = 200; // reduced for CI speed; same shape as 1000
const int subsPerSubject = 5;
for (var s = 0; s < subjects; s++)
{
for (var n = 0; n < subsPerSubject; n++)
{
subList.Insert(new Subscription
{
Subject = $"big.tree.{s}",
Sid = $"bt-{s}-{n}",
});
}
}
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, subjects * 3, i =>
{
try
{
var result = subList.Match($"big.tree.{i % subjects}");
result.PlainSubs.Length.ShouldBe(subsPerSubject);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceMixedWildcardLiteralConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_mixed_wildcard_and_literal_subscriptions_under_concurrent_match()
{
using var subList = new SubList();
// Mix of literals, * wildcards, and > wildcards
for (var i = 0; i < 20; i++)
{
subList.Insert(new Subscription { Subject = $"mix.{i}.literal", Sid = $"lit-{i}" });
subList.Insert(new Subscription { Subject = $"mix.{i}.*", Sid = $"pwc-{i}" });
}
subList.Insert(new Subscription { Subject = "mix.>", Sid = "fwc-root" });
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 300, i =>
{
try
{
var idx = i % 20;
var result = subList.Match($"mix.{idx}.literal");
// Matches: the literal sub, the * wildcard sub, and the > sub
result.PlainSubs.Length.ShouldBe(3);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceHighThroughputPublish norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_high_throughput_10000_messages_to_single_subscriber()
{
using var subList = new SubList();
subList.Insert(new Subscription { Subject = "throughput.test", Sid = "tp1" });
var count = 0;
var errors = new ConcurrentBag<Exception>();
for (var i = 0; i < 10_000; i++)
{
try
{
var result = subList.Match("throughput.test");
result.PlainSubs.Length.ShouldBe(1);
count++;
}
catch (Exception ex) { errors.Add(ex); }
}
errors.ShouldBeEmpty();
count.ShouldBe(10_000);
}
// ---------------------------------------------------------------
// Go: TestNoRaceQueueSubConcurrentUnsubscribe norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_concurrent_queue_group_subscribe_and_unsubscribe_is_safe()
{
using var subList = new SubList();
const int ops = 200;
var inserted = new ConcurrentBag<Subscription>();
var errors = new ConcurrentBag<Exception>();
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < ops; i++)
{
var sub = new Subscription
{
Subject = $"qg.stress.{i % 10}",
Queue = $"grp-{i % 5}",
Sid = $"qgs-{i}",
};
subList.Insert(sub);
inserted.Add(sub);
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
foreach (var sub in inserted.Take(ops / 2))
subList.Remove(sub);
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < ops; i++)
_ = subList.Match($"qg.stress.{i % 10}");
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRace500Subjects5SubscribersEach norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_500_subjects_5_subscribers_each_concurrent_match_returns_correct_results()
{
using var subList = new SubList();
const int subjects = 100; // scaled for CI speed
const int subsPerSubject = 5;
for (var s = 0; s < subjects; s++)
{
for (var n = 0; n < subsPerSubject; n++)
{
subList.Insert(new Subscription
{
Subject = $"five.subs.{s}",
Sid = $"fs-{s}-{n}",
});
}
}
var errors = new ConcurrentBag<Exception>();
var correctCount = 0;
Parallel.For(0, subjects * 4, i =>
{
try
{
var result = subList.Match($"five.subs.{i % subjects}");
if (result.PlainSubs.Length == subsPerSubject)
Interlocked.Increment(ref correctCount);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
correctCount.ShouldBe(subjects * 4);
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubjectValidationConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubjectMatch_validation_is_thread_safe_under_concurrent_calls()
{
var errors = new ConcurrentBag<Exception>();
var validCount = 0;
Parallel.For(0, 1000, i =>
{
try
{
var subject = (i % 4) switch
{
0 => $"valid.subject.{i}",
1 => $"valid.*.wildcard",
2 => $"valid.>",
_ => string.Empty, // invalid
};
var isValid = SubjectMatch.IsValidSubject(subject);
if (isValid)
Interlocked.Increment(ref validCount);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
// 750 valid, 250 empty (invalid)
validCount.ShouldBe(750);
}
// ---------------------------------------------------------------
// Go: TestNoRaceHasInterestConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_has_interest_returns_consistent_results_under_concurrent_insert()
{
using var subList = new SubList();
var errors = new ConcurrentBag<Exception>();
var interestFoundCount = 0;
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < 200; i++)
{
subList.Insert(new Subscription
{
Subject = $"interest.{i % 20}",
Sid = $"hi-{i}",
});
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 200; i++)
{
if (subList.HasInterest($"interest.{i % 20}"))
Interlocked.Increment(ref interestFoundCount);
}
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
interestFoundCount.ShouldBeGreaterThanOrEqualTo(0);
}
// ---------------------------------------------------------------
// Go: TestNoRaceNumInterestConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_num_interest_is_consistent_under_high_concurrency()
{
using var subList = new SubList();
const int subCount = 80;
for (var i = 0; i < subCount; i++)
{
subList.Insert(new Subscription
{
Subject = "num.interest.stress",
Sid = $"nis-{i}",
});
}
var errors = new ConcurrentBag<Exception>();
Parallel.For(0, 400, _ =>
{
try
{
var (plain, queue) = subList.NumInterest("num.interest.stress");
plain.ShouldBe(subCount);
queue.ShouldBe(0);
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceReverseMatchConcurrent norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_reverse_match_concurrent_with_inserts_does_not_throw()
{
using var subList = new SubList();
var errors = new ConcurrentBag<Exception>();
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < 100; i++)
{
subList.Insert(new Subscription
{
Subject = $"rev.stress.{i % 10}",
Sid = $"rs-{i}",
});
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 150; i++)
_ = subList.ReverseMatch($"rev.stress.{i % 10}");
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
}
// ---------------------------------------------------------------
// Go: TestNoRaceStatsConsistencyUnderLoad norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public void SubList_stats_remain_consistent_under_concurrent_insert_remove_match()
{
using var subList = new SubList();
const int ops = 300;
var insertedSubs = new ConcurrentBag<Subscription>();
var errors = new ConcurrentBag<Exception>();
Parallel.Invoke(
() =>
{
try
{
for (var i = 0; i < ops; i++)
{
var sub = new Subscription
{
Subject = $"stats.stress.{i % 30}",
Sid = $"ss-{i}",
};
subList.Insert(sub);
insertedSubs.Add(sub);
}
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < ops; i++)
_ = subList.Match($"stats.stress.{i % 30}");
}
catch (Exception ex) { errors.Add(ex); }
},
() =>
{
try
{
for (var i = 0; i < 50; i++)
_ = subList.Stats();
}
catch (Exception ex) { errors.Add(ex); }
});
errors.ShouldBeEmpty();
var finalStats = subList.Stats();
finalStats.NumInserts.ShouldBeGreaterThan(0UL);
finalStats.NumMatches.ShouldBeGreaterThan(0UL);
}
}

View File

@@ -0,0 +1,738 @@
// Go parity: golang/nats-server/server/norace_1_test.go
// Covers: slow consumer detection, backpressure stats, rapid subscribe/unsubscribe
// cycles, multi-client connection stress, large message delivery, and connection
// lifecycle stability under load using real NatsServer instances.
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.TestUtilities;
namespace NATS.Server.Core.Tests.Stress;
/// <summary>
/// Stress tests for slow consumer behaviour and connection lifecycle using real NatsServer
/// instances wired with raw Socket connections following the same pattern as
/// ClientSlowConsumerTests.cs and ServerTests.cs.
///
/// Go ref: norace_1_test.go — slow consumer, connection churn, and load tests.
/// </summary>
public class SlowConsumerStressTests
{
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static async Task<Socket> ConnectRawAsync(int port)
{
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(IPAddress.Loopback, port);
// Drain the INFO line
var buf = new byte[4096];
await sock.ReceiveAsync(buf, SocketFlags.None);
return sock;
}
// ---------------------------------------------------------------
// Go: TestNoRaceSlowConsumerStatIncrement norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Slow_consumer_stat_incremented_when_client_falls_behind()
{
// Go: TestNoClientLeakOnSlowConsumer — verify Stats.SlowConsumers increments.
const long maxPending = 512;
const int payloadSize = 256;
const int floodCount = 30;
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port, MaxPending = maxPending },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var slowSub = await ConnectRawAsync(port);
await slowSub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB sc.stat 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(slowSub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var payload = new string('Z', payloadSize);
var sb = new StringBuilder();
for (var i = 0; i < floodCount; i++)
sb.Append($"PUB sc.stat {payloadSize}\r\n{payload}\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
await Task.Delay(500);
var stats = server.Stats;
Interlocked.Read(ref stats.SlowConsumers).ShouldBeGreaterThan(0);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceSlowConsumerClientsTrackedIndependently norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Multiple_slow_consumers_tracked_independently_in_stats()
{
const long maxPending = 256;
const int payloadSize = 128;
const int floodCount = 20;
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port, MaxPending = maxPending },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
// Two independent slow subscribers
using var slow1 = await ConnectRawAsync(port);
using var slow2 = await ConnectRawAsync(port);
await slow1.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB multi.slow 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(slow1, "PONG");
await slow2.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB multi.slow 2\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(slow2, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var payload = new string('A', payloadSize);
var sb = new StringBuilder();
for (var i = 0; i < floodCount; i++)
sb.Append($"PUB multi.slow {payloadSize}\r\n{payload}\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
await Task.Delay(600);
var stats = server.Stats;
Interlocked.Read(ref stats.SlowConsumers).ShouldBeGreaterThan(0);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRacePublisherBackpressure norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Fast_publisher_with_slow_reader_generates_backpressure_stats()
{
const long maxPending = 512;
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port, MaxPending = maxPending },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub = await ConnectRawAsync(port);
await sub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB bp.test 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var payload = new string('P', 400);
var sb = new StringBuilder();
for (var i = 0; i < 25; i++)
sb.Append($"PUB bp.test 400\r\n{payload}\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
await Task.Delay(400);
var stats = server.Stats;
// At least the SlowConsumers counter or client count dropped
(Interlocked.Read(ref stats.SlowConsumers) > 0 || server.ClientCount <= 2)
.ShouldBeTrue();
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRace100RapidPublishes norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Subscriber_receives_messages_after_100_rapid_publishes()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub = await ConnectRawAsync(port);
await sub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB rapid 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var sb = new StringBuilder();
for (var i = 0; i < 100; i++)
sb.Append("PUB rapid 4\r\nping\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
var received = await SocketTestHelper.ReadUntilAsync(sub, "MSG rapid", 5000);
received.ShouldContain("MSG rapid");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceConcurrentSubscribeStartup norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Concurrent_publish_and_subscribe_startup_does_not_crash_server()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
var tasks = Enumerable.Range(0, 10).Select(async i =>
{
using var sock = await ConnectRawAsync(port);
await sock.SendAsync(
Encoding.ASCII.GetBytes($"CONNECT {{\"verbose\":false}}\r\nSUB conc.start.{i} {i + 1}\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sock, "PONG", 3000);
});
await Task.WhenAll(tasks);
server.ClientCount.ShouldBeGreaterThanOrEqualTo(0);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceLargeMessageMultipleSubscribers norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Large_message_published_and_received_by_multiple_subscribers()
{
// Use 8KB payload — large enough to span multiple TCP segments but small
// enough to stay well within the default MaxPending limit in CI.
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
const int payloadSize = 8192;
var payload = new string('L', payloadSize);
try
{
using var sub1 = await ConnectRawAsync(port);
using var sub2 = await ConnectRawAsync(port);
await sub1.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB large.msg 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub1, "PONG");
await sub2.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB large.msg 2\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub2, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
await pub.SendAsync(Encoding.ASCII.GetBytes($"PUB large.msg {payloadSize}\r\n{payload}\r\nPING\r\n"));
// Use a longer timeout for large message delivery
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 10000);
var r1 = await SocketTestHelper.ReadUntilAsync(sub1, "MSG large.msg", 10000);
var r2 = await SocketTestHelper.ReadUntilAsync(sub2, "MSG large.msg", 10000);
r1.ShouldContain("MSG large.msg");
r2.ShouldContain("MSG large.msg");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubscribeUnsubscribeResubscribeCycle norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Subscribe_unsubscribe_resubscribe_cycle_100_times_without_error()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var client = await ConnectRawAsync(port);
await client.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
for (var i = 1; i <= 100; i++)
{
await client.SendAsync(
Encoding.ASCII.GetBytes($"SUB resub.cycle {i}\r\nUNSUB {i}\r\n"));
}
await client.SendAsync(Encoding.ASCII.GetBytes("PING\r\n"));
var resp = await SocketTestHelper.ReadUntilAsync(client, "PONG", 5000);
resp.ShouldContain("PONG");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubscriberReceivesAfterPause norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Subscriber_receives_messages_correctly_after_brief_pause()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub = await ConnectRawAsync(port);
await sub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB pause.sub 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
// Brief pause simulating a subscriber that drifts slightly
await Task.Delay(100);
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
await pub.SendAsync(Encoding.ASCII.GetBytes("PUB pause.sub 5\r\nhello\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
var received = await SocketTestHelper.ReadUntilAsync(sub, "hello", 5000);
received.ShouldContain("hello");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceMultipleClientConnectDisconnect norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Multiple_client_connections_and_disconnections_leave_server_stable()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
// Connect and disconnect 20 clients sequentially to avoid hammering the port
for (var i = 0; i < 20; i++)
{
using var sock = await ConnectRawAsync(port);
await sock.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sock, "PONG", 3000);
sock.Close();
}
// Brief settle time
await Task.Delay(200);
// Server should still accept new connections
using var final = await ConnectRawAsync(port);
await final.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nPING\r\n"));
var resp = await SocketTestHelper.ReadUntilAsync(final, "PONG", 3000);
resp.ShouldContain("PONG");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceStatsCountersUnderLoad norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Stats_in_and_out_bytes_increment_correctly_under_load()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub = await ConnectRawAsync(port);
await sub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB stats.load 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var sb = new StringBuilder();
for (var i = 0; i < 50; i++)
sb.Append("PUB stats.load 10\r\n0123456789\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
await Task.Delay(200);
var stats = server.Stats;
Interlocked.Read(ref stats.InMsgs).ShouldBeGreaterThan(0);
Interlocked.Read(ref stats.OutMsgs).ShouldBeGreaterThan(0);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceRapidConnectDisconnect norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Rapid_connect_disconnect_cycles_do_not_corrupt_server_state()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
// 30 rapid sequential connect + disconnect cycles
for (var i = 0; i < 30; i++)
{
var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(IPAddress.Loopback, port);
// Drain INFO
var buf = new byte[512];
await sock.ReceiveAsync(buf, SocketFlags.None);
// Immediately close — simulates a client that disconnects without CONNECT
sock.Close();
sock.Dispose();
}
await Task.Delay(300);
// Server should still respond
using var healthy = await ConnectRawAsync(port);
await healthy.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nPING\r\n"));
var resp = await SocketTestHelper.ReadUntilAsync(healthy, "PONG", 3000);
resp.ShouldContain("PONG");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRacePublishWithCancellation norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Server_accepts_connection_after_cancelled_client_task()
{
var port = TestPortAllocator.GetFreePort();
using var serverCts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(serverCts.Token);
await server.WaitForReadyAsync();
try
{
using var clientCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
// Attempt a receive with a very short timeout — the token will cancel the read
// but the server should not be destabilised by the abrupt disconnect.
try
{
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(IPAddress.Loopback, port);
var buf = new byte[512];
await sock.ReceiveAsync(buf, SocketFlags.None, clientCts.Token);
}
catch (OperationCanceledException)
{
// Expected
}
await Task.Delay(200);
// Server should still function
using var good = await ConnectRawAsync(port);
await good.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nPING\r\n"));
var resp = await SocketTestHelper.ReadUntilAsync(good, "PONG", 3000);
resp.ShouldContain("PONG");
}
finally
{
await serverCts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceSlowConsumerClientCountDrops norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Slow_consumer_is_removed_from_client_count_after_detection()
{
const long maxPending = 512;
const int payloadSize = 256;
const int floodCount = 20;
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port, MaxPending = maxPending },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var slowSub = await ConnectRawAsync(port);
await slowSub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB drop.test 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(slowSub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var payload = new string('D', payloadSize);
var sb = new StringBuilder();
for (var i = 0; i < floodCount; i++)
sb.Append($"PUB drop.test {payloadSize}\r\n{payload}\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
await Task.Delay(600);
// Publisher is still alive; slow subscriber has been dropped
server.ClientCount.ShouldBeLessThanOrEqualTo(2);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceSubjectMatchingUnderConcurrentConnections norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Server_delivers_to_correct_subscriber_when_multiple_subjects_active()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub1 = await ConnectRawAsync(port);
using var sub2 = await ConnectRawAsync(port);
await sub1.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB target.A 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub1, "PONG");
await sub2.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB target.B 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub2, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
await pub.SendAsync(Encoding.ASCII.GetBytes("PUB target.A 5\r\nhello\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 5000);
var r1 = await SocketTestHelper.ReadUntilAsync(sub1, "hello", 3000);
r1.ShouldContain("MSG target.A");
// sub2 should NOT have received the target.A message
sub2.ReceiveTimeout = 200;
var buf = new byte[512];
var n = 0;
try { n = sub2.Receive(buf); } catch (SocketException) { }
var s2Data = Encoding.ASCII.GetString(buf, 0, n);
s2Data.ShouldNotContain("target.A");
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
// ---------------------------------------------------------------
// Go: TestNoRaceServerRejectsPayloadOverLimit norace_1_test.go
// ---------------------------------------------------------------
[Fact]
[Trait("Category", "Stress")]
public async Task Server_remains_stable_after_processing_many_medium_sized_messages()
{
var port = TestPortAllocator.GetFreePort();
using var cts = new CancellationTokenSource();
var server = new NatsServer(
new NatsOptions { Port = port },
NullLoggerFactory.Instance);
_ = server.StartAsync(cts.Token);
await server.WaitForReadyAsync();
try
{
using var sub = await ConnectRawAsync(port);
await sub.SendAsync(
Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\nSUB medium.msgs 1\r\nPING\r\n"));
await SocketTestHelper.ReadUntilAsync(sub, "PONG");
using var pub = await ConnectRawAsync(port);
await pub.SendAsync(Encoding.ASCII.GetBytes("CONNECT {\"verbose\":false}\r\n"));
var payload = new string('M', 1024); // 1 KB each
var sb = new StringBuilder();
for (var i = 0; i < 200; i++)
sb.Append($"PUB medium.msgs 1024\r\n{payload}\r\n");
sb.Append("PING\r\n");
await pub.SendAsync(Encoding.ASCII.GetBytes(sb.ToString()));
await SocketTestHelper.ReadUntilAsync(pub, "PONG", 10000);
var stats = server.Stats;
Interlocked.Read(ref stats.InMsgs).ShouldBeGreaterThanOrEqualTo(200);
}
finally
{
await cts.CancelAsync();
server.Dispose();
}
}
}