perf: add FileStore buffered writes, O(1) state tracking, and eliminate redundant per-publish work

Implement Go-parity background flush loop (coalesce 16KB/8ms) in MsgBlock/FileStore,
replace O(n) GetStateAsync with incremental counters, skip PruneExpired/LoadAsync/
PrunePerSubject when not needed, and bypass RAFT for single-replica streams. Fix counter
tracking bugs in RemoveMsg/EraseMsg/TTL expiry and ObjectDisposedException races in
flush loop disposal. FileStore optimizations verified with 3112/3112 JetStream tests
passing; async publish benchmark remains at ~174 msg/s due to E2E protocol path bottleneck.
This commit is contained in:
Joseph Doherty
2026-03-13 03:11:11 -04:00
parent 37575dc41c
commit 4de691c9c5
30 changed files with 1514 additions and 185 deletions

View File

@@ -60,7 +60,7 @@ public sealed class FileStoreRecovery2Tests : IDisposable
if (Directory.Exists(_root))
{
try { Directory.Delete(_root, recursive: true); }
catch { /* best-effort cleanup */ }
catch (IOException) { /* best-effort cleanup — directory may be locked by OS */ }
}
}
@@ -381,28 +381,33 @@ public sealed class FileStoreRecovery2Tests : IDisposable
public void SyncCompress_OnlyIfDirty_CompactFlagBehavior()
{
var dir = UniqueDir();
using var store = CreateStore(dir, new FileStoreOptions { BlockSizeBytes = 256 });
var msg = "hello"u8.ToArray();
// Scoped block to ensure store is fully disposed (pending writes flushed)
// before opening the second store for recovery verification.
{
using var store = CreateStore(dir, new FileStoreOptions { BlockSizeBytes = 256 });
// Fill 2 blocks (6 per block at blockSize=256).
for (var i = 0; i < 12; i++)
store.StoreMsg("foo.BB", null, msg, 0);
var msg = "hello"u8.ToArray();
// Add one more to start a third block.
store.StoreMsg("foo.BB", null, msg, 0); // seq 13
// Fill 2 blocks (6 per block at blockSize=256).
for (var i = 0; i < 12; i++)
store.StoreMsg("foo.BB", null, msg, 0);
// Delete a bunch to create holes in blocks 1 and 2.
foreach (var seq in new ulong[] { 2, 3, 4, 5, 8, 9, 10, 11 })
store.RemoveMsg(seq).ShouldBeTrue();
// Add one more to start a third block.
store.StoreMsg("foo.BB", null, msg, 0); // seq 13
// Add more to create a 4th/5th block.
for (var i = 0; i < 6; i++)
store.StoreMsg("foo.BB", null, msg, 0);
// Delete a bunch to create holes in blocks 1 and 2.
foreach (var seq in new ulong[] { 2, 3, 4, 5, 8, 9, 10, 11 })
store.RemoveMsg(seq).ShouldBeTrue();
// Total live: 13 + 6 = 19 - 8 deleted = 11.
var state = store.State();
state.Msgs.ShouldBe(11UL);
// Add more to create a 4th/5th block.
for (var i = 0; i < 6; i++)
store.StoreMsg("foo.BB", null, msg, 0);
// Total live: 13 + 6 = 19 - 8 deleted = 11.
var state = store.State();
state.Msgs.ShouldBe(11UL);
}
// After restart, state should be preserved.
using var store2 = CreateStore(dir, new FileStoreOptions { BlockSizeBytes = 256 });