Move 225 JetStream-related test files from NATS.Server.Tests into a dedicated NATS.Server.JetStream.Tests project. This includes root-level JetStream*.cs files, storage test files (FileStore, MemStore, StreamStoreContract), and the full JetStream/ subfolder tree (Api, Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams). Updated all namespaces, added InternalsVisibleTo, registered in the solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
952 lines
32 KiB
C#
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.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(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);
|
|
}
|
|
}
|