feat(storage): add tombstone tracking and purge operations (Go parity)

Implement PurgeEx, Compact, Truncate, FilteredState, SubjectsState,
SubjectsTotals, State, FastState, GetSeqFromTime on FileStore. Add
MsgBlock.IsDeleted, DeletedSequences, EnumerateNonDeleted. Includes
wildcard subject support via SubjectMatch for all filtered operations.
This commit is contained in:
Joseph Doherty
2026-02-24 13:42:17 -05:00
parent 2816e8f048
commit b0b64292b3
3 changed files with 794 additions and 0 deletions

View File

@@ -322,6 +322,69 @@ public sealed class MsgBlock : IDisposable
}
}
/// <summary>
/// Returns true if the given sequence number has been soft-deleted in this block.
/// Reference: golang/nats-server/server/filestore.go — dmap (deleted map) lookup.
/// </summary>
public bool IsDeleted(ulong sequence)
{
_lock.EnterReadLock();
try { return _deleted.Contains(sequence); }
finally { _lock.ExitReadLock(); }
}
/// <summary>
/// Exposes the set of soft-deleted sequence numbers for read-only inspection.
/// Reference: golang/nats-server/server/filestore.go — dmap access for state queries.
/// </summary>
public IReadOnlySet<ulong> DeletedSequences
{
get
{
_lock.EnterReadLock();
try { return new HashSet<ulong>(_deleted); }
finally { _lock.ExitReadLock(); }
}
}
/// <summary>
/// Enumerates all non-deleted sequences in this block along with their subjects.
/// Used by FileStore for subject-filtered operations (PurgeEx, SubjectsState, etc.).
/// Reference: golang/nats-server/server/filestore.go — loadBlock, iterating non-deleted records.
/// </summary>
public IEnumerable<(ulong Sequence, string Subject)> EnumerateNonDeleted()
{
// Snapshot index and deleted set under the read lock, then decode outside it.
List<(long Offset, int Length, ulong Seq)> entries;
_lock.EnterReadLock();
try
{
entries = new List<(long, int, ulong)>(_index.Count);
foreach (var (seq, (offset, length)) in _index)
{
if (!_deleted.Contains(seq))
entries.Add((offset, length, seq));
}
}
finally
{
_lock.ExitReadLock();
}
// Sort by sequence for deterministic output.
entries.Sort((a, b) => a.Seq.CompareTo(b.Seq));
foreach (var (offset, length, seq) in entries)
{
var buffer = new byte[length];
RandomAccess.Read(_handle, buffer, offset);
var record = MessageRecord.Decode(buffer);
if (record is not null && !record.Deleted)
yield return (record.Sequence, record.Subject);
}
}
/// <summary>
/// Flushes any buffered writes to disk.
/// </summary>