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:
@@ -78,7 +78,11 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
_maxp = cfg.MaxMsgsPer;
|
_maxp = cfg.MaxMsgsPer;
|
||||||
|
|
||||||
if (cfg.FirstSeq > 0)
|
if (cfg.FirstSeq > 0)
|
||||||
Purge();
|
{
|
||||||
|
// Set the initial state so that the first StoreMsg call assigns seq = cfg.FirstSeq.
|
||||||
|
_state.LastSeq = cfg.FirstSeq - 1;
|
||||||
|
_state.FirstSeq = cfg.FirstSeq;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -92,7 +96,10 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var seq = _state.LastSeq + 1;
|
var seq = _state.LastSeq + 1;
|
||||||
var ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
|
// Use 100-nanosecond Ticks for higher timestamp precision.
|
||||||
|
// Nanoseconds since Unix epoch: (Ticks - UnixEpochTicks) * 100
|
||||||
|
const long UnixEpochTicks = 621355968000000000L;
|
||||||
|
var ts = (DateTimeOffset.UtcNow.UtcTicks - UnixEpochTicks) * 100L;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StoreRawMsgLocked(subject, hdr, msg, seq, ts, ttl, discardNewCheck: true);
|
StoreRawMsgLocked(subject, hdr, msg, seq, ts, ttl, discardNewCheck: true);
|
||||||
@@ -134,12 +141,24 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
hdr ??= Array.Empty<byte>();
|
hdr ??= Array.Empty<byte>();
|
||||||
msg ??= Array.Empty<byte>();
|
msg ??= Array.Empty<byte>();
|
||||||
|
|
||||||
|
// Determine if we are at the per-subject limit.
|
||||||
|
bool atSubjectLimit = false;
|
||||||
|
if (_maxp > 0 && !string.IsNullOrEmpty(subject))
|
||||||
|
{
|
||||||
|
var subjectBytesCheck = Encoding.UTF8.GetBytes(subject);
|
||||||
|
var (ssCheck, foundCheck) = _fss.Find(subjectBytesCheck);
|
||||||
|
if (foundCheck && ssCheck != null)
|
||||||
|
atSubjectLimit = ssCheck.Msgs >= (ulong)_maxp;
|
||||||
|
}
|
||||||
|
|
||||||
// Discard-new enforcement
|
// Discard-new enforcement
|
||||||
if (discardNewCheck && _cfg.Discard == DiscardPolicy.DiscardNew)
|
if (discardNewCheck && _cfg.Discard == DiscardPolicy.DiscardNew)
|
||||||
{
|
{
|
||||||
if (_cfg.MaxMsgs > 0 && _state.Msgs >= (ulong)_cfg.MaxMsgs)
|
if (atSubjectLimit && _cfg.DiscardNewPer)
|
||||||
|
throw StoreErrors.ErrMaxMsgsPerSubject;
|
||||||
|
if (_cfg.MaxMsgs > 0 && _state.Msgs >= (ulong)_cfg.MaxMsgs && !atSubjectLimit)
|
||||||
throw StoreErrors.ErrMaxMsgs;
|
throw StoreErrors.ErrMaxMsgs;
|
||||||
if (_cfg.MaxBytes > 0 && _state.Bytes + MsgSize(subject, hdr, msg) > (ulong)_cfg.MaxBytes)
|
if (_cfg.MaxBytes > 0 && _state.Bytes + MsgSize(subject, hdr, msg) > (ulong)_cfg.MaxBytes && !atSubjectLimit)
|
||||||
throw StoreErrors.ErrMaxBytes;
|
throw StoreErrors.ErrMaxBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,8 +361,12 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
{
|
{
|
||||||
var (ss, found) = _fss.Find(Encoding.UTF8.GetBytes(subject));
|
var (ss, found) = _fss.Find(Encoding.UTF8.GetBytes(subject));
|
||||||
if (found && ss != null && ss.Msgs > 0)
|
if (found && ss != null && ss.Msgs > 0)
|
||||||
|
{
|
||||||
|
if (ss.LastNeedsUpdate)
|
||||||
|
RecalculateForSubj(subject, ss);
|
||||||
_msgs.TryGetValue(ss.Last, out stored);
|
_msgs.TryGetValue(ss.Last, out stored);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (stored == null)
|
if (stored == null)
|
||||||
throw StoreErrors.ErrStoreMsgNotFound;
|
throw StoreErrors.ErrStoreMsgNotFound;
|
||||||
@@ -603,15 +626,71 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public (ulong Purged, Exception? Error) PurgeEx(string subject, ulong seq, ulong keep)
|
public (ulong Purged, Exception? Error) PurgeEx(string subject, ulong seq, ulong keep)
|
||||||
{
|
{
|
||||||
// TODO: session 17 — full subject-filtered purge
|
var isAll = string.IsNullOrEmpty(subject) || subject == ">";
|
||||||
if (string.IsNullOrEmpty(subject) || subject == ">")
|
if (isAll)
|
||||||
{
|
{
|
||||||
if (keep == 0 && seq == 0)
|
if (keep == 0 && seq == 0)
|
||||||
return Purge();
|
return Purge();
|
||||||
|
if (seq > 1)
|
||||||
|
return Compact(seq);
|
||||||
|
if (keep > 0)
|
||||||
|
{
|
||||||
|
ulong msgs, lseq;
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
msgs = _state.Msgs;
|
||||||
|
lseq = _state.LastSeq;
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
if (keep >= msgs)
|
||||||
|
return (0, null);
|
||||||
|
return Compact(lseq - keep + 1);
|
||||||
}
|
}
|
||||||
return (0, null);
|
return (0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject-filtered purge
|
||||||
|
var ss = FilteredState(1, subject);
|
||||||
|
if (ss.Msgs == 0)
|
||||||
|
return (0, null);
|
||||||
|
|
||||||
|
if (keep > 0)
|
||||||
|
{
|
||||||
|
if (keep >= ss.Msgs)
|
||||||
|
return (0, null);
|
||||||
|
ss.Msgs -= keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = ss.Last;
|
||||||
|
if (seq > 1)
|
||||||
|
last = seq - 1;
|
||||||
|
|
||||||
|
ulong purged = 0;
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_msgs == null)
|
||||||
|
return (0, null);
|
||||||
|
|
||||||
|
for (var s = ss.First; s <= last; s++)
|
||||||
|
{
|
||||||
|
if (_msgs.TryGetValue(s, out var sm) && sm != null && sm.Subject == subject)
|
||||||
|
{
|
||||||
|
if (RemoveMsgLocked(s, false))
|
||||||
|
{
|
||||||
|
purged++;
|
||||||
|
if (purged >= ss.Msgs)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_mu.IsWriteLockHeld)
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
return (purged, null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public (ulong Purged, Exception? Error) Compact(ulong seq)
|
public (ulong Purged, Exception? Error) Compact(ulong seq)
|
||||||
{
|
{
|
||||||
@@ -703,9 +782,10 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
// Full reset
|
// Full reset
|
||||||
purged = (ulong)_msgs.Count;
|
purged = (ulong)_msgs.Count;
|
||||||
bytes = _state.Bytes;
|
bytes = _state.Bytes;
|
||||||
_state = new StreamState { LastTime = DateTime.UtcNow };
|
_state = new StreamState();
|
||||||
_msgs = new Dictionary<ulong, StoreMsg>();
|
_msgs = new Dictionary<ulong, StoreMsg>();
|
||||||
_dmap = new SequenceSet();
|
_dmap = new SequenceSet();
|
||||||
|
_fss.Reset();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -847,6 +927,8 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
return new SimpleState { Msgs = _state.Msgs, First = _state.FirstSeq, Last = _state.LastSeq };
|
return new SimpleState { Msgs = _state.Msgs, First = _state.FirstSeq, Last = _state.LastSeq };
|
||||||
|
|
||||||
var ss = new SimpleState();
|
var ss = new SimpleState();
|
||||||
|
var havePartial = false;
|
||||||
|
|
||||||
_fss.Match(Encoding.UTF8.GetBytes(filter), (subj, fss) =>
|
_fss.Match(Encoding.UTF8.GetBytes(filter), (subj, fss) =>
|
||||||
{
|
{
|
||||||
if (fss.FirstNeedsUpdate || fss.LastNeedsUpdate)
|
if (fss.FirstNeedsUpdate || fss.LastNeedsUpdate)
|
||||||
@@ -854,12 +936,46 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
|
|
||||||
if (sseq <= fss.First)
|
if (sseq <= fss.First)
|
||||||
{
|
{
|
||||||
|
// All messages in this subject are at or after sseq
|
||||||
ss.Msgs += fss.Msgs;
|
ss.Msgs += fss.Msgs;
|
||||||
if (ss.First == 0 || fss.First < ss.First) ss.First = fss.First;
|
if (ss.First == 0 || fss.First < ss.First) ss.First = fss.First;
|
||||||
if (fss.Last > ss.Last) ss.Last = fss.Last;
|
if (fss.Last > ss.Last) ss.Last = fss.Last;
|
||||||
}
|
}
|
||||||
|
else if (sseq <= fss.Last)
|
||||||
|
{
|
||||||
|
// Partial: sseq is inside this subject's range — need to scan
|
||||||
|
havePartial = true;
|
||||||
|
// Still track Last for the scan bounds
|
||||||
|
if (fss.Last > ss.Last) ss.Last = fss.Last;
|
||||||
|
}
|
||||||
|
// else sseq > fss.Last: all messages before sseq, skip
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!havePartial)
|
||||||
|
return ss;
|
||||||
|
|
||||||
|
// Need to scan messages from sseq to ss.Last
|
||||||
|
if (_msgs == null)
|
||||||
|
return ss;
|
||||||
|
|
||||||
|
var scanFirst = sseq;
|
||||||
|
var scanLast = ss.Last;
|
||||||
|
if (scanLast == 0) scanLast = _state.LastSeq;
|
||||||
|
|
||||||
|
// Reset and rescan
|
||||||
|
ss = new SimpleState();
|
||||||
|
for (var seq = scanFirst; seq <= scanLast; seq++)
|
||||||
|
{
|
||||||
|
if (!_msgs.TryGetValue(seq, out var sm) || sm == null)
|
||||||
|
continue;
|
||||||
|
if (isAll || MatchLiteral(sm.Subject, filter))
|
||||||
|
{
|
||||||
|
ss.Msgs++;
|
||||||
|
if (ss.First == 0) ss.First = seq;
|
||||||
|
ss.Last = seq;
|
||||||
|
}
|
||||||
|
}
|
||||||
return ss;
|
return ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -947,8 +1063,10 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
{
|
{
|
||||||
if (_msgs == null || _msgs.Count == 0) return (Array.Empty<ulong>(), null);
|
if (_msgs == null || _msgs.Count == 0) return (Array.Empty<ulong>(), null);
|
||||||
var seqs = new List<ulong>(_fss.Size());
|
var seqs = new List<ulong>(_fss.Size());
|
||||||
_fss.IterFast((_, ss) =>
|
_fss.IterFast((subj, ss) =>
|
||||||
{
|
{
|
||||||
|
if (ss.LastNeedsUpdate)
|
||||||
|
RecalculateForSubj(Encoding.UTF8.GetString(subj), ss);
|
||||||
seqs.Add(ss.Last);
|
seqs.Add(ss.Last);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -974,14 +1092,32 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
var seen = new HashSet<ulong>();
|
var seen = new HashSet<ulong>();
|
||||||
foreach (var filter in filters)
|
foreach (var filter in filters)
|
||||||
{
|
{
|
||||||
_fss.Match(Encoding.UTF8.GetBytes(filter), (_, ss) =>
|
_fss.Match(Encoding.UTF8.GetBytes(filter), (subj, ss) =>
|
||||||
{
|
{
|
||||||
if (ss.Last <= maxSeq && seen.Add(ss.Last))
|
if (ss.LastNeedsUpdate)
|
||||||
|
RecalculateForSubj(Encoding.UTF8.GetString(subj), ss);
|
||||||
|
if (ss.Last <= maxSeq)
|
||||||
|
{
|
||||||
|
if (seen.Add(ss.Last))
|
||||||
seqs.Add(ss.Last);
|
seqs.Add(ss.Last);
|
||||||
|
}
|
||||||
|
else if (ss.Msgs > 1)
|
||||||
|
{
|
||||||
|
// Last is beyond maxSeq — scan backwards for the most recent msg <= maxSeq.
|
||||||
|
var s = Encoding.UTF8.GetString(subj);
|
||||||
|
for (var seq = maxSeq; seq > 0; seq--)
|
||||||
|
{
|
||||||
|
if (_msgs.TryGetValue(seq, out var sm) && sm != null && sm.Subject == s)
|
||||||
|
{
|
||||||
|
if (seen.Add(seq)) seqs.Add(seq);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (maxAllowed > 0 && seqs.Count > maxAllowed)
|
if (maxAllowed > 0 && seqs.Count > maxAllowed)
|
||||||
return (Array.Empty<ulong>(), StoreErrors.ErrTooManyResults);
|
return (null!, StoreErrors.ErrTooManyResults);
|
||||||
}
|
}
|
||||||
seqs.Sort();
|
seqs.Sort();
|
||||||
return (seqs.ToArray(), null);
|
return (seqs.ToArray(), null);
|
||||||
@@ -1017,7 +1153,9 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ulong GetSeqFromTime(DateTime t)
|
public ulong GetSeqFromTime(DateTime t)
|
||||||
{
|
{
|
||||||
var ts = new DateTimeOffset(t).ToUnixTimeMilliseconds() * 1_000_000L;
|
// Use same 100-nanosecond precision as StoreMsg timestamps.
|
||||||
|
const long UnixEpochTicksGsft = 621355968000000000L;
|
||||||
|
var ts = (new DateTimeOffset(t, TimeSpan.Zero).UtcTicks - UnixEpochTicksGsft) * 100L;
|
||||||
_mu.EnterReadLock();
|
_mu.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1038,7 +1176,9 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (lastSm == null) return _state.LastSeq + 1;
|
if (lastSm == null) return _state.LastSeq + 1;
|
||||||
if (ts >= lastSm.Ts) return _state.LastSeq + 1;
|
// Mirror Go: if ts == last ts return that seq; if ts > last ts return pastEnd.
|
||||||
|
if (ts == lastSm.Ts) return lastSm.Seq;
|
||||||
|
if (ts > lastSm.Ts) return _state.LastSeq + 1;
|
||||||
|
|
||||||
// Linear scan fallback
|
// Linear scan fallback
|
||||||
for (var seq = _state.FirstSeq; seq <= _state.LastSeq; seq++)
|
for (var seq = _state.FirstSeq; seq <= _state.LastSeq; seq++)
|
||||||
@@ -1066,9 +1206,32 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_cfg = cfg.Clone();
|
_cfg = cfg.Clone();
|
||||||
_maxp = cfg.MaxMsgsPer;
|
|
||||||
|
// Clamp MaxMsgsPer to minimum of -1
|
||||||
|
if (_cfg.MaxMsgsPer < -1)
|
||||||
|
{
|
||||||
|
_cfg.MaxMsgsPer = -1;
|
||||||
|
cfg.MaxMsgsPer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldMaxp = _maxp;
|
||||||
|
_maxp = _cfg.MaxMsgsPer;
|
||||||
|
|
||||||
EnforceMsgLimit();
|
EnforceMsgLimit();
|
||||||
EnforceBytesLimit();
|
EnforceBytesLimit();
|
||||||
|
|
||||||
|
// Enforce per-subject limits if MaxMsgsPer was reduced or newly set
|
||||||
|
if (_maxp > 0 && (oldMaxp == 0 || _maxp < oldMaxp))
|
||||||
|
{
|
||||||
|
var lm = (ulong)_maxp;
|
||||||
|
_fss.IterFast((subj, ss) =>
|
||||||
|
{
|
||||||
|
if (ss.Msgs > lm)
|
||||||
|
EnforcePerSubjectLimit(Encoding.UTF8.GetString(subj), ss);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (_ageChk == null && _cfg.MaxAge != TimeSpan.Zero)
|
if (_ageChk == null && _cfg.MaxAge != TimeSpan.Zero)
|
||||||
StartAgeChk();
|
StartAgeChk();
|
||||||
if (_ageChk != null && _cfg.MaxAge == TimeSpan.Zero)
|
if (_ageChk != null && _cfg.MaxAge == TimeSpan.Zero)
|
||||||
@@ -1400,7 +1563,9 @@ public sealed class JetStreamMemStore : IStreamStore
|
|||||||
{
|
{
|
||||||
if (_msgs == null || _cfg.MaxAge == TimeSpan.Zero) return;
|
if (_msgs == null || _cfg.MaxAge == TimeSpan.Zero) return;
|
||||||
var minAge = DateTime.UtcNow - _cfg.MaxAge;
|
var minAge = DateTime.UtcNow - _cfg.MaxAge;
|
||||||
var minTs = new DateTimeOffset(minAge).ToUnixTimeMilliseconds() * 1_000_000L;
|
// Use same 100-nanosecond precision as StoreMsg timestamps.
|
||||||
|
const long UnixEpochTicksExp = 621355968000000000L;
|
||||||
|
var minTs = (new DateTimeOffset(minAge, TimeSpan.Zero).UtcTicks - UnixEpochTicksExp) * 100L;
|
||||||
var toRemove = new List<ulong>();
|
var toRemove = new List<ulong>();
|
||||||
foreach (var kv in _msgs)
|
foreach (var kv in _msgs)
|
||||||
{
|
{
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-27 00:15:57 UTC
|
Generated: 2026-02-27 00:35:59 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
@@ -21,10 +21,10 @@ Generated: 2026-02-27 00:15:57 UTC
|
|||||||
|
|
||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| complete | 214 |
|
| complete | 252 |
|
||||||
| deferred | 215 |
|
| deferred | 235 |
|
||||||
| n_a | 187 |
|
| n_a | 187 |
|
||||||
| not_started | 2527 |
|
| not_started | 2469 |
|
||||||
| verified | 114 |
|
| verified | 114 |
|
||||||
|
|
||||||
## Library Mappings (36 total)
|
## Library Mappings (36 total)
|
||||||
@@ -36,4 +36,4 @@ Generated: 2026-02-27 00:15:57 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**4199/6942 items complete (60.5%)**
|
**4237/6942 items complete (61.0%)**
|
||||||
|
|||||||
39
reports/report_917cd33.md
Normal file
39
reports/report_917cd33.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
|
Generated: 2026-02-27 00:35:59 UTC
|
||||||
|
|
||||||
|
## Modules (12 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| not_started | 1 |
|
||||||
|
| verified | 11 |
|
||||||
|
|
||||||
|
## Features (3673 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 3368 |
|
||||||
|
| n_a | 26 |
|
||||||
|
| verified | 279 |
|
||||||
|
|
||||||
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| complete | 252 |
|
||||||
|
| deferred | 235 |
|
||||||
|
| n_a | 187 |
|
||||||
|
| not_started | 2469 |
|
||||||
|
| verified | 114 |
|
||||||
|
|
||||||
|
## Library Mappings (36 total)
|
||||||
|
|
||||||
|
| Status | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| mapped | 36 |
|
||||||
|
|
||||||
|
|
||||||
|
## Overall Progress
|
||||||
|
|
||||||
|
**4237/6942 items complete (61.0%)**
|
||||||
Reference in New Issue
Block a user