feat(filestore): implement FlushAllPending with atomic stream state writes
Add FlushAllPending() to FileStore, fulfilling the IStreamStore interface contract. The method flushes the active MsgBlock to disk and atomically writes a stream.state checkpoint using write-to-temp + rename, matching Go's flushPendingWritesUnlocked / writeFullState pattern. Add FileStoreCrashRecoveryTests with 5 tests covering: - FlushAllPending flushes block data to .blk file - FlushAllPending writes a valid atomic stream.state JSON checkpoint - FlushAllPending is idempotent (second call overwrites with latest state) - Recovery prunes messages backdated past the MaxAgeMs cutoff - Recovery handles a tail-truncated block without throwing Reference: golang/nats-server/server/filestore.go:5783-5842
This commit is contained in:
@@ -1633,6 +1633,64 @@ public sealed class FileStore : IStreamStore, IAsyncDisposable, IDisposable
|
||||
return new ConsumerFileStore(stateFile, cfg);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// FlushAllPending: flush buffered writes and checkpoint stream state.
|
||||
// Reference: golang/nats-server/server/filestore.go:5783-5842
|
||||
// (flushPendingWritesUnlocked / writeFullState)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Flushes any buffered writes in the active block to disk and atomically
|
||||
/// persists a lightweight stream state checkpoint (stream.state) so that a
|
||||
/// subsequent recovery after a crash can quickly identify the last known
|
||||
/// good sequence without re-scanning every block.
|
||||
/// Reference: golang/nats-server/server/filestore.go:5783 (flushPendingWritesUnlocked).
|
||||
/// </summary>
|
||||
public void FlushAllPending()
|
||||
{
|
||||
_activeBlock?.Flush();
|
||||
WriteStreamState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atomically persists a compact stream state snapshot to disk using the
|
||||
/// write-to-temp-then-rename pattern so that a partial write never leaves
|
||||
/// a corrupt state file.
|
||||
/// The file is written as JSON to <c>{Directory}/stream.state</c>.
|
||||
/// Reference: golang/nats-server/server/filestore.go:5820 (writeFullState).
|
||||
/// </summary>
|
||||
private void WriteStreamState()
|
||||
{
|
||||
var statePath = Path.Combine(_options.Directory, "stream.state");
|
||||
var tmpPath = statePath + ".tmp";
|
||||
|
||||
var snapshot = new StreamStateSnapshot
|
||||
{
|
||||
// Derive FirstSeq from the live message cache to stay accurate across
|
||||
// Purge/Truncate operations that may leave _first out of sync.
|
||||
FirstSeq = _messages.Count > 0 ? _messages.Keys.Min() : 0UL,
|
||||
LastSeq = _last,
|
||||
Messages = (ulong)_messages.Count,
|
||||
Bytes = (ulong)_blocks.Sum(b => b.BytesUsed),
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(snapshot);
|
||||
File.WriteAllText(tmpPath, json);
|
||||
File.Move(tmpPath, statePath, overwrite: true);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// StreamStateSnapshot — private checkpoint record written by WriteStreamState.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private sealed record StreamStateSnapshot
|
||||
{
|
||||
public ulong FirstSeq { get; init; }
|
||||
public ulong LastSeq { get; init; }
|
||||
public ulong Messages { get; init; }
|
||||
public ulong Bytes { get; init; }
|
||||
}
|
||||
|
||||
private sealed class FileRecord
|
||||
{
|
||||
public ulong Sequence { get; init; }
|
||||
|
||||
Reference in New Issue
Block a user