docs: add XML doc comments to server types and fix flaky test timings

Add XML doc comments to public properties across EventTypes, Connz, Varz,
NatsOptions, StreamConfig, IStreamStore, FileStore, MqttListener,
MqttSessionStore, MessageTraceContext, and JetStreamApiResponse. Fix flaky
tests by increasing timing margins (ResponseTracker expiry 1ms→50ms,
sleep 50ms→200ms) and document known flaky test patterns in tests.md.
This commit is contained in:
Joseph Doherty
2026-03-13 18:47:48 -04:00
parent 1d4b87e5f9
commit 88a82ee860
24 changed files with 2874 additions and 216 deletions

View File

@@ -42,7 +42,7 @@ public class AccountIsolationTests(AccountServerFixture fixture)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();
var completed = await Task.WhenAny(readTask, Task.Delay(1000));
var completed = await Task.WhenAny(readTask, Task.Delay(3000));
completed.ShouldNotBe(readTask);
}
@@ -75,7 +75,7 @@ public class AccountIsolationTests(AccountServerFixture fixture)
using var ctsBNoMsg = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var readBTask = subscriptionB.Msgs.ReadAsync(ctsBNoMsg.Token).AsTask();
var completedB = await Task.WhenAny(readBTask, Task.Delay(1000));
var completedB = await Task.WhenAny(readBTask, Task.Delay(3000));
completedB.ShouldNotBe(readBTask);
// Cancel the abandoned read so it doesn't consume the next message
await ctsBNoMsg.CancelAsync();
@@ -90,7 +90,7 @@ public class AccountIsolationTests(AccountServerFixture fixture)
using var ctsANoMsg = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var readATask2 = subscriptionA.Msgs.ReadAsync(ctsANoMsg.Token).AsTask();
var completedA2 = await Task.WhenAny(readATask2, Task.Delay(1000));
var completedA2 = await Task.WhenAny(readATask2, Task.Delay(3000));
completedA2.ShouldNotBe(readATask2);
await ctsANoMsg.CancelAsync();
try { await readATask2; } catch (OperationCanceledException) { }

View File

@@ -62,7 +62,7 @@ public class CoreMessagingTests(NatsServerFixture fixture)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();
var winner = await Task.WhenAny(readTask, Task.Delay(1000));
var winner = await Task.WhenAny(readTask, Task.Delay(3000));
winner.ShouldNotBe(readTask);
}
@@ -327,7 +327,7 @@ public class CoreMessagingTests(NatsServerFixture fixture)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var readTask = subscription.Msgs.ReadAsync(cts.Token).AsTask();
var winner = await Task.WhenAny(readTask, Task.Delay(1000));
var winner = await Task.WhenAny(readTask, Task.Delay(3000));
winner.ShouldNotBe(readTask);
}

View File

@@ -491,7 +491,7 @@ public class ConcurrencyStressTests
for (var i = 0; i < 10; i++)
{
streamManager.Purge("PURGECONC");
Thread.Sleep(1);
Thread.Sleep(5);
}
}
catch (Exception ex) { errors.Add(ex); }

View File

@@ -32,19 +32,19 @@ public class ResponseTrackerTests
[Fact]
public void Enforces_expiry()
{
var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(1));
var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(50));
tracker.RegisterReply("_INBOX.abc");
Thread.Sleep(50);
Thread.Sleep(200);
tracker.IsReplyAllowed("_INBOX.abc").ShouldBeFalse();
}
[Fact]
public void Prune_removes_expired()
{
var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(1));
var tracker = new ResponseTracker(maxMsgs: 0, expires: TimeSpan.FromMilliseconds(50));
tracker.RegisterReply("_INBOX.a");
tracker.RegisterReply("_INBOX.b");
Thread.Sleep(50);
Thread.Sleep(200);
tracker.Prune();
tracker.Count.ShouldBe(0);
}

View File

@@ -296,7 +296,7 @@ public class ClusterStressTests
for (var i = 0; i < 5; i++)
{
meta.StepDown();
Thread.Sleep(2);
Thread.Sleep(10);
}
}
catch (Exception ex) { errors.Add(ex); }

View File

@@ -186,11 +186,11 @@ public class GatewayGoParityTests
var sw = System.Diagnostics.Stopwatch.StartNew();
var disposeTask = manager.DisposeAsync().AsTask();
var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(5)));
var completed = await Task.WhenAny(disposeTask, Task.Delay(TimeSpan.FromSeconds(10)));
sw.Stop();
completed.ShouldBe(disposeTask, "DisposeAsync should complete within 5 seconds");
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(4));
completed.ShouldBe(disposeTask, "DisposeAsync should complete within 10 seconds");
sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(8));
}
// ── TestGatewayAuth (stub — auth not yet wired to gateway handshake) ──

