feat(p7-06): port memstore & store interface tests (38 tests)
Add JetStreamMemoryStoreTests (27 tests, T:2023-2056) and StorageEngineTests (11 tests, T:2943-2957) covering the JetStream memory store and IStreamStore interface. Fix 10 bugs in MemStore.cs discovered during test authoring: FirstSeq constructor, Truncate(0) SubjectTree reset, PurgeEx subject-filtered implementation, UpdateConfig MaxMsgsPer enforcement, FilteredStateLocked partial range scan, StoreRawMsgLocked DiscardNewPer, MultiLastSeqs maxSeq fallback scan + LastNeedsUpdate recalculation, AllLastSeqs LastNeedsUpdate recalculation, LoadLastLocked LazySubjectState recalculation, GetSeqFromTime ts==last equality, and timestamp precision (100-ns throughout). 20 tests deferred (internal fields, benchmarks, TTL, filestore-only). All 701 unit tests pass.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,575 @@
|
||||
// Copyright 2012-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/store_test.go (MemStore permutation only; file store permutations deferred).
|
||||
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for IStreamStore contract, exercised against JetStreamMemStore.
|
||||
/// Mirrors server/store_test.go (memory permutations only).
|
||||
/// File-store-specific and infrastructure-dependent tests are marked deferred.
|
||||
/// </summary>
|
||||
public class StorageEngineTests
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private static JetStreamMemStore NewMemStore(StreamConfig cfg)
|
||||
{
|
||||
cfg.Storage = StorageType.MemoryStorage;
|
||||
return new JetStreamMemStore(cfg);
|
||||
}
|
||||
|
||||
private static byte[] Bytes(string s) => Encoding.UTF8.GetBytes(s);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreDeleteSlice (T:2943)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2943
|
||||
public void StoreDeleteSlice_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreDeleteSlice line 147
|
||||
var ds = new DeleteSlice(new ulong[] { 2 });
|
||||
var deletes = new List<ulong>();
|
||||
ds.Range(seq => { deletes.Add(seq); return true; });
|
||||
deletes.Count.ShouldBe(1);
|
||||
deletes[0].ShouldBe(2UL);
|
||||
|
||||
var (first, last, num) = ds.GetState();
|
||||
first.ShouldBe(2UL);
|
||||
last.ShouldBe(2UL);
|
||||
num.ShouldBe(1UL);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreDeleteRange (T:2944)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2944
|
||||
public void StoreDeleteRange_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreDeleteRange line 163
|
||||
var dr = new DeleteRange { First = 2, Num = 1 };
|
||||
var deletes = new List<ulong>();
|
||||
dr.Range(seq => { deletes.Add(seq); return true; });
|
||||
deletes.Count.ShouldBe(1);
|
||||
deletes[0].ShouldBe(2UL);
|
||||
|
||||
var (first, last, num) = dr.GetState();
|
||||
first.ShouldBe(2UL);
|
||||
last.ShouldBe(2UL);
|
||||
num.ShouldBe(1UL);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreSubjectStateConsistency (T:2945) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2945
|
||||
public void StoreSubjectStateConsistency_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreSubjectStateConsistency line 179
|
||||
var fs = NewMemStore(new StreamConfig { Name = "TEST", Subjects = new[] { "foo" } });
|
||||
|
||||
SimpleState GetSubjectState()
|
||||
{
|
||||
var ss = fs.SubjectsState("foo");
|
||||
ss.TryGetValue("foo", out var result);
|
||||
return result ?? new SimpleState();
|
||||
}
|
||||
|
||||
var smp = new StoreMsg();
|
||||
|
||||
ulong ExpectFirstSeq()
|
||||
{
|
||||
var (sm, _, err) = fs.LoadNextMsg("foo", false, 0, smp).Sm?.Seq is ulong s
|
||||
? (smp, s, (Exception?)null)
|
||||
: (null, 0UL, StoreErrors.ErrStoreMsgNotFound);
|
||||
var (smr, skip) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
smr.ShouldNotBeNull();
|
||||
return skip;
|
||||
}
|
||||
|
||||
ulong ExpectLastSeq()
|
||||
{
|
||||
var sm = fs.LoadLastMsg("foo", smp);
|
||||
sm.ShouldNotBeNull();
|
||||
return sm!.Seq;
|
||||
}
|
||||
|
||||
// Publish 4 messages
|
||||
for (var i = 0; i < 4; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
var ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(4UL);
|
||||
ss.First.ShouldBe(1UL);
|
||||
ss.Last.ShouldBe(4UL);
|
||||
|
||||
// Verify first/last via LoadNextMsg / LoadLastMsg
|
||||
var (firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(1UL);
|
||||
var lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(4UL);
|
||||
|
||||
// Remove first message
|
||||
var (removed, _) = fs.RemoveMsg(1);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(3UL);
|
||||
ss.First.ShouldBe(2UL);
|
||||
ss.Last.ShouldBe(4UL);
|
||||
|
||||
(firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(2UL);
|
||||
lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(4UL);
|
||||
|
||||
// Remove last message
|
||||
(removed, _) = fs.RemoveMsg(4);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(2UL);
|
||||
ss.First.ShouldBe(2UL);
|
||||
ss.Last.ShouldBe(3UL);
|
||||
|
||||
(firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(2UL);
|
||||
lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(3UL);
|
||||
|
||||
// Remove seq 2
|
||||
(removed, _) = fs.RemoveMsg(2);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(1UL);
|
||||
ss.First.ShouldBe(3UL);
|
||||
ss.Last.ShouldBe(3UL);
|
||||
|
||||
(firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(3UL);
|
||||
lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(3UL);
|
||||
|
||||
// Publish 3 more
|
||||
for (var i = 0; i < 3; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(4UL);
|
||||
ss.First.ShouldBe(3UL);
|
||||
ss.Last.ShouldBe(7UL);
|
||||
|
||||
// Remove seq 7 and seq 3
|
||||
(removed, _) = fs.RemoveMsg(7);
|
||||
removed.ShouldBeTrue();
|
||||
(removed, _) = fs.RemoveMsg(3);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
// Remove seq 5 (the now-first)
|
||||
(removed, _) = fs.RemoveMsg(5);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(1UL);
|
||||
ss.First.ShouldBe(6UL);
|
||||
ss.Last.ShouldBe(6UL);
|
||||
|
||||
(firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(6UL);
|
||||
lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(6UL);
|
||||
|
||||
// Store + immediately remove seq 8, then store seq 9
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
(removed, _) = fs.RemoveMsg(8);
|
||||
removed.ShouldBeTrue();
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
ss = GetSubjectState();
|
||||
ss.Msgs.ShouldBe(2UL);
|
||||
ss.First.ShouldBe(6UL);
|
||||
ss.Last.ShouldBe(9UL);
|
||||
|
||||
(firstSm, firstSeq) = fs.LoadNextMsg("foo", false, 0, smp);
|
||||
firstSm.ShouldNotBeNull();
|
||||
firstSeq.ShouldBe(6UL);
|
||||
lastSm = fs.LoadLastMsg("foo", smp);
|
||||
lastSm!.Seq.ShouldBe(9UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreMaxMsgsPerUpdateBug (T:2947) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2947
|
||||
public void StoreMaxMsgsPerUpdateBug_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreMaxMsgsPerUpdateBug line 405
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "TEST",
|
||||
Subjects = new[] { "foo" },
|
||||
MaxMsgsPer = 0,
|
||||
};
|
||||
var fs = NewMemStore(cfg);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
var ss = fs.State();
|
||||
ss.Msgs.ShouldBe(5UL);
|
||||
ss.FirstSeq.ShouldBe(1UL);
|
||||
ss.LastSeq.ShouldBe(5UL);
|
||||
|
||||
// Update max messages per-subject from 0 (infinite) to 1
|
||||
cfg.MaxMsgsPer = 1;
|
||||
fs.UpdateConfig(cfg);
|
||||
|
||||
// Only one message should remain
|
||||
ss = fs.State();
|
||||
ss.Msgs.ShouldBe(1UL);
|
||||
ss.FirstSeq.ShouldBe(5UL);
|
||||
ss.LastSeq.ShouldBe(5UL);
|
||||
|
||||
// Update to invalid value (< -1) — should clamp to -1
|
||||
cfg.MaxMsgsPer = -2;
|
||||
fs.UpdateConfig(cfg);
|
||||
cfg.MaxMsgsPer.ShouldBe(-1L);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreCompactCleansUpDmap (T:2948) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2948
|
||||
public void StoreCompactCleansUpDmap_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreCompactCleansUpDmap line 449
|
||||
// We run for compact sequences 2, 3, 4
|
||||
for (var cseq = 2UL; cseq <= 4UL; cseq++)
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "TEST",
|
||||
Subjects = new[] { "foo" },
|
||||
MaxMsgsPer = 0,
|
||||
};
|
||||
var fs = NewMemStore(cfg);
|
||||
|
||||
// Publish 3 messages; no interior deletes
|
||||
for (var i = 0; i < 3; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
// Remove one message in the middle = interior delete
|
||||
var (removed, _) = fs.RemoveMsg(2);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
// The dmap should have 1 entry (seq 2) — verify via State().NumDeleted
|
||||
var state = fs.State();
|
||||
state.NumDeleted.ShouldBe(1);
|
||||
|
||||
// Compact — must clean up the interior delete
|
||||
var (_, err) = fs.Compact(cseq);
|
||||
err.ShouldBeNull();
|
||||
|
||||
// After compaction, no deleted entries in the range
|
||||
state = fs.State();
|
||||
state.NumDeleted.ShouldBe(0);
|
||||
|
||||
// Validate first/last sequence
|
||||
var expectedFirst = Math.Max(3UL, cseq);
|
||||
state.FirstSeq.ShouldBe(expectedFirst);
|
||||
state.LastSeq.ShouldBe(3UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreTruncateCleansUpDmap (T:2949) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2949
|
||||
public void StoreTruncateCleansUpDmap_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreTruncateCleansUpDmap line 500
|
||||
// We run for truncate sequences 0 and 1
|
||||
for (var tseq = 0UL; tseq <= 1UL; tseq++)
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "TEST",
|
||||
Subjects = new[] { "foo" },
|
||||
MaxMsgsPer = 0,
|
||||
};
|
||||
var fs = NewMemStore(cfg);
|
||||
|
||||
// Publish 3 messages
|
||||
for (var i = 0; i < 3; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
// Remove middle message = interior delete
|
||||
var (removed, _) = fs.RemoveMsg(2);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
var state = fs.State();
|
||||
state.NumDeleted.ShouldBe(1);
|
||||
|
||||
// Truncate
|
||||
fs.Truncate(tseq);
|
||||
|
||||
state = fs.State();
|
||||
state.NumDeleted.ShouldBe(0);
|
||||
|
||||
// Validate first/last sequence
|
||||
var expectedFirst = Math.Min(1UL, tseq);
|
||||
state.FirstSeq.ShouldBe(expectedFirst);
|
||||
state.LastSeq.ShouldBe(tseq);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStorePurgeExZero (T:2950) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2950
|
||||
public void StorePurgeExZero_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStorePurgeExZero line 552
|
||||
var fs = NewMemStore(new StreamConfig { Name = "TEST", Subjects = new[] { "foo" } });
|
||||
|
||||
// Simple purge all
|
||||
var (_, err) = fs.Purge();
|
||||
err.ShouldBeNull();
|
||||
|
||||
var ss = fs.State();
|
||||
ss.FirstSeq.ShouldBe(1UL);
|
||||
ss.LastSeq.ShouldBe(0UL);
|
||||
|
||||
// PurgeEx(seq=0) must equal Purge()
|
||||
(_, err) = fs.PurgeEx(string.Empty, 0, 0);
|
||||
err.ShouldBeNull();
|
||||
|
||||
ss = fs.State();
|
||||
ss.FirstSeq.ShouldBe(1UL);
|
||||
ss.LastSeq.ShouldBe(0UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreGetSeqFromTimeWithInteriorDeletesGap (T:2955) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2955
|
||||
public void StoreGetSeqFromTimeWithInteriorDeletesGap_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreGetSeqFromTimeWithInteriorDeletesGap line 874
|
||||
// Go: start = ts from StoreMsg at i==1; ts := time.Unix(0, start).UTC()
|
||||
// .NET: convert the 100-ns store timestamp directly to DateTime (same precision).
|
||||
var fs = NewMemStore(new StreamConfig { Name = "zzz", Subjects = new[] { "foo" } });
|
||||
|
||||
long start = 0;
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var (_, ts) = fs.StoreMsg("foo", null, null, 0);
|
||||
if (i == 1)
|
||||
start = ts; // exact timestamp of seq 2
|
||||
}
|
||||
|
||||
// Create a delete gap at seqs 4-7
|
||||
for (var seq = 4UL; seq <= 7UL; seq++)
|
||||
fs.RemoveMsg(seq);
|
||||
|
||||
// Convert 100-ns-since-epoch to DateTime (mirrors Go's time.Unix(0, start))
|
||||
const long UnixEpochTicks = 621355968000000000L;
|
||||
var t = new DateTime(start / 100L + UnixEpochTicks, DateTimeKind.Utc);
|
||||
|
||||
var gotSeq = fs.GetSeqFromTime(t);
|
||||
gotSeq.ShouldBe(2UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreGetSeqFromTimeWithTrailingDeletes (T:2956) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2956
|
||||
public void StoreGetSeqFromTimeWithTrailingDeletes_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreGetSeqFromTimeWithTrailingDeletes line 900
|
||||
// Go: start = ts from StoreMsg at i==1; ts := time.Unix(0, start).UTC()
|
||||
// .NET: convert the 100-ns store timestamp directly to DateTime (same precision).
|
||||
var fs = NewMemStore(new StreamConfig { Name = "zzz", Subjects = new[] { "foo" } });
|
||||
|
||||
long start = 0;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var (_, ts) = fs.StoreMsg("foo", null, null, 0);
|
||||
if (i == 1)
|
||||
start = ts; // exact timestamp of seq 2
|
||||
}
|
||||
|
||||
fs.RemoveMsg(3);
|
||||
|
||||
// Convert 100-ns-since-epoch to DateTime (mirrors Go's time.Unix(0, start))
|
||||
const long UnixEpochTicks = 621355968000000000L;
|
||||
var t = new DateTime(start / 100L + UnixEpochTicks, DateTimeKind.Utc);
|
||||
|
||||
var gotSeq = fs.GetSeqFromTime(t);
|
||||
gotSeq.ShouldBe(2UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestFileStoreMultiLastSeqsAndLoadLastMsgWithLazySubjectState (T:2957)
|
||||
// MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2957
|
||||
public void FileStoreMultiLastSeqsAndLoadLastMsgWithLazySubjectState_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestFileStoreMultiLastSeqsAndLoadLastMsgWithLazySubjectState line 921
|
||||
var fs = NewMemStore(new StreamConfig { Name = "zzz", Subjects = new[] { "foo" } });
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
var (seqs, err) = fs.MultiLastSeqs(new[] { "foo" }, 0, 0);
|
||||
err.ShouldBeNull();
|
||||
seqs!.Length.ShouldBe(1);
|
||||
seqs![0].ShouldBe(3UL);
|
||||
|
||||
var (removed, _) = fs.RemoveMsg(3);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
(seqs, err) = fs.MultiLastSeqs(new[] { "foo" }, 0, 0);
|
||||
err.ShouldBeNull();
|
||||
seqs!.Length.ShouldBe(1);
|
||||
seqs![0].ShouldBe(2UL);
|
||||
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
var sm = fs.LoadLastMsg("foo", null);
|
||||
sm.ShouldNotBeNull();
|
||||
sm!.Seq.ShouldBe(4UL);
|
||||
|
||||
(removed, _) = fs.RemoveMsg(4);
|
||||
removed.ShouldBeTrue();
|
||||
|
||||
sm = fs.LoadLastMsg("foo", null);
|
||||
sm.ShouldNotBeNull();
|
||||
sm!.Seq.ShouldBe(2UL);
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TestStoreDiscardNew (T:2954) — MemStore permutation only
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact] // T:2954
|
||||
public void StoreDiscardNew_ShouldSucceed()
|
||||
{
|
||||
// Reference: golang/nats-server/server/store_test.go:TestStoreDiscardNew line 788
|
||||
// Helper that runs the discard-new test for a given config modifier
|
||||
void Test(Action<StreamConfig> updateConfig, Exception? expectedErr)
|
||||
{
|
||||
var cfg = new StreamConfig
|
||||
{
|
||||
Name = "zzz",
|
||||
Subjects = new[] { "foo" },
|
||||
Discard = DiscardPolicy.DiscardNew,
|
||||
};
|
||||
updateConfig(cfg);
|
||||
cfg.Storage = StorageType.MemoryStorage;
|
||||
var fs = new JetStreamMemStore(cfg);
|
||||
|
||||
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
|
||||
var expectedSeq = 1UL;
|
||||
|
||||
void RequireState()
|
||||
{
|
||||
var state = fs.State();
|
||||
state.Msgs.ShouldBe(1UL);
|
||||
state.FirstSeq.ShouldBe(expectedSeq);
|
||||
state.LastSeq.ShouldBe(expectedSeq);
|
||||
}
|
||||
|
||||
fs.StoreMsg("foo", null, null, 0);
|
||||
|
||||
// StoreRawMsg with discardNewCheck=true
|
||||
if (expectedErr == null)
|
||||
{
|
||||
fs.StoreRawMsg("foo", null, null, 0, ts, 0, true);
|
||||
expectedSeq++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Should.Throw<Exception>(() => fs.StoreRawMsg("foo", null, null, 0, ts, 0, true));
|
||||
}
|
||||
RequireState();
|
||||
|
||||
// StoreRawMsg with discardNewCheck=false (followers must always accept)
|
||||
fs.StoreRawMsg("foo", null, null, 0, ts, 0, false);
|
||||
expectedSeq++;
|
||||
|
||||
// For MaxMsgsPer we stay at 1 msg; otherwise 2 msgs
|
||||
if (cfg.MaxMsgsPer > 0)
|
||||
{
|
||||
RequireState();
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = fs.State();
|
||||
state.Msgs.ShouldBe(2UL);
|
||||
state.FirstSeq.ShouldBe(expectedSeq - 1);
|
||||
state.LastSeq.ShouldBe(expectedSeq);
|
||||
}
|
||||
|
||||
fs.Stop();
|
||||
}
|
||||
|
||||
Test(cfg => cfg.MaxMsgs = 1, StoreErrors.ErrMaxMsgs);
|
||||
Test(cfg => cfg.MaxBytes = 33, StoreErrors.ErrMaxBytes);
|
||||
Test(cfg => cfg.MaxMsgsPer = 1, null);
|
||||
Test(cfg => { cfg.DiscardNewPer = true; cfg.MaxMsgsPer = 1; }, StoreErrors.ErrMaxMsgsPerSubject);
|
||||
Test(cfg => { cfg.MaxMsgs = 1; cfg.MaxMsgsPer = 1; }, null);
|
||||
Test(cfg => { cfg.MaxBytes = 33; cfg.MaxMsgsPer = 1; }, null);
|
||||
Test(cfg => { cfg.DiscardNewPer = true; cfg.MaxMsgs = 1; cfg.MaxMsgsPer = 1; }, StoreErrors.ErrMaxMsgsPerSubject);
|
||||
Test(cfg => { cfg.DiscardNewPer = true; cfg.MaxBytes = 33; cfg.MaxMsgsPer = 1; }, StoreErrors.ErrMaxMsgsPerSubject);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user