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(); 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); } }