using System.Collections.Concurrent; using Microsoft.Extensions.Options; using StellaOps.Router.Common.Models; namespace StellaOps.Gateway.WebService.Middleware; /// /// Tracks payload bytes across requests, connections, and globally. /// public interface IPayloadTracker { /// /// Tries to reserve capacity for an estimated payload size. /// /// The connection identifier. /// The estimated bytes to reserve. /// True if capacity was reserved; false if limits would be exceeded. bool TryReserve(string connectionId, long estimatedBytes); /// /// Releases previously reserved capacity. /// /// The connection identifier. /// The actual bytes to release. void Release(string connectionId, long actualBytes); /// /// Gets the current total inflight bytes across all connections. /// long CurrentInflightBytes { get; } /// /// Gets a value indicating whether the system is overloaded. /// bool IsOverloaded { get; } /// /// Gets the current inflight bytes for a specific connection. /// /// The connection identifier. /// The current inflight bytes for the connection. long GetConnectionInflightBytes(string connectionId); } /// /// Default implementation of . /// public sealed class PayloadTracker : IPayloadTracker { private readonly PayloadLimits _limits; private readonly ILogger _logger; private long _totalInflightBytes; private readonly ConcurrentDictionary _perConnectionBytes = new(); /// /// Initializes a new instance of the class. /// public PayloadTracker(IOptions limits, ILogger logger) { _limits = limits.Value; _logger = logger; } /// public long CurrentInflightBytes => Interlocked.Read(ref _totalInflightBytes); /// public bool IsOverloaded => CurrentInflightBytes > _limits.MaxAggregateInflightBytes; /// public bool TryReserve(string connectionId, long estimatedBytes) { // Check aggregate limit var newTotal = Interlocked.Add(ref _totalInflightBytes, estimatedBytes); if (newTotal > _limits.MaxAggregateInflightBytes) { Interlocked.Add(ref _totalInflightBytes, -estimatedBytes); _logger.LogWarning( "Aggregate payload limit exceeded. Current: {Current}, Limit: {Limit}", newTotal - estimatedBytes, _limits.MaxAggregateInflightBytes); return false; } // Check per-connection limit var connectionBytes = _perConnectionBytes.AddOrUpdate( connectionId, estimatedBytes, (_, current) => current + estimatedBytes); if (connectionBytes > _limits.MaxRequestBytesPerConnection) { // Roll back _perConnectionBytes.AddOrUpdate( connectionId, 0, (_, current) => current - estimatedBytes); Interlocked.Add(ref _totalInflightBytes, -estimatedBytes); _logger.LogWarning( "Per-connection payload limit exceeded for {ConnectionId}. Current: {Current}, Limit: {Limit}", connectionId, connectionBytes - estimatedBytes, _limits.MaxRequestBytesPerConnection); return false; } return true; } /// public void Release(string connectionId, long actualBytes) { Interlocked.Add(ref _totalInflightBytes, -actualBytes); _perConnectionBytes.AddOrUpdate( connectionId, 0, (_, current) => Math.Max(0, current - actualBytes)); } /// public long GetConnectionInflightBytes(string connectionId) { return _perConnectionBytes.TryGetValue(connectionId, out var bytes) ? bytes : 0; } }