314 lines
8.9 KiB
C#
314 lines
8.9 KiB
C#
namespace ZB.MOM.NatsNet.Server;
|
|
|
|
internal sealed partial class NatsConsumer
|
|
{
|
|
internal (JsPubMsg? Message, ulong DeliveryCount, Exception? Error) GetNextMsg()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_closed)
|
|
return (null, 0, new InvalidOperationException("consumer not valid"));
|
|
|
|
if (_state.Pending is { Count: > 0 })
|
|
{
|
|
var sequence = _state.Pending.Keys.Min();
|
|
var deliveryCount = IncDeliveryCount(sequence);
|
|
var message = new JsPubMsg
|
|
{
|
|
Subject = Config.DeliverSubject ?? string.Empty,
|
|
Reply = AckReply(sequence, _state.Delivered.Consumer + 1, deliveryCount, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), NumPending()),
|
|
Msg = [],
|
|
};
|
|
return (message, deliveryCount, null);
|
|
}
|
|
|
|
return (null, 0, null);
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal (int Expired, int Waiting, int BatchRequestsPending, DateTime FirstExpiration) ProcessWaiting(bool endOfStream)
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
var firstExpiration = DateTime.MinValue;
|
|
if (_waiting is null || _waiting.IsEmpty())
|
|
return (0, 0, 0, firstExpiration);
|
|
|
|
var expired = 0;
|
|
var batchRequestsPending = 0;
|
|
var now = DateTime.UtcNow;
|
|
var toRemove = new List<WaitingRequest>();
|
|
|
|
foreach (var waitingRequest in _waiting.Snapshot())
|
|
{
|
|
var isExpired = waitingRequest.Expires is DateTime expiresAt && now >= expiresAt;
|
|
if ((endOfStream && waitingRequest.NoWait == 1) || isExpired)
|
|
{
|
|
toRemove.Add(waitingRequest);
|
|
expired++;
|
|
continue;
|
|
}
|
|
|
|
batchRequestsPending += Math.Max(0, waitingRequest.N);
|
|
if (waitingRequest.Expires is DateTime wrExpires &&
|
|
(firstExpiration == DateTime.MinValue || wrExpires < firstExpiration))
|
|
{
|
|
firstExpiration = wrExpires;
|
|
}
|
|
}
|
|
|
|
foreach (var waitingRequest in toRemove)
|
|
_waiting.Remove(null, waitingRequest);
|
|
|
|
return (expired, _waiting.Len, batchRequestsPending, firstExpiration);
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal bool CheckWaitingForInterest()
|
|
{
|
|
var (_, waiting, _, _) = ProcessWaiting(endOfStream: true);
|
|
return waiting > 0;
|
|
}
|
|
|
|
internal (TimeSpan Duration, Timer? Timer) HbTimer()
|
|
{
|
|
if (Config.Heartbeat <= TimeSpan.Zero)
|
|
return (TimeSpan.Zero, null);
|
|
|
|
return (Config.Heartbeat, new Timer(static _ => { }, null, Config.Heartbeat, Timeout.InfiniteTimeSpan));
|
|
}
|
|
|
|
internal void CheckAckFloor()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_closed || _state.Pending is not { Count: > 0 })
|
|
return;
|
|
|
|
var minPending = _state.Pending.OrderBy(static pair => pair.Key).First();
|
|
var pendingStream = minPending.Key;
|
|
var pendingConsumer = minPending.Value?.Sequence ?? pendingStream;
|
|
|
|
var desiredStreamFloor = pendingStream > 0 ? pendingStream - 1 : 0;
|
|
var desiredConsumerFloor = pendingConsumer > 0 ? pendingConsumer - 1 : 0;
|
|
|
|
if (_state.AckFloor.Stream < desiredStreamFloor)
|
|
_state.AckFloor.Stream = desiredStreamFloor;
|
|
|
|
if (_state.AckFloor.Consumer < desiredConsumerFloor)
|
|
_state.AckFloor.Consumer = desiredConsumerFloor;
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal int ProcessInboundAcks(CancellationToken cancellationToken)
|
|
{
|
|
var processed = 0;
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
JsAckMsg? ack;
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_ackQueue.Count == 0)
|
|
break;
|
|
ack = _ackQueue.Dequeue();
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
|
|
ProcessAck(ack.Subject, ack.Reply, ack.HeaderBytes, ack.Msg);
|
|
processed++;
|
|
}
|
|
|
|
return processed;
|
|
}
|
|
|
|
internal int ProcessInboundNextMsgReqs(CancellationToken cancellationToken)
|
|
{
|
|
var processed = 0;
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
NextMsgReq? request;
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_nextMsgReqs is null || _nextMsgReqs.Count == 0)
|
|
break;
|
|
request = _nextMsgReqs.Dequeue();
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
|
|
_ = ProcessNextMsgRequest(request.Reply, request.Message);
|
|
request.ReturnToPool();
|
|
processed++;
|
|
}
|
|
|
|
return processed;
|
|
}
|
|
|
|
internal void SuppressDeletion()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_closed || _deleteTimer is null || _deleteThreshold <= TimeSpan.Zero)
|
|
return;
|
|
|
|
_deleteTimer.Change(_deleteThreshold, Timeout.InfiniteTimeSpan);
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal int LoopAndGatherMsgs(int maxIterations, CancellationToken cancellationToken)
|
|
{
|
|
var delivered = 0;
|
|
for (var i = 0; i < maxIterations && !cancellationToken.IsCancellationRequested; i++)
|
|
{
|
|
var (message, _, error) = GetNextMsg();
|
|
if (error is not null || message is null)
|
|
break;
|
|
|
|
delivered++;
|
|
_state.Delivered.Stream = Math.Max(_state.Delivered.Stream, (ulong)delivered);
|
|
_state.Delivered.Consumer = Math.Max(_state.Delivered.Consumer, (ulong)delivered);
|
|
|
|
var expired = ProcessWaiting(endOfStream: false);
|
|
if (expired.Waiting == 0)
|
|
break;
|
|
}
|
|
|
|
return delivered;
|
|
}
|
|
|
|
internal string SendIdleHeartbeat(string subject)
|
|
{
|
|
var streamSequence = _state.Delivered.Stream;
|
|
var consumerSequence = _state.Delivered.Consumer;
|
|
var heartbeat = $"NATS/1.0 100 Idle Heartbeat\r\nNats-Last-Consumer: {consumerSequence}\r\nNats-Last-Stream: {streamSequence}\r\n\r\n";
|
|
|
|
_ = SendAdvisory(subject, heartbeat);
|
|
return heartbeat;
|
|
}
|
|
|
|
internal string AckReply(ulong streamSequence, ulong deliverySequence, ulong deliveryCount, long timestamp, ulong pending) =>
|
|
$"$JS.ACK.{deliveryCount}.{streamSequence}.{deliverySequence}.{timestamp}.{pending}";
|
|
|
|
internal void SetMaxPendingBytes(int limit)
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
_maxPendingBytesLimit = Math.Max(0, limit);
|
|
_maxPendingBytesThreshold = Math.Max(1, _maxPendingBytesLimit / 16);
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal (ulong Pending, Exception? Error) CheckNumPending()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_npc < 0)
|
|
_npc = 0;
|
|
|
|
var (pending, floor, error) = CalculateNumPending();
|
|
if (error is not null)
|
|
return (0, error);
|
|
|
|
_npc = (long)pending;
|
|
_npf = floor;
|
|
return (NumPending(), null);
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal ulong NumPending() => _npc < 0 ? 0UL : (ulong)_npc;
|
|
|
|
internal void CheckNumPendingOnEOF()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_npc < 0)
|
|
_npc = 0;
|
|
|
|
if (_state.Delivered.Stream <= _state.AckFloor.Stream)
|
|
{
|
|
_npc = 0;
|
|
_npf = _state.Delivered.Stream;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal (ulong Pending, Exception? Error) StreamNumPendingLocked()
|
|
{
|
|
_mu.EnterWriteLock();
|
|
try
|
|
{
|
|
return StreamNumPending();
|
|
}
|
|
finally
|
|
{
|
|
_mu.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
internal (ulong Pending, Exception? Error) StreamNumPending()
|
|
{
|
|
var (pending, floor, error) = CalculateNumPending();
|
|
if (error is not null)
|
|
return (0, error);
|
|
|
|
_npc = (long)pending;
|
|
_npf = floor;
|
|
return (NumPending(), null);
|
|
}
|
|
|
|
internal (ulong Pending, ulong Floor, Exception? Error) CalculateNumPending()
|
|
{
|
|
if (_closed)
|
|
return (0, 0, null);
|
|
|
|
var delivered = _state.Delivered.Stream;
|
|
var acked = _state.AckFloor.Stream;
|
|
if (delivered <= acked)
|
|
return (0, delivered, null);
|
|
|
|
return (delivered - acked, delivered, null);
|
|
}
|
|
}
|