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:
@@ -226,17 +226,95 @@ public sealed class FileStoreEncryptionTests : IDisposable
|
||||
}
|
||||
|
||||
// Go: TestFileStoreDoubleCompactWithWriteInBetweenEncryptedBug server/filestore_test.go:3924
|
||||
[Fact(Skip = "Compact not yet implemented in .NET FileStore")]
|
||||
[Fact]
|
||||
public async Task Encrypted_double_compact_with_write_in_between()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
await using var store = CreateStore("enc-double-compact");
|
||||
|
||||
const string subject = "foo";
|
||||
var payload = "ouch"u8.ToArray();
|
||||
|
||||
// Write 10 messages (seqs 1-10).
|
||||
for (var i = 0; i < 10; i++)
|
||||
await store.AppendAsync(subject, payload, default);
|
||||
|
||||
// First compact: remove seqs 1-4 (seq < 5).
|
||||
store.Compact(5);
|
||||
|
||||
// 6 messages remain (seqs 5-10).
|
||||
var stateAfterFirstCompact = await store.GetStateAsync(default);
|
||||
stateAfterFirstCompact.Messages.ShouldBe(6UL);
|
||||
stateAfterFirstCompact.LastSeq.ShouldBe(10UL);
|
||||
|
||||
// Write 5 more messages (seqs 11-15).
|
||||
for (var i = 0; i < 5; i++)
|
||||
await store.AppendAsync(subject, payload, default);
|
||||
|
||||
// Second compact: remove seqs 5-9 (seq < 10).
|
||||
store.Compact(10);
|
||||
|
||||
// 6 messages remain (seqs 10-15).
|
||||
var stateAfterSecondCompact = await store.GetStateAsync(default);
|
||||
stateAfterSecondCompact.Messages.ShouldBe(6UL);
|
||||
stateAfterSecondCompact.LastSeq.ShouldBe(15UL);
|
||||
stateAfterSecondCompact.FirstSeq.ShouldBe(10UL);
|
||||
|
||||
// All remaining messages (seqs 10-15) must be loadable and readable.
|
||||
for (var seq = 10UL; seq <= 15UL; seq++)
|
||||
{
|
||||
var msg = await store.LoadAsync(seq, default);
|
||||
msg.ShouldNotBeNull($"seq {seq} should still be loadable after double compact");
|
||||
msg!.Subject.ShouldBe(subject);
|
||||
msg.Payload.ToArray().ShouldBe(payload);
|
||||
}
|
||||
|
||||
// Compacted-away sequences must not be loadable.
|
||||
(await store.LoadAsync(1, default)).ShouldBeNull();
|
||||
(await store.LoadAsync(9, default)).ShouldBeNull();
|
||||
}
|
||||
|
||||
// Go: TestFileStoreEncryptedKeepIndexNeedBekResetBug server/filestore_test.go:3956
|
||||
[Fact(Skip = "Block encryption key reset not yet implemented in .NET FileStore")]
|
||||
// Verifies that after all messages in a block are removed (leaving the block empty),
|
||||
// subsequent writes to that block are readable — i.e., the block encryption key
|
||||
// (BEK) is correctly reset when new data follows a fully-emptied block.
|
||||
// Go: TestFileStoreEncryptedKeepIndexNeedBekResetBug server/filestore_test.go:3956
|
||||
[Fact]
|
||||
public async Task Encrypted_keep_index_bek_reset()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
await using var store = CreateStore("enc-bek-reset");
|
||||
|
||||
var payload = "ouch"u8.ToArray();
|
||||
|
||||
// Write 5 messages (seqs 1-5) into the active block.
|
||||
for (var i = 0; i < 5; i++)
|
||||
await store.AppendAsync("foo", payload, default);
|
||||
|
||||
// Remove all 5 — the block is now empty, mirroring the Go test's TTL-expiry path.
|
||||
for (var seq = 1UL; seq <= 5UL; seq++)
|
||||
(await store.RemoveAsync(seq, default)).ShouldBeTrue();
|
||||
|
||||
var emptyState = await store.GetStateAsync(default);
|
||||
emptyState.Messages.ShouldBe((ulong)0);
|
||||
|
||||
// Write 5 more messages into the same (now-empty) block.
|
||||
// The BEK must be reset so that encryption/decryption is valid for the new data.
|
||||
var firstNewSeq = await store.AppendAsync("foo", payload, default);
|
||||
for (var i = 1; i < 5; i++)
|
||||
await store.AppendAsync("foo", payload, default);
|
||||
|
||||
var state = await store.GetStateAsync(default);
|
||||
state.Messages.ShouldBe((ulong)5);
|
||||
|
||||
// Every message written after the block was emptied must decrypt correctly.
|
||||
var msg = await store.LoadAsync(firstNewSeq, default);
|
||||
msg.ShouldNotBeNull();
|
||||
msg!.Subject.ShouldBe("foo");
|
||||
msg.Payload.ToArray().ShouldBe(payload);
|
||||
|
||||
// Spot-check the last seq as well.
|
||||
var lastMsg = await store.LoadAsync(firstNewSeq + 4, default);
|
||||
lastMsg.ShouldNotBeNull();
|
||||
lastMsg!.Payload.ToArray().ShouldBe(payload);
|
||||
}
|
||||
|
||||
// Verify encryption with no-op key (empty key) does not crash.
|
||||
|
||||
Reference in New Issue
Block a user