perf: batch flush + signal-based wakeup for JS pull consumers
Replace per-message DeliverMessage/flush in DeliverPullFetchMessagesAsync with SendMessageNoFlush + batch flush every 64 messages. Add signal-based wakeup (StreamHandle.NotifyPublish/WaitForPublishAsync) to replace 5ms Task.Delay polling in both DeliverPullFetchMessagesAsync and PullConsumerEngine.WaitForMessageAsync. Publishers signal waiting consumers immediately after store append.
This commit is contained in:
@@ -1694,7 +1694,14 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
var numPending = batch - delivered;
|
||||
var ackReply = BuildAckReply(ackPrefix, message.Sequence, deliverySeq, tsNanos, numPending);
|
||||
|
||||
DeliverMessage(inboxSub, message.Subject, ackReply, minHeaders, message.Payload);
|
||||
// Bypass DeliverMessage — we already know the target client is sender.
|
||||
// Skip permission check and auto-unsub overhead for JS delivery inbox.
|
||||
// Go reference: consumer.go — batch delivery with deferred flush.
|
||||
sender.SendMessageNoFlush(message.Subject, inboxSub.Sid, ackReply, minHeaders, message.Payload);
|
||||
|
||||
// Batch flush every 64 messages to amortize write-loop wakeup cost.
|
||||
if ((delivered & 63) == 0)
|
||||
sender.SignalFlush();
|
||||
|
||||
if (consumer.Config.AckPolicy is JetStream.Models.AckPolicy.Explicit or JetStream.Models.AckPolicy.All)
|
||||
{
|
||||
@@ -1708,6 +1715,11 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
// Flush any buffered messages before blocking on the signal.
|
||||
// Without this, messages queued via SendMessageNoFlush would sit
|
||||
// in the buffer until the next batch boundary or loop exit.
|
||||
sender.SignalFlush();
|
||||
|
||||
// No message available — send idle heartbeat if needed
|
||||
if (DateTime.UtcNow - lastDeliveryTime >= hbInterval)
|
||||
{
|
||||
@@ -1719,8 +1731,17 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
lastDeliveryTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// Poll briefly before retrying
|
||||
await Task.Delay(5, ct).ConfigureAwait(false);
|
||||
// Wait for publisher to signal a new message, with a heartbeat-interval
|
||||
// timeout so the heartbeat check is re-evaluated periodically.
|
||||
// Go reference: consumer.go — channel signaling from publisher.
|
||||
try
|
||||
{
|
||||
await streamHandle.WaitForPublishAsync(ct).WaitAsync(hbInterval, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
// Heartbeat interval elapsed — loop back to re-check
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1729,13 +1750,16 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
||||
// ExpiresMs timeout — expected
|
||||
}
|
||||
|
||||
// Final flush for any remaining batched messages
|
||||
sender.SignalFlush();
|
||||
|
||||
consumer.NextSequence = sequence;
|
||||
|
||||
// Send terminal status
|
||||
// Send terminal status directly to sender (last message, includes flush)
|
||||
ReadOnlyMemory<byte> statusHeader = delivered == 0
|
||||
? System.Text.Encoding.UTF8.GetBytes("NATS/1.0 404 No Messages\r\n\r\n")
|
||||
: System.Text.Encoding.UTF8.GetBytes("NATS/1.0 408 Request Timeout\r\n\r\n");
|
||||
DeliverMessage(inboxSub, replyTo, null, statusHeader, default);
|
||||
sender.SendMessage(replyTo, inboxSub.Sid, null, statusHeader, default);
|
||||
}
|
||||
|
||||
private void DeliverFetchedMessages(Subscription inboxSub, string streamName, string consumerName,
|
||||
|
||||
Reference in New Issue
Block a user