// Copyright 2019-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Mirrors server/memstore_test.go using System.Text; using Shouldly; using ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; /// /// Unit tests for the in-memory JetStream message store. /// Mirrors server/memstore_test.go. /// public class JetStreamMemoryStoreTests { // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- private static JetStreamMemStore NewMemStore(StreamConfig? cfg = null) { cfg ??= new StreamConfig { Storage = StorageType.MemoryStorage, Name = "test" }; return JetStreamMemStore.NewMemStore(cfg); } private static byte[] Bytes(string s) => Encoding.UTF8.GetBytes(s); /// Computes the expected stored-message size: subj.len + hdr.len + msg.len + 16. private static ulong MsgSize(string subj, byte[]? hdr, byte[]? msg) => (ulong)(subj.Length + (hdr?.Length ?? 0) + (msg?.Length ?? 0) + 16); [Fact] public void NewMemStore_FactoryMethod_InitializesFirstSequence() { var method = typeof(JetStreamMemStore).GetMethod( "NewMemStore", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); method.ShouldNotBeNull(); var cfg = new StreamConfig { Name = "factory-init", Storage = StorageType.MemoryStorage, FirstSeq = 1000, }; var created = method!.Invoke(null, new object?[] { cfg }); created.ShouldBeOfType(); var ms = (JetStreamMemStore)created!; var state = ms.State(); state.FirstSeq.ShouldBe(1000UL); state.LastSeq.ShouldBe(999UL); ms.Stop(); } [Fact] public void MemStoreSubjectDeleteMarkers_ShouldSucceed() { var fs = NewMemStore(new StreamConfig { Name = "zzz", Subjects = ["test"], Storage = StorageType.MemoryStorage, MaxAge = TimeSpan.FromSeconds(1), AllowMsgTTL = true, SubjectDeleteMarkerTTL = TimeSpan.FromSeconds(1), }); var (seq, _) = fs.StoreMsg("test", null, Bytes("x"), 0); seq.ShouldBe(1UL); var (removed, err) = fs.RemoveMsg(seq); removed.ShouldBeTrue(); err.ShouldBeNull(); fs.State().Msgs.ShouldBe(0UL); fs.Stop(); } [Fact] public void AllLastSeqsLocked_MatchesPublicAllLastSeqsOrdering() { var cfg = new StreamConfig { Name = "locked-all-last-seqs", Subjects = new[] { "*.*" }, MaxMsgsPer = 50, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var subjs = new[] { "foo.foo", "foo.bar", "foo.baz", "bar.foo", "bar.bar", "bar.baz" }; var msg = Bytes("abc"); var rng = new Random(11); for (var i = 0; i < 1_000; i++) { var subj = subjs[rng.Next(subjs.Length)]; ms.StoreMsg(subj, null, msg, 0); } var method = typeof(JetStreamMemStore).GetMethod( "AllLastSeqsLocked", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); method.ShouldNotBeNull(); var result = method!.Invoke(ms, Array.Empty()); result.ShouldNotBeNull(); var (lockedSeqs, lockedErr) = ((ValueTuple)result!); var (publicSeqs, publicErr) = ms.AllLastSeqs(); lockedErr.ShouldBeNull(); publicErr.ShouldBeNull(); lockedSeqs.SequenceEqual(publicSeqs).ShouldBeTrue(); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreBasics (T:2023) // ----------------------------------------------------------------------- [Fact] // T:2023 public void MemStoreBasics_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreBasics line 32 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); ms.Stop(); ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var msg = Bytes("Hello World"); var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; var (seq, ts) = ms.StoreMsg(subj, null, msg, 0); seq.ShouldBe(1UL); ts.ShouldBeGreaterThanOrEqualTo(now); var state = ms.State(); state.Msgs.ShouldBe(1UL); var expectedSize = MsgSize(subj, null, msg); state.Bytes.ShouldBe(expectedSize); var sm = ms.LoadMsg(1, null); sm.ShouldNotBeNull(); sm!.Subject.ShouldBe(subj); sm.Msg.ShouldBe(msg); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreMsgLimit (T:2024) // ----------------------------------------------------------------------- [Fact] // T:2024 public void MemStoreMsgLimit_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreMsgLimit line 67 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage, MaxMsgs = 10 }); var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 10; i++) ms.StoreMsg(subj, null, msg, 0); var state = ms.State(); state.Msgs.ShouldBe(10UL); // Adding one more should still keep 10 (oldest evicted) ms.StoreMsg(subj, null, msg, 0); state = ms.State(); state.Msgs.ShouldBe(10UL); state.LastSeq.ShouldBe(11UL); state.FirstSeq.ShouldBe(2UL); // Seq 1 should not be loadable Should.Throw(() => ms.LoadMsg(1, null)); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreBytesLimit (T:2025) // ----------------------------------------------------------------------- [Fact] // T:2025 public void MemStoreBytesLimit_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreBytesLimit line 99 var subj = "foo"; var msg = new byte[512]; var storedMsgSize = MsgSize(subj, null, msg); var toStore = 1024UL; var maxBytes = storedMsgSize * toStore; var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage, MaxBytes = (long)maxBytes }); for (var i = 0UL; i < toStore; i++) ms.StoreMsg(subj, null, msg, 0); var state = ms.State(); state.Msgs.ShouldBe(toStore); state.Bytes.ShouldBe(storedMsgSize * toStore); // Now send 10 more and check that bytes limit enforced for (var i = 0; i < 10; i++) ms.StoreMsg(subj, null, msg, 0); state = ms.State(); state.Msgs.ShouldBe(toStore); state.Bytes.ShouldBe(storedMsgSize * toStore); state.FirstSeq.ShouldBe(11UL); state.LastSeq.ShouldBe(toStore + 10UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreBytesLimitWithDiscardNew (T:2026) // ----------------------------------------------------------------------- [Fact] // T:2026 public void MemStoreBytesLimitWithDiscardNew_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreBytesLimitWithDiscardNew line 143 var subj = "tiny"; var msg = new byte[7]; var storedMsgSize = MsgSize(subj, null, msg); var toStore = 3UL; var maxBytes = 100; var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage, MaxBytes = maxBytes, Discard = DiscardPolicy.DiscardNew }); // Send 10 messages; first toStore succeed, the rest hit ErrMaxBytes for (var i = 0; i < 10; i++) { var (seq, _) = ms.StoreMsg(subj, null, msg, 0); if (i < (int)toStore) seq.ShouldNotBe(0UL); // success // After toStore, StoreMsg returns (0, 0) on error } var state = ms.State(); state.Msgs.ShouldBe(toStore); state.Bytes.ShouldBe(storedMsgSize * toStore); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreAgeLimit (T:2027) // ----------------------------------------------------------------------- [Fact] // T:2027 public async Task MemStoreAgeLimit_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreAgeLimit line 174 var maxAge = TimeSpan.FromMilliseconds(100); var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage, MaxAge = maxAge }); var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 100; i++) ms.StoreMsg(subj, null, msg, 0); var state = ms.State(); state.Msgs.ShouldBe(100UL); // Wait for messages to expire await Task.Delay(TimeSpan.FromMilliseconds(500)); // Check messages expired var deadline = DateTime.UtcNow.AddSeconds(2); while (DateTime.UtcNow < deadline) { state = ms.State(); if (state.Msgs == 0 && state.Bytes == 0) break; await Task.Delay(10); } state = ms.State(); state.Msgs.ShouldBe(0UL); state.Bytes.ShouldBe(0UL); // Now add some more and make sure that timer will fire again for (var i = 0; i < 100; i++) ms.StoreMsg(subj, null, msg, 0); state = ms.State(); state.Msgs.ShouldBe(100UL); await Task.Delay(TimeSpan.FromMilliseconds(500)); deadline = DateTime.UtcNow.AddSeconds(2); while (DateTime.UtcNow < deadline) { state = ms.State(); if (state.Msgs == 0 && state.Bytes == 0) break; await Task.Delay(10); } state = ms.State(); state.Msgs.ShouldBe(0UL); state.Bytes.ShouldBe(0UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreTimeStamps (T:2028) // ----------------------------------------------------------------------- [Fact] // T:2028 public async Task MemStoreTimeStamps_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreTimeStamps line 216 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var last = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 10; i++) { await Task.Delay(5); // small delay to ensure distinct timestamps ms.StoreMsg(subj, null, msg, 0); } var smv = new StoreMsg(); for (var seq = 1UL; seq <= 10UL; seq++) { var sm = ms.LoadMsg(seq, smv); sm.ShouldNotBeNull(); sm!.Ts.ShouldBeGreaterThan(last); last = sm.Ts; } ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStorePurge (T:2029) // ----------------------------------------------------------------------- [Fact] // T:2029 public void MemStorePurge_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStorePurge line 241 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 10; i++) ms.StoreMsg(subj, null, msg, 0); ms.State().Msgs.ShouldBe(10UL); ms.Purge(); ms.State().Msgs.ShouldBe(0UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreCompact (T:2030) // ----------------------------------------------------------------------- [Fact] // T:2030 public void MemStoreCompact_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreCompact line 259 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 10; i++) ms.StoreMsg(subj, null, msg, 0); ms.State().Msgs.ShouldBe(10UL); var (n, err) = ms.Compact(6); err.ShouldBeNull(); n.ShouldBe(5UL); var state = ms.State(); state.Msgs.ShouldBe(5UL); state.FirstSeq.ShouldBe(6UL); // Compact past end should purge (n, err) = ms.Compact(100); err.ShouldBeNull(); n.ShouldBe(5UL); state = ms.State(); state.FirstSeq.ShouldBe(100UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreEraseMsg (T:2031) // ----------------------------------------------------------------------- [Fact] // T:2031 public void MemStoreEraseMsg_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreEraseMsg line 298 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var msg = Bytes("Hello World"); ms.StoreMsg(subj, null, msg, 0); var sm = ms.LoadMsg(1, null); sm.ShouldNotBeNull(); sm!.Msg.ShouldBe(msg); var (removed, _) = ms.EraseMsg(1); removed.ShouldBeTrue(); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreMsgHeaders (T:2032) // ----------------------------------------------------------------------- [Fact] // T:2032 public void MemStoreMsgHeaders_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreMsgHeaders line 317 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var hdr = Bytes("name:derek"); var msg = Bytes("Hello World"); // Verify computed size var sz = (int)MsgSize(subj, hdr, msg); sz.ShouldBe(subj.Length + hdr.Length + msg.Length + 16); ms.StoreMsg(subj, hdr, msg, 0); var sm = ms.LoadMsg(1, null); sm.ShouldNotBeNull(); sm!.Msg.ShouldBe(msg); sm.Hdr.ShouldBe(hdr); var (removed, _) = ms.EraseMsg(1); removed.ShouldBeTrue(); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreStreamStateDeleted (T:2033) // ----------------------------------------------------------------------- [Fact] // T:2033 public void MemStoreStreamStateDeleted_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreStreamStateDeleted line 342 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var subj = "foo"; var toStore = 10UL; for (var i = 1UL; i <= toStore; i++) { var m = Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!"); ms.StoreMsg(subj, null, m, 0); } var state = ms.State(); state.Deleted.ShouldBeNull(); // Remove some interior messages (even seqs: 2,4,6,8) var expected = new List(); for (var seq = 2UL; seq < toStore; seq += 2) { ms.RemoveMsg(seq); expected.Add(seq); } state = ms.State(); state.Deleted.ShouldNotBeNull(); state.Deleted!.ShouldBe(expected.ToArray()); // Now fill the gap by deleting 1 and 3 ms.RemoveMsg(1); ms.RemoveMsg(3); expected.RemoveRange(0, 2); // remove 2 and 4 (the first two) state = ms.State(); state.Deleted!.ShouldBe(expected.ToArray()); state.FirstSeq.ShouldBe(5UL); ms.Purge(); state = ms.State(); (state.Deleted?.Length ?? 0).ShouldBe(0); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreStreamTruncate (T:2034) // ----------------------------------------------------------------------- [Fact] // T:2034 public void MemStoreStreamTruncate_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreStreamTruncate line 385 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); var tseq = 50UL; var toStore = 100UL; for (var i = 1UL; i < tseq; i++) ms.StoreMsg("foo", null, Bytes("ok"), 0); for (var i = tseq; i <= toStore; i++) ms.StoreMsg("bar", null, Bytes("ok"), 0); ms.State().Msgs.ShouldBe(toStore); ms.Truncate(tseq); ms.State().Msgs.ShouldBe(tseq); // Now test with interior deletes ms.RemoveMsg(10); ms.RemoveMsg(20); ms.RemoveMsg(30); ms.RemoveMsg(40); tseq = 25UL; ms.Truncate(tseq); var state = ms.State(); state.Msgs.ShouldBe(tseq - 2); // 10 and 20 deleted state.NumSubjects.ShouldBe(1); state.Deleted.ShouldBe(new ulong[] { 10, 20 }); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStorePurgeExWithSubject (T:2035) // ----------------------------------------------------------------------- [Fact] // T:2035 public void MemStorePurgeExWithSubject_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStorePurgeExWithSubject line 437 var ms = NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage }); for (var i = 0; i < 100; i++) ms.StoreMsg("foo", null, null, 0); // Purge all with subject "foo" ms.PurgeEx("foo", 1, 0); ms.State().Msgs.ShouldBe(0UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreUpdateMaxMsgsPerSubject (T:2036) // ----------------------------------------------------------------------- [Fact] // T:2036 public void MemStoreUpdateMaxMsgsPerSubject_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreUpdateMaxMsgsPerSubject line 452 var cfg = new StreamConfig { Name = "TEST", Storage = StorageType.MemoryStorage, Subjects = new[] { "foo" }, MaxMsgsPer = 10, }; var ms = NewMemStore(cfg); // Increase limit cfg.MaxMsgsPer = 50; ms.UpdateConfig(cfg); var numStored = 22; for (var i = 0; i < numStored; i++) ms.StoreMsg("foo", null, null, 0); var ss = ms.SubjectsState("foo")["foo"]; ss.Msgs.ShouldBe((ulong)numStored); // Decrease limit to 10 — should trim to 10 cfg.MaxMsgsPer = 10; ms.UpdateConfig(cfg); ss = ms.SubjectsState("foo")["foo"]; ss.Msgs.ShouldBe(10UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreStreamTruncateReset (T:2037) // ----------------------------------------------------------------------- [Fact] // T:2037 public void MemStoreStreamTruncateReset_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreStreamTruncateReset line 490 var cfg = new StreamConfig { Name = "TEST", Storage = StorageType.MemoryStorage, Subjects = new[] { "foo" }, }; var ms = NewMemStore(cfg); var subj = "foo"; var msg = Bytes("Hello World"); for (var i = 0; i < 1000; i++) ms.StoreMsg(subj, null, msg, 0); // Reset everything via Truncate(0) ms.Truncate(0); var state = ms.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); // Store 1000 more after reset for (var i = 0; i < 1000; i++) ms.StoreMsg(subj, null, msg, 0); state = ms.State(); state.Msgs.ShouldBe(1000UL); state.FirstSeq.ShouldBe(1UL); state.LastSeq.ShouldBe(1000UL); state.NumSubjects.ShouldBe(1); state.NumDeleted.ShouldBe(0); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreStreamCompactMultiBlockSubjectInfo (T:2038) // ----------------------------------------------------------------------- [Fact] // T:2038 public void MemStoreStreamCompactMultiBlockSubjectInfo_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreStreamCompactMultiBlockSubjectInfo line 531 var cfg = new StreamConfig { Name = "TEST", Storage = StorageType.MemoryStorage, Subjects = new[] { "foo.*" }, }; var ms = NewMemStore(cfg); for (var i = 0; i < 1000; i++) ms.StoreMsg($"foo.{i}", null, Bytes("Hello World"), 0); var (deleted, err) = ms.Compact(501); err.ShouldBeNull(); deleted.ShouldBe(500UL); var state = ms.State(); state.NumSubjects.ShouldBe(500); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreSubjectsTotals (T:2039) // ----------------------------------------------------------------------- [Fact] // T:2039 public void MemStoreSubjectsTotals_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreSubjectsTotals line 557 var cfg = new StreamConfig { Name = "TEST", Storage = StorageType.MemoryStorage, Subjects = new[] { "*.*" }, }; var ms = NewMemStore(cfg); var rng = new Random(42); var fmap = new Dictionary(); var bmap = new Dictionary(); for (var i = 0; i < 10_000; i++) { string ft; Dictionary 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[dt] = m.GetValueOrDefault(dt) + 1; ms.StoreMsg(subj, null, Bytes("Hello World"), 0); } // Check each individual subject in fmap foreach (var (dt, total) in fmap) { var subj = $"foo.{dt}"; var m = ms.SubjectsTotals(subj); m[subj].ShouldBe((ulong)total); } // Check fmap wildcard totals var st = ms.SubjectsTotals("foo.*"); st.Count.ShouldBe(fmap.Count); var expectedTotal = fmap.Values.Sum(); var receivedTotal = st.Values.Select(v => (long)v).Sum(); receivedTotal.ShouldBe(expectedTotal); // Check bmap wildcard totals st = ms.SubjectsTotals("bar.*"); st.Count.ShouldBe(bmap.Count); expectedTotal = bmap.Values.Sum(); receivedTotal = st.Values.Select(v => (long)v).Sum(); receivedTotal.ShouldBe(expectedTotal); // All with pwc match st = ms.SubjectsTotals("*.*"); st.Count.ShouldBe(bmap.Count + fmap.Count); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreNumPending (T:2040) // ----------------------------------------------------------------------- [Fact] // T:2040 public void MemStoreNumPending_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreNumPending line 637 var cfg = new StreamConfig { Name = "TEST", Storage = StorageType.MemoryStorage, Subjects = new[] { "*.*.*.*" }, }; var ms = NewMemStore(cfg); var tokens = new[] { "foo", "bar", "baz" }; var rng = new Random(99); string GenSubj() => $"{tokens[rng.Next(tokens.Length)]}.{tokens[rng.Next(tokens.Length)]}.{tokens[rng.Next(tokens.Length)]}.{tokens[rng.Next(tokens.Length)]}"; for (var i = 0; i < 5_000; i++) { var subj = GenSubj(); ms.StoreMsg(subj, null, Bytes("Hello World"), 0); } var state = ms.State(); // Scan one by one for sanity check SimpleState SanityCheck(ulong sseq, string filter) { var ss = new SimpleState(); var smv = new StoreMsg(); if (sseq == 0) sseq = 1; for (var seq = sseq; seq <= state.LastSeq; seq++) { try { var sm = ms.LoadMsg(seq, smv); if (sm != null && SubjectIsSubsetMatch(sm.Subject, filter)) { ss.Msgs++; ss.Last = seq; if (ss.First == 0 || seq < ss.First) ss.First = seq; } } catch { /* skip deleted */ } } return ss; } void Check(ulong sseq, string filter) { var (np, lvs, err) = ms.NumPending(sseq, filter, false); err.ShouldBeNull(); lvs.ShouldBe(state.LastSeq); var ss = ms.FilteredState(sseq, filter); var sss = SanityCheck(sseq, filter); ss.Msgs.ShouldBe(np); ss.Msgs.ShouldBe(sss.Msgs); } var startSeqs = new ulong[] { 0, 1, 2, 200, 444, 555, 2222 }; var checkSubs = new[] { "foo.>", "*.bar.>", "foo.bar.*.baz" }; foreach (var filter in checkSubs) foreach (var start in startSeqs) Check(start, filter); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreInitialFirstSeq (T:2041) // ----------------------------------------------------------------------- [Fact] // T:2041 public void MemStoreInitialFirstSeq_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreInitialFirstSeq line 765 var cfg = new StreamConfig { Name = "zzz", Storage = StorageType.MemoryStorage, FirstSeq = 1000, }; var ms = NewMemStore(cfg); var (seq, _, err) = (ms.StoreMsg("A", null, Bytes("OK"), 0).Seq, 0L, (Exception?)null); seq.ShouldBe(1000UL); var (seq2, _) = ms.StoreMsg("B", null, Bytes("OK"), 0); seq2.ShouldBe(1001UL); var state = new StreamState(); ms.FastState(state); state.Msgs.ShouldBe(2UL); state.FirstSeq.ShouldBe(1000UL); state.LastSeq.ShouldBe(1001UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreGetSeqFromTimeWithLastDeleted (T:2043) // ----------------------------------------------------------------------- [Fact] // T:2043 public async Task MemStoreGetSeqFromTimeWithLastDeleted_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreGetSeqFromTimeWithLastDeleted line 839 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "*" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var total = 100; DateTime st = default; for (var i = 1; i <= total; i++) { ms.StoreMsg("A", null, Bytes("OK"), 0); if (i == total / 2) { await Task.Delay(50); st = DateTime.UtcNow; } } // Delete last 10 for (var seq = total - 10; seq <= total; seq++) ms.RemoveMsg((ulong)seq); // Make sure this does not panic with last sequence no longer accessible var gotSeq = ms.GetSeqFromTime(st); // Should return around 51 (the halfway point) gotSeq.ShouldBeGreaterThanOrEqualTo(49UL); gotSeq.ShouldBeLessThanOrEqualTo(53UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreSkipMsgs (T:2044) // ----------------------------------------------------------------------- [Fact] // T:2044 public void MemStoreSkipMsgs_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreSkipMsgs line 871 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "*" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); // Wrong starting sequence must fail Should.Throw(() => ms.SkipMsgs(10, 100)); ms.SkipMsgs(1, 100); var state = ms.State(); state.FirstSeq.ShouldBe(101UL); state.LastSeq.ShouldBe(100UL); // Now skip a large amount ms.SkipMsgs(101, 100_000); state = ms.State(); state.FirstSeq.ShouldBe(100_101UL); state.LastSeq.ShouldBe(100_100UL); // New store: add a message then skip ms = NewMemStore(cfg); ms.StoreMsg("foo", null, null, 0); ms.SkipMsgs(2, 10); state = ms.State(); state.FirstSeq.ShouldBe(1UL); state.LastSeq.ShouldBe(11UL); state.Msgs.ShouldBe(1UL); state.NumDeleted.ShouldBe(10); state.Deleted!.Length.ShouldBe(10); // Check FastState state.Deleted = null; ms.FastState(state); state.FirstSeq.ShouldBe(1UL); state.LastSeq.ShouldBe(11UL); state.Msgs.ShouldBe(1UL); state.NumDeleted.ShouldBe(10); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreMultiLastSeqs (T:2045) // ----------------------------------------------------------------------- [Fact] // T:2045 public void MemStoreMultiLastSeqs_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreMultiLastSeqs line 923 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "foo.*", "bar.*" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var msg = Bytes("abc"); for (var i = 0; i < 33; i++) { ms.StoreMsg("foo.foo", null, msg, 0); ms.StoreMsg("foo.bar", null, msg, 0); ms.StoreMsg("foo.baz", null, msg, 0); } for (var i = 0; i < 33; i++) { ms.StoreMsg("bar.foo", null, msg, 0); ms.StoreMsg("bar.bar", null, msg, 0); ms.StoreMsg("bar.baz", null, msg, 0); } void CheckResults(ulong[] seqs, ulong[] expected) { seqs.Length.ShouldBe(expected.Length); for (var i = 0; i < seqs.Length; i++) seqs[i].ShouldBe(expected[i]); } // UpTo sequence 3 var (seqs, err) = ms.MultiLastSeqs(new[] { "foo.*" }, 3, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 1, 2, 3 }); // Up to last sequence of the stream (seqs, err) = ms.MultiLastSeqs(new[] { "foo.*" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 97, 98, 99 }); // Check for bar.* at the end (seqs, err) = ms.MultiLastSeqs(new[] { "bar.*" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 196, 197, 198 }); // This should find nothing (seqs, err) = ms.MultiLastSeqs(new[] { "bar.*" }, 99, -1); err.ShouldBeNull(); CheckResults(seqs!, Array.Empty()); // Explicit subjects (seqs, err) = ms.MultiLastSeqs(new[] { "foo.foo", "foo.bar", "foo.baz" }, 3, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 1, 2, 3 }); (seqs, err) = ms.MultiLastSeqs(new[] { "foo.foo", "foo.bar", "foo.baz" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 97, 98, 99 }); (seqs, err) = ms.MultiLastSeqs(new[] { "bar.foo", "bar.bar", "bar.baz" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 196, 197, 198 }); (seqs, err) = ms.MultiLastSeqs(new[] { "bar.foo", "bar.bar", "bar.baz" }, 99, -1); err.ShouldBeNull(); CheckResults(seqs!, Array.Empty()); // Single subject (seqs, err) = ms.MultiLastSeqs(new[] { "foo.foo" }, 3, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 1 }); // De-duplicate (seqs, err) = ms.MultiLastSeqs(new[] { "foo.*", "foo.bar" }, 3, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 1, 2, 3 }); (seqs, err) = ms.MultiLastSeqs(new[] { "bar.>", "bar.bar", "bar.baz" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 196, 197, 198 }); // All (seqs, err) = ms.MultiLastSeqs(new[] { ">" }, 0, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 97, 98, 99, 196, 197, 198 }); (seqs, err) = ms.MultiLastSeqs(new[] { ">" }, 99, -1); err.ShouldBeNull(); CheckResults(seqs!, new ulong[] { 97, 98, 99 }); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreMultiLastSeqsMaxAllowed (T:2046) // ----------------------------------------------------------------------- [Fact] // T:2046 public void MemStoreMultiLastSeqsMaxAllowed_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreMultiLastSeqsMaxAllowed line 1010 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "foo.*" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var msg = Bytes("abc"); for (var i = 1; i <= 100; i++) ms.StoreMsg($"foo.{i}", null, msg, 0); var (seqs, err) = ms.MultiLastSeqs(new[] { "foo.*" }, 0, 10); seqs.ShouldBeNull(); err.ShouldNotBeNull(); err!.Message.ShouldContain("too many"); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStorePurgeExWithDeletedMsgs (T:2047) // ----------------------------------------------------------------------- [Fact] // T:2047 public void MemStorePurgeExWithDeletedMsgs_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStorePurgeExWithDeletedMsgs line 1031 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "foo" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var msg = Bytes("abc"); for (var i = 1; i <= 10; i++) ms.StoreMsg("foo", null, msg, 0); ms.RemoveMsg(2); ms.RemoveMsg(9); // This was the bug var (n, err) = ms.PurgeEx(string.Empty, 9, 0); err.ShouldBeNull(); n.ShouldBe(7UL); var state = new StreamState(); ms.FastState(state); state.FirstSeq.ShouldBe(10UL); state.LastSeq.ShouldBe(10UL); state.Msgs.ShouldBe(1UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreDeleteAllFirstSequenceCheck (T:2048) // ----------------------------------------------------------------------- [Fact] // T:2048 public void MemStoreDeleteAllFirstSequenceCheck_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreDeleteAllFirstSequenceCheck line 1060 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "foo" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var msg = Bytes("abc"); for (var i = 1; i <= 10; i++) ms.StoreMsg("foo", null, msg, 0); for (var seq = 1UL; seq <= 10UL; seq++) ms.RemoveMsg(seq); var state = new StreamState(); ms.FastState(state); state.FirstSeq.ShouldBe(11UL); state.LastSeq.ShouldBe(10UL); state.Msgs.ShouldBe(0UL); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreAllLastSeqs (T:2054) // ----------------------------------------------------------------------- [Fact] // T:2054 public void MemStoreAllLastSeqs_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreAllLastSeqs line 1266 var cfg = new StreamConfig { Name = "zzz", Subjects = new[] { "*.*" }, MaxMsgsPer = 50, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var subjs = new[] { "foo.foo", "foo.bar", "foo.baz", "bar.foo", "bar.bar", "bar.baz" }; var msg = Bytes("abc"); var rng = new Random(7); for (var i = 0; i < 10_000; i++) { var subj = subjs[rng.Next(subjs.Length)]; ms.StoreMsg(subj, null, msg, 0); } var expected = new List(); var smv = new StoreMsg(); foreach (var subj in subjs) { var sm = ms.LoadLastMsg(subj, smv); sm.ShouldNotBeNull(); expected.Add(sm!.Seq); } expected.Sort(); var (seqs, err) = ms.AllLastSeqs(); err.ShouldBeNull(); seqs!.SequenceEqual(expected).ShouldBeTrue(); ms.Stop(); } // ----------------------------------------------------------------------- // TestMemStoreSubjectForSeq (T:2056) // ----------------------------------------------------------------------- [Fact] // T:2056 public void MemStoreSubjectForSeq_ShouldSucceed() { // Reference: golang/nats-server/server/memstore_test.go:TestMemStoreSubjectForSeq line 1319 var cfg = new StreamConfig { Name = "foo", Subjects = new[] { "foo.>" }, Storage = StorageType.MemoryStorage, }; var ms = NewMemStore(cfg); var (seq, _) = ms.StoreMsg("foo.bar", null, null, 0); seq.ShouldBe(1UL); var (subj, err) = ms.SubjectForSeq(0); err.ShouldNotBeNull(); (subj, err) = ms.SubjectForSeq(1); err.ShouldBeNull(); subj.ShouldBe("foo.bar"); (subj, err) = ms.SubjectForSeq(2); err.ShouldNotBeNull(); ms.Stop(); } // ----------------------------------------------------------------------- // Private helper: subject subset matching (mirrors Go subjectIsSubsetMatch) // ----------------------------------------------------------------------- private static bool SubjectIsSubsetMatch(string subject, string filter) { var sParts = subject.Split('.'); var fParts = filter.Split('.'); var si = 0; for (var fi = 0; fi < fParts.Length; fi++) { if (fParts[fi] == ">") return si < sParts.Length; if (si >= sParts.Length) return false; if (fParts[fi] != "*" && fParts[fi] != sParts[si]) return false; si++; } return si == sParts.Length; } }