View File

@@ -53,13 +53,12 @@ public class ReplyMapCacheTests
// Go: gateway.go — entries expire after the configured TTL window
[Fact]
[SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")]
public void TTL_expiration()
{
var cache = new ReplyMapCache(capacity: 16, ttlMs: 1);
var cache = new ReplyMapCache(capacity: 16, ttlMs: 50);
cache.Set("_INBOX.ttl", "_GR_.c1.1._INBOX.ttl");
Thread.Sleep(5); // Wait longer than the 1ms TTL
Thread.Sleep(200); // Wait longer than the 50ms TTL
var found = cache.TryGet("_INBOX.ttl", out var value);
@@ -126,14 +125,13 @@ public class ReplyMapCacheTests
// Go: gateway.go — PurgeExpired removes only expired entries
[Fact]
[SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")]
public void PurgeExpired_removes_old_entries()
{
var cache = new ReplyMapCache(capacity: 16, ttlMs: 1);
var cache = new ReplyMapCache(capacity: 16, ttlMs: 50);
cache.Set("old1", "v1");
cache.Set("old2", "v2");
Thread.Sleep(5); // Ensure both entries are past the 1ms TTL
Thread.Sleep(200); // Ensure both entries are past the 50ms TTL
var purged = cache.PurgeExpired();

View File

@@ -1160,11 +1160,11 @@ public sealed class FileStoreGoParityTests : IDisposable
store.StoreMsg("foo", null, "1"u8.ToArray(), 0); // seq 1
// A small sleep so timestamps are distinct.
System.Threading.Thread.Sleep(10);
System.Threading.Thread.Sleep(50);
var t2 = DateTime.UtcNow;
store.StoreMsg("foo", null, "2"u8.ToArray(), 0); // seq 2
System.Threading.Thread.Sleep(10);
System.Threading.Thread.Sleep(50);
var t3 = DateTime.UtcNow;
store.StoreMsg("foo", null, "3"u8.ToArray(), 0); // seq 3

View File

@@ -25,6 +25,7 @@
using System.Text;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
@@ -335,7 +336,7 @@ public sealed class FileStoreTombstoneTests : IDisposable
// After restart the message should still be present (not yet expired),
// and after waiting 2 seconds it should expire.
[Fact]
public void MessageTTL_RecoverSingleMessageWithoutStreamState()
public async Task MessageTTL_RecoverSingleMessageWithoutStreamState()
{
var dir = UniqueDir("ttl-recover");
var opts = new FileStoreOptions { Directory = dir, MaxAgeMs = 1000 };
@@ -358,8 +359,8 @@ public sealed class FileStoreTombstoneTests : IDisposable
ss.LastSeq.ShouldBe(1UL);
ss.Msgs.ShouldBe(1UL);
// Wait for TTL to expire.
Thread.Sleep(2000);
// Wait for TTL to expire (1s TTL + generous margin).
await Task.Delay(2_500);
// Force expiry by storing a new message (expiry check runs before store).
store.StoreMsg("test", null, [], 0);
@@ -373,7 +374,7 @@ public sealed class FileStoreTombstoneTests : IDisposable
// After TTL expiry and restart (without stream state file),
// a tombstone should allow proper recovery of the stream state.
[Fact]
public void MessageTTL_WriteTombstoneAllowsRecovery()
public async Task MessageTTL_WriteTombstoneAllowsRecovery()
{
var dir = UniqueDir("ttl-tombstone");
var opts = new FileStoreOptions { Directory = dir, MaxAgeMs = 1000 };
@@ -388,8 +389,8 @@ public sealed class FileStoreTombstoneTests : IDisposable
ss.FirstSeq.ShouldBe(1UL);
ss.LastSeq.ShouldBe(2UL);
// Wait for seq=1 to expire.
Thread.Sleep(1500);
// Wait for seq=1 to expire (1s TTL + generous margin).
await Task.Delay(2_500);
// Force expiry.
store.StoreMsg("test", null, [], 0);
@@ -629,7 +630,6 @@ public sealed class FileStoreTombstoneTests : IDisposable
cs1.UpdateDelivered(5, 2, 1, ts);
cs1.Stop();
Thread.Sleep(20); // wait for flush
// Reopen — should recover redelivered.
var cs2 = store.ConsumerStore("o22", DateTime.UtcNow, cfg);
@@ -641,7 +641,6 @@ public sealed class FileStoreTombstoneTests : IDisposable
cs2.UpdateDelivered(7, 3, 1, ts);
cs2.Stop();
Thread.Sleep(20);
// Reopen again.
var cs3 = store.ConsumerStore("o22", DateTime.UtcNow, cfg);
@@ -654,7 +653,6 @@ public sealed class FileStoreTombstoneTests : IDisposable
cs3.UpdateAcks(6, 2);
cs3.Stop();
Thread.Sleep(20);
// Reopen and ack 4.
var cs4 = store.ConsumerStore("o22", DateTime.UtcNow, cfg);

View File

@@ -29,6 +29,7 @@
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
@@ -491,7 +492,7 @@ public sealed class MemStoreGoParityTests
s.StoreMsg("A", null, "OK"u8.ToArray(), 0);
if (i == total / 2)
{
Thread.Sleep(100);
Thread.Sleep(250);
midTime = DateTime.UtcNow;
}
}
@@ -593,7 +594,7 @@ public sealed class MemStoreGoParityTests
// Go: TestMemStoreMessageTTL server/memstore_test.go:1202
[Fact]
public void MessageTTL_ExpiresAfterDelay()
public async Task MessageTTL_ExpiresAfterDelay()
{
var cfg = new StreamConfig
{
@@ -616,8 +617,13 @@ public sealed class MemStoreGoParityTests
ss.LastSeq.ShouldBe(10UL);
ss.Msgs.ShouldBe(10UL);
// Wait for TTL to expire (> 1 sec + check interval of 1 sec)
Thread.Sleep(2_500);
// Wait for TTL to expire
await PollHelper.WaitOrThrowAsync(() =>
{
var ss2 = new StreamState();
s.FastState(ref ss2);
return ss2.Msgs == 0;
}, "TTL expiry", timeoutMs: 10_000, intervalMs: 100);
s.FastState(ref ss);
ss.FirstSeq.ShouldBe(11UL);

View File

@@ -14,6 +14,7 @@
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
@@ -286,8 +287,7 @@ public sealed class StoreInterfaceTests
// Go: TestStoreUpdateConfigTTLState server/store_test.go:574
[Fact]
[SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; Thread.Sleep waits for message TTL to expire or survive")]
public void UpdateConfigTTLState_MessageSurvivesWhenTtlDisabled()
public async Task UpdateConfigTTLState_MessageSurvivesWhenTtlDisabled()
{
var cfg = new StreamConfig
{
@@ -301,7 +301,7 @@ public sealed class StoreInterfaceTests
// TTLs disabled — message with ttl=1s should survive even after 2s.
var (seq, _) = s.StoreMsg("foo", null, [], 1);
Thread.Sleep(2_000);
await Task.Delay(2_500);
// Should not throw — message should still be present.
var loaded = s.LoadMsg(seq, null);
loaded.Sequence.ShouldBe(seq);
@@ -312,9 +312,11 @@ public sealed class StoreInterfaceTests
// TTLs enabled — message with ttl=1s should expire.
var (seq2, _) = s.StoreMsg("foo", null, [], 1);
Thread.Sleep(2_500);
// Should throw — message should have expired.
Should.Throw<KeyNotFoundException>(() => s.LoadMsg(seq2, null));
await PollHelper.WaitOrThrowAsync(() =>
{
try { s.LoadMsg(seq2, null); return false; }
catch (KeyNotFoundException) { return true; }
}, "TTL expiry", timeoutMs: 10_000, intervalMs: 100);
// Now disable TTLs again.
cfg.AllowMsgTtl = false;
@@ -322,7 +324,7 @@ public sealed class StoreInterfaceTests
// TTLs disabled — message with ttl=1s should survive.
var (seq3, _) = s.StoreMsg("foo", null, [], 1);
Thread.Sleep(2_000);
await Task.Delay(2_500);
// Should not throw — TTL wheel is gone so message stays.
var loaded3 = s.LoadMsg(seq3, null);
loaded3.Sequence.ShouldBe(seq3);

View File

@@ -1,5 +1,6 @@
using NATS.Server;
using NATS.Server.Raft;
using NATS.Server.TestUtilities;
namespace NATS.Server.Raft.Tests.Raft;
@@ -226,7 +227,6 @@ public class RaftElectionTimerTests : IDisposable
}
[Fact]
[SlopwatchSuppress("SW004", "Testing timer fires after heartbeats stop requires real delays for heartbeat simulation and timeout expiry")]
public async Task Timer_fires_after_heartbeats_stop()
{
var nodes = CreateTrackedCluster(3);
@@ -246,7 +246,7 @@ public class RaftElectionTimerTests : IDisposable
node.Role.ShouldBe(RaftRole.Follower);
// Stop sending heartbeats and wait for timer to fire
await Task.Delay(200);
await PollHelper.WaitOrThrowAsync(() => node.Role == RaftRole.Candidate, "election timeout", timeoutMs: 5000);
// Should have started an election
node.Role.ShouldBe(RaftRole.Candidate);