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:
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -404,6 +404,14 @@ public class AccountImportExportTests
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SendMessageNoFlush(string subject, string sid, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SignalFlush() { }
|
||||
|
||||
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
|
||||
|
||||
public void RemoveSubscription(string sid) { }
|
||||
|
||||
@@ -513,6 +513,14 @@ public class AccountIsolationTests
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SendMessageNoFlush(string subject, string sid, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SignalFlush() { }
|
||||
|
||||
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
|
||||
public void RemoveSubscription(string sid) { }
|
||||
}
|
||||
|
||||
@@ -814,6 +814,14 @@ public class AuthCalloutTests
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SendMessageNoFlush(string subject, string sid, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SignalFlush() { }
|
||||
|
||||
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
|
||||
public void RemoveSubscription(string sid) { }
|
||||
}
|
||||
|
||||
@@ -322,6 +322,14 @@ public class ImportExportTests
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SendMessageNoFlush(string subject, string sid, string? replyTo,
|
||||
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
OnMessage?.Invoke(subject, sid, replyTo, headers, payload);
|
||||
}
|
||||
|
||||
public void SignalFlush() { }
|
||||
|
||||
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
|
||||
|
||||
public void RemoveSubscription(string sid) { }
|
||||
|
||||
@@ -121,6 +121,12 @@ public class SubListParityBatch2Tests
|
||||
{
|
||||
}
|
||||
|
||||
public void SendMessageNoFlush(string subject, string sid, string? replyTo, ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
}
|
||||
|
||||
public void SignalFlush() { }
|
||||
|
||||
public bool QueueOutbound(ReadOnlyMemory<byte> data) => true;
|
||||
|
||||
public void RemoveSubscription(string sid)
|
||||
|
||||
Reference in New Issue
Block a user