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