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

@@ -24,6 +24,12 @@ public sealed class ConsumerManager : IDisposable
/// </summary>
public event EventHandler<(string Stream, string Name)>? OnAutoResumed;
/// <summary>
/// Optional reference to the stream manager, used to resolve DeliverPolicy.New
/// start sequences at consumer creation time.
/// </summary>
public StreamManager? StreamManager { get; set; }
public ConsumerManager(JetStreamMetaGroup? metaGroup = null)
{
_metaGroup = metaGroup;
@@ -47,6 +53,20 @@ public sealed class ConsumerManager : IDisposable
if (!JetStreamConfigValidator.IsMetadataWithinLimit(config.Metadata))
return JetStreamApiResponse.ErrorResponse(400, "consumer metadata exceeds maximum size");
// Go: DeliverPolicy.New — snapshot the stream's current last sequence at creation
// time so the consumer only sees messages published after this point.
// Reference: server/consumer.go — setStartingSequenceForDeliverNew.
// We set OptStartSeq but preserve DeliverPolicy.New in the stored config;
// the fetch engine uses OptStartSeq when set regardless of policy.
if (config.DeliverPolicy == DeliverPolicy.New && StreamManager != null)
{
if (StreamManager.TryGet(stream, out var streamHandle))
{
var streamState = streamHandle.Store.GetStateAsync(default).GetAwaiter().GetResult();
config.OptStartSeq = streamState.LastSeq + 1;
}
}
if (config.FilterSubjects.Count == 0 && !string.IsNullOrWhiteSpace(config.FilterSubject))
config.FilterSubjects.Add(config.FilterSubject);
@@ -302,6 +322,13 @@ public sealed class ConsumerManager : IDisposable
return handle.AckProcessor.PendingCount;
}
/// <summary>
/// Returns true if there are any consumers registered for the given stream.
/// Used to short-circuit the LoadAsync call on the publish hot path.
/// </summary>
public bool HasConsumersForStream(string stream)
=> _consumers.Keys.Any(k => string.Equals(k.Stream, stream, StringComparison.Ordinal));
public void OnPublished(string stream, StoredMessage message)
{
foreach (var handle in _consumers.Values.Where(c => c.Stream == stream && c.Config.Push))