refactor: extract NATS.Server.JetStream.Tests project
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.
This commit is contained in:
@@ -0,0 +1,951 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user