feat(storage): port FileStore Go tests and add sync methods (Go parity)

Add 67 Go-parity tests from filestore_test.go covering:
- SkipMsg/SkipMsgs sequence reservation
- RemoveMsg/EraseMsg soft-delete
- LoadMsg/LoadLastMsg/LoadNextMsg message retrieval
- AllLastSeqs/MultiLastSeqs per-subject last sequences
- SubjectForSeq reverse lookup
- NumPending with filters and last-per-subject mode
- Recovery watermark preservation after purge
- FastState NumDeleted/LastTime correctness
- PurgeEx with empty subject + keep parameter
- Compact _first watermark tracking
- Multi-block operations and state verification

Implements missing IStreamStore sync methods on FileStore:
RemoveMsg, EraseMsg, SkipMsg, SkipMsgs, LoadMsg, LoadLastMsg,
LoadNextMsg, AllLastSeqs, MultiLastSeqs, SubjectForSeq, NumPending.

Adds MsgBlock.WriteSkip() for tombstone sequence reservation.
Adds IDisposable to FileStore for synchronous test disposal.
This commit is contained in:
Joseph Doherty
2026-02-24 14:43:06 -05:00
parent d0068b121f
commit a245bd75a7
3 changed files with 2466 additions and 7 deletions

View File

@@ -367,6 +367,56 @@ public sealed class MsgBlock : IDisposable
}
}
/// <summary>
/// Writes a skip record for the given sequence number — reserves the sequence
/// without storing actual message data. The record is written with the Deleted
/// flag set so recovery skips it when rebuilding the in-memory message cache.
/// This mirrors Go's SkipMsg tombstone behaviour.
/// Reference: golang/nats-server/server/filestore.go — SkipMsg.
/// </summary>
public void WriteSkip(ulong sequence)
{
_lock.EnterWriteLock();
try
{
if (_writeOffset >= _maxBytes)
throw new InvalidOperationException("Block is sealed; cannot write skip record.");
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L;
var record = new MessageRecord
{
Sequence = sequence,
Subject = string.Empty,
Headers = ReadOnlyMemory<byte>.Empty,
Payload = ReadOnlyMemory<byte>.Empty,
Timestamp = now,
Deleted = true, // skip = deleted from the start
};
var encoded = MessageRecord.Encode(record);
var offset = _writeOffset;
RandomAccess.Write(_handle, encoded, offset);
_writeOffset = offset + encoded.Length;
_index[sequence] = (offset, encoded.Length);
_deleted.Add(sequence);
// Note: intentionally NOT added to _cache since it is deleted.
if (_totalWritten == 0)
_firstSequence = sequence;
_lastSequence = Math.Max(_lastSequence, sequence);
_nextSequence = Math.Max(_nextSequence, sequence + 1);
_totalWritten++;
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// Clears the write cache, releasing memory. After this call, all reads will
/// go to disk. Called when the block is sealed (no longer the active block)