Files
natsdotnet/tests/NATS.Server.JetStream.Tests/JetStream/Storage/MemStoreGoParityTests.cs
Joseph Doherty 88a82ee860 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.
2026-03-13 18:47:48 -04:00

958 lines
32 KiB
C#

// Reference: golang/nats-server/server/memstore_test.go
// Tests ported in this file:
// TestMemStoreCompact → Compact_RemovesMessagesBeforeSeq
// TestMemStoreStreamStateDeleted → StreamStateDeleted_TracksDmapCorrectly
// TestMemStoreStreamTruncate → Truncate_RemovesMessagesAfterSeq
// TestMemStoreUpdateMaxMsgsPerSubject → UpdateMaxMsgsPerSubject_EnforcesNewLimit
// TestMemStoreStreamCompactMultiBlockSubjectInfo → Compact_AdjustsSubjectCount
// TestMemStoreSubjectsTotals → SubjectsTotals_MatchesStoredCounts
// TestMemStoreNumPending → NumPending_MatchesFilteredCount
// TestMemStoreMultiLastSeqs → MultiLastSeqs_ReturnsLastPerSubject
// TestMemStoreSubjectForSeq → SubjectForSeq_ReturnsCorrectSubject
// TestMemStoreSubjectDeleteMarkers → SubjectDeleteMarkers_TtlExpiry (skipped: needs pmsgcb)
// TestMemStoreAllLastSeqs → AllLastSeqs_ReturnsLastPerSubjectSorted
// TestMemStoreGetSeqFromTimeWithLastDeleted → GetSeqFromTime_WithLastDeleted
// TestMemStoreSkipMsgs → SkipMsgs_ReservesSequences
// TestMemStoreDeleteBlocks → DeleteBlocks_DmapSizeMatchesNumDeleted
// TestMemStoreMessageTTL → MessageTTL_ExpiresAfterDelay
// TestMemStoreUpdateConfigTTLState → UpdateConfig_TtlStateInitializedAndDestroyed
// TestMemStoreNextWildcardMatch → NextWildcardMatch_BoundsAreCorrect
// TestMemStoreNextLiteralMatch → NextLiteralMatch_BoundsAreCorrect
// TestMemStoreInitialFirstSeq → InitialFirstSeq_StartAtConfiguredSeq
// TestMemStoreStreamTruncateReset → TruncateReset_ClearsEverything
// TestMemStorePurgeExWithSubject → PurgeEx_WithSubject_PurgesAll
// TestMemStorePurgeExWithDeletedMsgs → PurgeEx_WithDeletedMsgs_CorrectFirstSeq
// TestMemStoreDeleteAllFirstSequenceCheck → DeleteAll_FirstSeqIsLastPlusOne
// TestMemStoreNumPendingBug → NumPending_Bug_CorrectCount
// TestMemStorePurgeLeaksDmap → Purge_ClearsDmap
// TestMemStoreMultiLastSeqsMaxAllowed → MultiLastSeqs_MaxAllowed_ThrowsWhenExceeded
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream.Storage;
/// <summary>
/// Go MemStore parity tests. Each test mirrors a specific Go test from
/// golang/nats-server/server/memstore_test.go to verify behaviour parity.
/// </summary>
public sealed class MemStoreGoParityTests
{
// Helper: cast to IStreamStore for sync methods
private static IStreamStore Sync(MemStore ms) => ms;
// -------------------------------------------------------------------------
// Compact
// -------------------------------------------------------------------------
// Go: TestMemStoreCompact server/memstore_test.go:259
[Fact]
public void Compact_RemovesMessagesBeforeSeq()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 0; i < 10; i++)
s.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
s.State().Msgs.ShouldBe(10UL);
var n = s.Compact(6);
n.ShouldBe(5UL);
var state = s.State();
state.Msgs.ShouldBe(5UL);
state.FirstSeq.ShouldBe(6UL);
// Compact past the end resets first seq
n = s.Compact(100);
n.ShouldBe(5UL);
s.State().FirstSeq.ShouldBe(100UL);
}
// -------------------------------------------------------------------------
// StreamStateDeleted
// -------------------------------------------------------------------------
// Go: TestMemStoreStreamStateDeleted server/memstore_test.go:342
[Fact]
public void StreamStateDeleted_TracksDmapCorrectly()
{
var ms = new MemStore();
var s = Sync(ms);
const ulong toStore = 10;
for (ulong i = 1; i <= toStore; i++)
s.StoreMsg("foo", null, new byte[8], 0);
s.State().Deleted.ShouldBeNull();
// Delete even sequences 2,4,6,8
var expectedDeleted = new List<ulong>();
for (ulong seq = 2; seq < toStore; seq += 2)
{
s.RemoveMsg(seq);
expectedDeleted.Add(seq);
}
var state = s.State();
state.Deleted.ShouldNotBeNull();
state.Deleted!.ShouldBe(expectedDeleted.ToArray());
// Delete 1 and 3 to fill first gap — deleted should shift forward
s.RemoveMsg(1);
s.RemoveMsg(3);
expectedDeleted = expectedDeleted.Skip(2).ToList(); // remove 2 and 4 from start
state = s.State();
state.Deleted!.ShouldBe(expectedDeleted.ToArray());
state.FirstSeq.ShouldBe(5UL);
s.Purge();
s.State().Deleted.ShouldBeNull();
}
// -------------------------------------------------------------------------
// Truncate
// -------------------------------------------------------------------------
// Go: TestMemStoreStreamTruncate server/memstore_test.go:385
[Fact]
public void Truncate_RemovesMessagesAfterSeq()
{
var ms = new MemStore();
var s = Sync(ms);
const ulong tseq = 50;
const ulong toStore = 100;
for (ulong i = 1; i < tseq; i++)
s.StoreMsg("foo", null, "ok"u8.ToArray(), 0);
for (var i = tseq; i <= toStore; i++)
s.StoreMsg("bar", null, "ok"u8.ToArray(), 0);
s.State().Msgs.ShouldBe(toStore);
s.Truncate(tseq);
s.State().Msgs.ShouldBe(tseq);
// Truncate with some interior deletes
s.RemoveMsg(10);
s.RemoveMsg(20);
s.RemoveMsg(30);
s.RemoveMsg(40);
s.Truncate(25);
var state = s.State();
// 25 seqs remaining, minus 2 deleted (10, 20) = 23 messages
state.Msgs.ShouldBe(tseq - 2 - (tseq - 25));
state.NumSubjects.ShouldBe(1); // only "foo" left
state.Deleted!.ShouldBe([10UL, 20UL]);
}
// -------------------------------------------------------------------------
// TruncateReset
// -------------------------------------------------------------------------
// Go: TestMemStoreStreamTruncateReset server/memstore_test.go:490
[Fact]
public void TruncateReset_ClearsEverything()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 0; i < 1000; i++)
s.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
s.Truncate(0);
var state = s.State();
state.Msgs.ShouldBe(0UL);
state.Bytes.ShouldBe(0UL);
state.FirstSeq.ShouldBe(0UL);
state.LastSeq.ShouldBe(0UL);
state.NumSubjects.ShouldBe(0);
state.NumDeleted.ShouldBe(0);
// Can store again after reset
for (var i = 0; i < 1000; i++)
s.StoreMsg("foo", null, "Hello World"u8.ToArray(), 0);
state = s.State();
state.Msgs.ShouldBe(1000UL);
state.FirstSeq.ShouldBe(1UL);
state.LastSeq.ShouldBe(1000UL);
state.NumSubjects.ShouldBe(1);
state.NumDeleted.ShouldBe(0);
}
// -------------------------------------------------------------------------
// UpdateMaxMsgsPerSubject
// -------------------------------------------------------------------------
// Go: TestMemStoreUpdateMaxMsgsPerSubject server/memstore_test.go:452
[Fact]
public void UpdateMaxMsgsPerSubject_EnforcesNewLimit()
{
var cfg = new StreamConfig
{
Name = "TEST",
Storage = StorageType.Memory,
Subjects = ["foo"],
MaxMsgsPer = 10,
};
var ms = new MemStore(cfg);
var s = Sync(ms);
// Increase limit — should allow more
cfg.MaxMsgsPer = 50;
s.UpdateConfig(cfg);
const int numStored = 22;
for (var i = 0; i < numStored; i++)
s.StoreMsg("foo", null, [], 0);
var ss = s.SubjectsState("foo")["foo"];
ss.Msgs.ShouldBe((ulong)numStored);
// Shrink limit — should truncate stored
cfg.MaxMsgsPer = 10;
s.UpdateConfig(cfg);
ss = s.SubjectsState("foo")["foo"];
ss.Msgs.ShouldBe(10UL);
}
// -------------------------------------------------------------------------
// CompactMultiBlockSubjectInfo
// -------------------------------------------------------------------------
// Go: TestMemStoreStreamCompactMultiBlockSubjectInfo server/memstore_test.go:531
[Fact]
public void Compact_AdjustsSubjectCount()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 0; i < 1000; i++)
s.StoreMsg($"foo.{i}", null, "Hello World"u8.ToArray(), 0);
var deleted = s.Compact(501);
deleted.ShouldBe(500UL);
s.State().NumSubjects.ShouldBe(500);
}
// -------------------------------------------------------------------------
// SubjectsTotals
// -------------------------------------------------------------------------
// Go: TestMemStoreSubjectsTotals server/memstore_test.go:557
[Fact]
public void SubjectsTotals_MatchesStoredCounts()
{
var ms = new MemStore();
var s = Sync(ms);
var fmap = new Dictionary<int, int>();
var bmap = new Dictionary<int, int>();
var rng = new Random(42);
for (var i = 0; i < 10_000; i++)
{
string ft;
Dictionary<int, int> m;
if (rng.Next(2) == 0) { ft = "foo"; m = fmap; }
else { ft = "bar"; m = bmap; }
var dt = rng.Next(100);
var subj = $"{ft}.{dt}";
m.TryGetValue(dt, out var c);
m[dt] = c + 1;
s.StoreMsg(subj, null, "Hello World"u8.ToArray(), 0);
}
// Check individual foo subjects
foreach (var kv in fmap)
{
var subj = $"foo.{kv.Key}";
var totals = s.SubjectsTotals(subj);
totals[subj].ShouldBe((ulong)kv.Value);
}
// Check foo.* wildcard
var fooTotals = s.SubjectsTotals("foo.*");
fooTotals.Count.ShouldBe(fmap.Count);
var fooExpected = (ulong)fmap.Values.Sum(n => n);
fooTotals.Values.Aggregate(0UL, (a, v) => a + v).ShouldBe(fooExpected);
// Check bar.* wildcard
var barTotals = s.SubjectsTotals("bar.*");
barTotals.Count.ShouldBe(bmap.Count);
// Check *.*
var allTotals = s.SubjectsTotals("*.*");
allTotals.Count.ShouldBe(fmap.Count + bmap.Count);
}
// -------------------------------------------------------------------------
// NumPending
// -------------------------------------------------------------------------
// Go: TestMemStoreNumPending server/memstore_test.go:637
[Fact]
public void NumPending_MatchesFilteredCount()
{
var ms = new MemStore();
var s = Sync(ms);
var tokens = new[] { "foo", "bar", "baz" };
var rng = new Random(99);
string GenSubj() => $"{tokens[rng.Next(3)]}.{tokens[rng.Next(3)]}.{tokens[rng.Next(3)]}.{tokens[rng.Next(3)]}";
for (var i = 0; i < 5_000; i++)
s.StoreMsg(GenSubj(), null, "Hello World"u8.ToArray(), 0);
var state = s.State();
var startSeqs = new ulong[] { 0, 1, 2, 200, 444, 555, 2222, 4000 };
var checkSubs = new[] { "foo.>", "*.bar.>", "foo.bar.*.baz", "*.foo.bar.*", "foo.foo.bar.baz" };
foreach (var filter in checkSubs)
{
foreach (var startSeq in startSeqs)
{
var (total, validThrough) = s.NumPending(startSeq, filter, false);
validThrough.ShouldBe(state.LastSeq);
// Sanity-check: manually count matching msgs from startSeq
var sseq = startSeq == 0 ? 1 : startSeq;
ulong expected = 0;
for (var seq = sseq; seq <= state.LastSeq; seq++)
{
try
{
var sm = s.LoadMsg(seq, null);
if (SubjectMatchesFilter(sm.Subject, filter)) expected++;
}
catch (KeyNotFoundException) { }
}
total.ShouldBe(expected, $"filter={filter} start={startSeq}");
}
}
}
// -------------------------------------------------------------------------
// MultiLastSeqs
// -------------------------------------------------------------------------
// Go: TestMemStoreMultiLastSeqs server/memstore_test.go:923
[Fact]
public void MultiLastSeqs_ReturnsLastPerSubject()
{
var ms = new MemStore();
var s = Sync(ms);
var msg = "abc"u8.ToArray();
for (var i = 0; i < 33; i++)
{
s.StoreMsg("foo.foo", null, msg, 0);
s.StoreMsg("foo.bar", null, msg, 0);
s.StoreMsg("foo.baz", null, msg, 0);
}
for (var i = 0; i < 33; i++)
{
s.StoreMsg("bar.foo", null, msg, 0);
s.StoreMsg("bar.bar", null, msg, 0);
s.StoreMsg("bar.baz", null, msg, 0);
}
// Up to seq 3
s.MultiLastSeqs(["foo.*"], 3, -1).ShouldBe([1UL, 2UL, 3UL]);
// All of foo.*
s.MultiLastSeqs(["foo.*"], 0, -1).ShouldBe([97UL, 98UL, 99UL]);
// All of bar.*
s.MultiLastSeqs(["bar.*"], 0, -1).ShouldBe([196UL, 197UL, 198UL]);
// bar.* at seq <= 99 — nothing
s.MultiLastSeqs(["bar.*"], 99, -1).ShouldBe([]);
// Explicit subjects
s.MultiLastSeqs(["foo.foo", "foo.bar", "foo.baz"], 3, -1).ShouldBe([1UL, 2UL, 3UL]);
s.MultiLastSeqs(["foo.foo", "foo.bar", "foo.baz"], 0, -1).ShouldBe([97UL, 98UL, 99UL]);
s.MultiLastSeqs(["bar.foo", "bar.bar", "bar.baz"], 0, -1).ShouldBe([196UL, 197UL, 198UL]);
s.MultiLastSeqs(["bar.foo", "bar.bar", "bar.baz"], 99, -1).ShouldBe([]);
// Single filter
s.MultiLastSeqs(["foo.foo"], 3, -1).ShouldBe([1UL]);
// De-duplicate overlapping filters
s.MultiLastSeqs(["foo.*", "foo.bar"], 3, -1).ShouldBe([1UL, 2UL, 3UL]);
// All subjects
s.MultiLastSeqs([">"], 0, -1).ShouldBe([97UL, 98UL, 99UL, 196UL, 197UL, 198UL]);
s.MultiLastSeqs([">"], 99, -1).ShouldBe([97UL, 98UL, 99UL]);
}
// -------------------------------------------------------------------------
// MultiLastSeqs — maxAllowed
// -------------------------------------------------------------------------
// Go: TestMemStoreMultiLastSeqsMaxAllowed server/memstore_test.go:1010
[Fact]
public void MultiLastSeqs_MaxAllowed_ThrowsWhenExceeded()
{
var ms = new MemStore();
var s = Sync(ms);
var msg = "abc"u8.ToArray();
for (var i = 1; i <= 100; i++)
s.StoreMsg($"foo.{i}", null, msg, 0);
Should.Throw<InvalidOperationException>(() => s.MultiLastSeqs(["foo.*"], 0, 10));
}
// -------------------------------------------------------------------------
// SubjectForSeq
// -------------------------------------------------------------------------
// Go: TestMemStoreSubjectForSeq server/memstore_test.go:1319
[Fact]
public void SubjectForSeq_ReturnsCorrectSubject()
{
var ms = new MemStore();
var s = Sync(ms);
s.StoreMsg("foo.bar", null, [], 0);
// seq 0 (not found)
Should.Throw<KeyNotFoundException>(() => s.SubjectForSeq(0));
// seq 1 — should be "foo.bar"
s.SubjectForSeq(1).ShouldBe("foo.bar");
// seq 2 (not yet stored)
Should.Throw<KeyNotFoundException>(() => s.SubjectForSeq(2));
}
// -------------------------------------------------------------------------
// AllLastSeqs
// -------------------------------------------------------------------------
// Go: TestMemStoreAllLastSeqs server/memstore_test.go:1266
[Fact]
public void AllLastSeqs_ReturnsLastPerSubjectSorted()
{
var cfg = new StreamConfig
{
Name = "zzz",
Subjects = ["*.*"],
MaxMsgsPer = 50,
Storage = StorageType.Memory,
};
var ms = new MemStore(cfg);
var s = Sync(ms);
var subjs = new[] { "foo.foo", "foo.bar", "foo.baz", "bar.foo", "bar.bar", "bar.baz" };
var msg = "abc"u8.ToArray();
var rng = new Random(7);
for (var i = 0; i < 10_000; i++)
s.StoreMsg(subjs[rng.Next(subjs.Length)], null, msg, 0);
// Compute expected last sequences per subject
var expected = new List<ulong>();
foreach (var subj in subjs)
{
try
{
var sm = s.LoadLastMsg(subj, null);
expected.Add(sm.Sequence);
}
catch (KeyNotFoundException) { }
}
expected.Sort();
var seqs = s.AllLastSeqs();
seqs.ShouldBe(expected.ToArray());
}
// -------------------------------------------------------------------------
// GetSeqFromTime with last deleted
// -------------------------------------------------------------------------
// Go: TestMemStoreGetSeqFromTimeWithLastDeleted server/memstore_test.go:839
[Fact]
public void GetSeqFromTime_WithLastDeleted()
{
var ms = new MemStore();
var s = Sync(ms);
const int total = 1000;
DateTime midTime = default;
for (var i = 1; i <= total; i++)
{
s.StoreMsg("A", null, "OK"u8.ToArray(), 0);
if (i == total / 2)
{
Thread.Sleep(250);
midTime = DateTime.UtcNow;
}
}
// Delete last 100
for (var seq = total - 100; seq <= total; seq++)
s.RemoveMsg((ulong)seq);
// Should not panic and should return correct value
var found = s.GetSeqFromTime(midTime);
found.ShouldBe(501UL);
}
// -------------------------------------------------------------------------
// SkipMsgs
// -------------------------------------------------------------------------
// Go: TestMemStoreSkipMsgs server/memstore_test.go:871
[Fact]
public void SkipMsgs_ReservesSequences()
{
var ms = new MemStore();
var s = Sync(ms);
// Wrong starting sequence should fail
Should.Throw<InvalidOperationException>(() => s.SkipMsgs(10, 100));
// Skip from seq 1
s.SkipMsgs(1, 100);
var state = s.State();
state.FirstSeq.ShouldBe(101UL);
state.LastSeq.ShouldBe(100UL);
// Skip many more
s.SkipMsgs(101, 100_000);
state = s.State();
state.FirstSeq.ShouldBe(100_101UL);
state.LastSeq.ShouldBe(100_100UL);
// New store: store a message then skip
var ms2 = new MemStore();
var s2 = Sync(ms2);
s2.StoreMsg("foo", null, [], 0);
s2.SkipMsgs(2, 10);
state = s2.State();
state.FirstSeq.ShouldBe(1UL);
state.LastSeq.ShouldBe(11UL);
state.Msgs.ShouldBe(1UL);
state.NumDeleted.ShouldBe(10);
state.Deleted.ShouldNotBeNull();
state.Deleted!.Length.ShouldBe(10);
// FastState consistency
var fstate = new StreamState();
s2.FastState(ref fstate);
fstate.FirstSeq.ShouldBe(1UL);
fstate.LastSeq.ShouldBe(11UL);
fstate.Msgs.ShouldBe(1UL);
fstate.NumDeleted.ShouldBe(10);
}
// -------------------------------------------------------------------------
// DeleteBlocks
// -------------------------------------------------------------------------
// Go: TestMemStoreDeleteBlocks server/memstore_test.go:799
[Fact]
public void DeleteBlocks_DmapSizeMatchesNumDeleted()
{
var ms = new MemStore();
var s = Sync(ms);
const int total = 10_000;
for (var i = 0; i < total; i++)
s.StoreMsg("A", null, "OK"u8.ToArray(), 0);
// Delete 5000 random sequences
var rng = new Random(13);
var deleteSet = new HashSet<int>();
while (deleteSet.Count < 5000)
deleteSet.Add(rng.Next(total) + 1);
foreach (var seq in deleteSet)
s.RemoveMsg((ulong)seq);
var fstate = new StreamState();
s.FastState(ref fstate);
// NumDeleted from FastState must equal interior gap count
var fullState = s.State();
var dmapSize = fullState.Deleted?.Length ?? 0;
dmapSize.ShouldBe(fstate.NumDeleted);
}
// -------------------------------------------------------------------------
// MessageTTL
// -------------------------------------------------------------------------
// Go: TestMemStoreMessageTTL server/memstore_test.go:1202
[Fact]
public async Task MessageTTL_ExpiresAfterDelay()
{
var cfg = new StreamConfig
{
Name = "zzz",
Subjects = ["test"],
Storage = StorageType.Memory,
AllowMsgTtl = true,
};
var ms = new MemStore(cfg);
var s = Sync(ms);
const long ttl = 1; // 1 second
for (var i = 1; i <= 10; i++)
s.StoreMsg("test", null, [], ttl);
var ss = new StreamState();
s.FastState(ref ss);
ss.FirstSeq.ShouldBe(1UL);
ss.LastSeq.ShouldBe(10UL);
ss.Msgs.ShouldBe(10UL);
// 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);
ss.LastSeq.ShouldBe(10UL);
ss.Msgs.ShouldBe(0UL);
}
// -------------------------------------------------------------------------
// UpdateConfigTTLState
// -------------------------------------------------------------------------
// Go: TestMemStoreUpdateConfigTTLState server/memstore_test.go:1299
[Fact]
public void UpdateConfig_TtlStateInitializedAndDestroyed()
{
var cfg = new StreamConfig
{
Name = "zzz",
Subjects = [">"],
Storage = StorageType.Memory,
AllowMsgTtl = false,
};
var ms = new MemStore(cfg);
var s = Sync(ms);
// TTL disabled — internal TTL wheel should be null (we cannot observe it directly,
// but UpdateConfig must not throw and subsequent behaviour must be correct)
cfg.AllowMsgTtl = true;
s.UpdateConfig(cfg);
// Store with TTL — should work
s.StoreMsg("test", null, [], 3600);
s.State().Msgs.ShouldBe(1UL);
// Disable TTL again
cfg.AllowMsgTtl = false;
s.UpdateConfig(cfg);
// Message stored before disabling should still be present (TTL wheel gone but msg stays)
s.State().Msgs.ShouldBe(1UL);
}
// -------------------------------------------------------------------------
// NextWildcardMatch
// -------------------------------------------------------------------------
// Go: TestMemStoreNextWildcardMatch server/memstore_test.go:1373
[Fact]
public void NextWildcardMatch_BoundsAreCorrect()
{
var ms = new MemStore();
var s = Sync(ms);
void StoreN(string subj, int n)
{
for (var i = 0; i < n; i++)
s.StoreMsg(subj, null, "msg"u8.ToArray(), 0);
}
StoreN("foo.bar.a", 1); // seq 1
StoreN("foo.baz.bar", 10); // seqs 2-11
StoreN("foo.bar.b", 1); // seq 12
StoreN("foo.baz.bar", 10); // seqs 13-22
StoreN("foo.baz.bar.no.match", 10); // seqs 23-32
lock (ms.Gate)
{
var (first, last, found) = ms.NextWildcardMatchLocked("foo.bar.*", 0);
found.ShouldBeTrue();
first.ShouldBe(1UL);
last.ShouldBe(12UL);
(first, last, found) = ms.NextWildcardMatchLocked("foo.bar.*", 1);
found.ShouldBeTrue();
first.ShouldBe(1UL);
last.ShouldBe(12UL);
(first, last, found) = ms.NextWildcardMatchLocked("foo.bar.*", 2);
found.ShouldBeTrue();
first.ShouldBe(12UL);
last.ShouldBe(12UL);
(_, _, found) = ms.NextWildcardMatchLocked("foo.bar.*", first + 1);
found.ShouldBeFalse();
(first, last, found) = ms.NextWildcardMatchLocked("foo.baz.*", 1);
found.ShouldBeTrue();
first.ShouldBe(2UL);
last.ShouldBe(22UL);
(first, last, found) = ms.NextWildcardMatchLocked("foo.nope.*", 1);
found.ShouldBeFalse();
first.ShouldBe(0UL);
last.ShouldBe(0UL);
(first, last, found) = ms.NextWildcardMatchLocked("foo.>", 1);
found.ShouldBeTrue();
first.ShouldBe(1UL);
last.ShouldBe(32UL);
}
}
// -------------------------------------------------------------------------
// NextLiteralMatch
// -------------------------------------------------------------------------
// Go: TestMemStoreNextLiteralMatch server/memstore_test.go:1454
[Fact]
public void NextLiteralMatch_BoundsAreCorrect()
{
var ms = new MemStore();
var s = Sync(ms);
void StoreN(string subj, int n)
{
for (var i = 0; i < n; i++)
s.StoreMsg(subj, null, "msg"u8.ToArray(), 0);
}
StoreN("foo.bar.a", 1); // seq 1
StoreN("foo.baz.bar", 10); // seqs 2-11
StoreN("foo.bar.b", 1); // seq 12
StoreN("foo.baz.bar", 10); // seqs 13-22
StoreN("foo.baz.bar.no.match", 10); // seqs 23-32
lock (ms.Gate)
{
var (first, last, found) = ms.NextLiteralMatchLocked("foo.bar.a", 0);
found.ShouldBeTrue();
first.ShouldBe(1UL);
last.ShouldBe(1UL);
(_, _, found) = ms.NextLiteralMatchLocked("foo.bar.a", 2);
found.ShouldBeFalse();
(first, last, found) = ms.NextLiteralMatchLocked("foo.baz.bar", 1);
found.ShouldBeTrue();
first.ShouldBe(2UL);
last.ShouldBe(22UL);
(first, last, found) = ms.NextLiteralMatchLocked("foo.baz.bar", 22);
found.ShouldBeTrue();
first.ShouldBe(22UL);
last.ShouldBe(22UL);
(first, last, found) = ms.NextLiteralMatchLocked("foo.baz.bar", 23);
found.ShouldBeFalse();
first.ShouldBe(0UL);
last.ShouldBe(0UL);
(_, _, found) = ms.NextLiteralMatchLocked("foo.nope", 1);
found.ShouldBeFalse();
}
}
// -------------------------------------------------------------------------
// InitialFirstSeq
// -------------------------------------------------------------------------
// Go: TestMemStoreInitialFirstSeq server/memstore_test.go:765
[Fact]
public void InitialFirstSeq_StartAtConfiguredSeq()
{
var cfg = new StreamConfig
{
Name = "zzz",
Storage = StorageType.Memory,
FirstSeq = 1000,
};
var ms = new MemStore(cfg);
var s = Sync(ms);
var (seq, _) = s.StoreMsg("A", null, "OK"u8.ToArray(), 0);
seq.ShouldBe(1000UL);
(seq, _) = s.StoreMsg("B", null, "OK"u8.ToArray(), 0);
seq.ShouldBe(1001UL);
var state = new StreamState();
s.FastState(ref state);
state.Msgs.ShouldBe(2UL);
state.FirstSeq.ShouldBe(1000UL);
state.LastSeq.ShouldBe(1001UL);
}
// -------------------------------------------------------------------------
// PurgeEx with subject
// -------------------------------------------------------------------------
// Go: TestMemStorePurgeExWithSubject server/memstore_test.go:437
[Fact]
public void PurgeEx_WithSubject_PurgesAll()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 0; i < 100; i++)
s.StoreMsg("foo", null, [], 0);
s.PurgeEx("foo", 1, 0);
s.State().Msgs.ShouldBe(0UL);
}
// -------------------------------------------------------------------------
// PurgeEx with deleted messages
// -------------------------------------------------------------------------
// Go: TestMemStorePurgeExWithDeletedMsgs server/memstore_test.go:1031
[Fact]
public void PurgeEx_WithDeletedMsgs_CorrectFirstSeq()
{
var ms = new MemStore();
var s = Sync(ms);
var msg = "abc"u8.ToArray();
for (var i = 1; i <= 10; i++)
s.StoreMsg("foo", null, msg, 0);
s.RemoveMsg(2);
s.RemoveMsg(9); // was the bug
var n = s.PurgeEx("", 9, 0);
n.ShouldBe(7UL); // seqs 1,3,4,5,6,7,8 (not 2 since deleted, not 9 since deleted)
var state = new StreamState();
s.FastState(ref state);
state.FirstSeq.ShouldBe(10UL);
state.LastSeq.ShouldBe(10UL);
state.Msgs.ShouldBe(1UL);
}
// -------------------------------------------------------------------------
// DeleteAll FirstSequenceCheck
// -------------------------------------------------------------------------
// Go: TestMemStoreDeleteAllFirstSequenceCheck server/memstore_test.go:1060
[Fact]
public void DeleteAll_FirstSeqIsLastPlusOne()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 1; i <= 10; i++)
s.StoreMsg("foo", null, "abc"u8.ToArray(), 0);
for (ulong seq = 1; seq <= 10; seq++)
s.RemoveMsg(seq);
var state = new StreamState();
s.FastState(ref state);
state.FirstSeq.ShouldBe(11UL);
state.LastSeq.ShouldBe(10UL);
state.Msgs.ShouldBe(0UL);
}
// -------------------------------------------------------------------------
// NumPending — bug fix
// -------------------------------------------------------------------------
// Go: TestMemStoreNumPendingBug server/memstore_test.go:1137
[Fact]
public void NumPending_Bug_CorrectCount()
{
var ms = new MemStore();
var s = Sync(ms);
foreach (var subj in new[] { "foo.foo", "foo.bar", "foo.baz", "foo.zzz" })
{
s.StoreMsg("foo.aaa", null, [], 0);
s.StoreMsg(subj, null, [], 0);
s.StoreMsg(subj, null, [], 0);
}
// 12 msgs total
var (total, _) = s.NumPending(4, "foo.*", false);
ulong expected = 0;
for (var seq = 4; seq <= 12; seq++)
{
try
{
var sm = s.LoadMsg((ulong)seq, null);
if (SubjectMatchesFilter(sm.Subject, "foo.*")) expected++;
}
catch (KeyNotFoundException) { }
}
total.ShouldBe(expected);
}
// -------------------------------------------------------------------------
// Purge clears dmap
// -------------------------------------------------------------------------
// Go: TestMemStorePurgeLeaksDmap server/memstore_test.go:1168
[Fact]
public void Purge_ClearsDmap()
{
var ms = new MemStore();
var s = Sync(ms);
for (var i = 0; i < 10; i++)
s.StoreMsg("foo", null, [], 0);
for (ulong i = 2; i <= 9; i++)
s.RemoveMsg(i);
// 8 interior gaps now
var state = s.State();
state.NumDeleted.ShouldBe(8);
// Purge should also clear dmap
var purged = s.Purge();
purged.ShouldBe(2UL); // 2 actual msgs remain (1 and 10)
state = s.State();
state.NumDeleted.ShouldBe(0);
state.Deleted.ShouldBeNull();
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
private static bool SubjectMatchesFilter(string subject, string filter)
{
if (string.IsNullOrEmpty(filter) || filter == ">") return true;
if (NATS.Server.Subscriptions.SubjectMatch.IsLiteral(filter))
return string.Equals(subject, filter, StringComparison.Ordinal);
return NATS.Server.Subscriptions.SubjectMatch.MatchLiteral(subject, filter);
}
}