perf: batch flush signaling and fetch path optimizations (Round 6)

Implement Go's pcd (per-client deferred flush) pattern to reduce write-loop
wakeups during fan-out delivery, optimize ack reply string construction with
stack-based formatting, cache CompiledFilter on ConsumerHandle, and pool
fetch message lists. Durable consumer fetch improves from 0.60x to 0.74x Go.
This commit is contained in:
Joseph Doherty
2026-03-13 09:35:57 -04:00
parent 0a4e7a822f
commit 0be321fa53
13 changed files with 680 additions and 153 deletions

View File

@@ -521,6 +521,72 @@ public class JetStreamTests(JetStreamServerFixture fixture)
sequences[i].ShouldBeGreaterThan(sequences[i - 1]);
}
// -------------------------------------------------------------------------
// Test 16b — Ordered consumer with ConsumeAsync (not FetchAsync)
// -------------------------------------------------------------------------
[Fact]
public async Task Consumer_Ordered_ConsumeAsync()
{
await using var client = fixture.CreateClient();
await client.ConnectAsync();
var js = new NatsJSContext(client);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var streamName = $"E2E_ORD_CONSUME_{Random.Shared.Next(100000)}";
await js.CreateStreamAsync(new StreamConfig(streamName, [$"js.ordcon.{streamName}.>"]), cts.Token);
for (var i = 0; i < 5; i++)
await js.PublishAsync($"js.ordcon.{streamName}.{i}", i, cancellationToken: cts.Token);
// Test ordered ConsumeAsync
var consumer = await js.CreateOrderedConsumerAsync(streamName, cancellationToken: cts.Token);
var sequences = new List<ulong>();
await foreach (var msg in consumer.ConsumeAsync<int>(cancellationToken: cts.Token))
{
sequences.Add(msg.Metadata!.Value.Sequence.Stream);
if (sequences.Count >= 5) break;
}
sequences.Count.ShouldBe(5);
for (var i = 1; i < sequences.Count; i++)
sequences[i].ShouldBeGreaterThan(sequences[i - 1]);
}
// -------------------------------------------------------------------------
// Test 16c — Durable consumer with ConsumeAsync (not FetchAsync)
// -------------------------------------------------------------------------
[Fact]
public async Task Consumer_Durable_ConsumeAsync()
{
await using var client = fixture.CreateClient();
await client.ConnectAsync();
var js = new NatsJSContext(client);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var streamName = $"E2E_DUR_CONSUME_{Random.Shared.Next(100000)}";
await js.CreateStreamAsync(new StreamConfig(streamName, [$"js.durcon.{streamName}.>"]), cts.Token);
for (var i = 0; i < 5; i++)
await js.PublishAsync($"js.durcon.{streamName}.{i}", i, cancellationToken: cts.Token);
var durConsumerName = $"dur_{Random.Shared.Next(100000)}";
var durConsumer = await js.CreateOrUpdateConsumerAsync(streamName,
new ConsumerConfig(durConsumerName) { AckPolicy = ConsumerConfigAckPolicy.None },
cts.Token);
var sequences = new List<ulong>();
await foreach (var msg in durConsumer.ConsumeAsync<int>(cancellationToken: cts.Token))
{
sequences.Add(msg.Metadata!.Value.Sequence.Stream);
if (sequences.Count >= 5) break;
}
sequences.Count.ShouldBe(5, $"Durable ConsumeAsync should get 5 messages but got {sequences.Count}");
}
// -------------------------------------------------------------------------
// Test 17 — Mirror stream: replicates messages from a source stream
// -------------------------------------------------------------------------