feat: add FileStore tombstone, TTL & consumer state persistence (Task 2)

Port Go filestore tombstone/deletion tests, consumer state encode/decode,
consumer file store persistence, and message TTL enforcement. Adds
ConsumerStateCodec and ConsumerFileStore implementations.

17 new tests ported from filestore_test.go.
This commit is contained in:
Joseph Doherty
2026-02-24 20:17:35 -05:00
parent a9967d3077
commit 7eb06c8ac5
6 changed files with 1557 additions and 7 deletions

View File

@@ -26,6 +26,9 @@ public sealed class MsgBlock : IDisposable
private readonly SafeFileHandle _handle;
private readonly Dictionary<ulong, (long Offset, int Length)> _index = new();
private readonly HashSet<ulong> _deleted = new();
// Go: SkipMsg writes tombstone records with empty subject — tracked separately so
// recovery can distinguish intentional sequence gaps from soft-deleted messages.
private readonly HashSet<ulong> _skipSequences = new();
private readonly long _maxBytes;
private readonly ReaderWriterLockSlim _lock = new();
private long _writeOffset; // Tracks the append position independently of FileStream.Position
@@ -402,6 +405,7 @@ public sealed class MsgBlock : IDisposable
_index[sequence] = (offset, encoded.Length);
_deleted.Add(sequence);
_skipSequences.Add(sequence); // Track skip sequences separately for recovery
// Note: intentionally NOT added to _cache since it is deleted.
if (_totalWritten == 0)
@@ -447,6 +451,22 @@ public sealed class MsgBlock : IDisposable
finally { _lock.ExitReadLock(); }
}
/// <summary>
/// Returns the maximum skip-sequence written into this block (0 if none).
/// Skip sequences are intentional tombstones from SkipMsg/SkipMsgs —
/// they bump _last without storing a live message, so recovery must account
/// for them when computing the high-water mark.
/// </summary>
public ulong MaxSkipSequence
{
get
{
_lock.EnterReadLock();
try { return _skipSequences.Count > 0 ? _skipSequences.Max() : 0UL; }
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.
@@ -582,7 +602,12 @@ public sealed class MsgBlock : IDisposable
_index[record.Sequence] = (offset, recordLength);
if (record.Deleted)
{
_deleted.Add(record.Sequence);
// Empty subject = skip/tombstone record (from SkipMsg/SkipMsgs).
if (string.IsNullOrEmpty(record.Subject))
_skipSequences.Add(record.Sequence);
}
if (count == 0)
_firstSequence = record.Sequence;