Files
natsdotnet/tests/NATS.Server.Tests/JetStream/Storage/MemStoreGoParityTests.cs
Joseph Doherty f35961abea feat: add MemStore Go-parity methods & 25 new tests (Task 3)
Port Go memstore sync interface: Compact, Truncate, PurgeEx, SkipMsgs,
LoadNextMsg, NumPending, MultiLastSeqs, AllLastSeqs, SubjectsTotals,
SubjectsState, GetSeqFromTime, NextWildcardMatch, NextLiteralMatch,
MessageTTL, UpdateConfig. Fix RestoreSnapshotAsync to handle gapped
sequences, update MsgSize to include subject length + 16 overhead
(Go parity).

25 new tests ported from memstore_test.go.
2026-02-24 20:17:42 -05:00

952 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;
namespace NATS.Server.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(100);
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 void 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 (> 1 sec + check interval of 1 sec)
Thread.Sleep(2_500);
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);
}
}