using w4c_workflows.Services.Security;
namespace w4c_workflows.Services.Nodes;
///
/// Enforces during a run. This is resource
/// governance (a per-run outbound-request and response-size ceiling), not
/// authorization, so it lives with node execution rather than under
/// Security/.
///
/// The request budget is kept in the run-scoped
/// NodeExecutionContext.State under , so
/// the count is shared by every HTTP node in the same run and is created fresh
/// per run (and per sub-workflow run).
///
public sealed class NodeRequestBudget
{
/// Run-state key holding the outbound request counter.
public const string RequestCounterKey = "node.quota.outboundRequests";
private readonly NodeQuotaOptions _options;
public NodeRequestBudget(NodeQuotaOptions options) => _options = options;
/// Response-body cap in bytes; 0 means unlimited.
public long MaxResponseBytes => _options.MaxResponseBytes;
/// Outbound-request cap per run; 0 means unlimited.
public int MaxRequestsPerRun => _options.MaxRequestsPerRun;
///
/// Reserves one outbound request from the run budget. Returns false
/// once the run has consumed its allowance; is the
/// post-increment count so the caller can report it. The counter is a single
/// mutable object in , so concurrent item execution
/// still reserves exactly once per request.
///
public bool TryReserveRequest(IDictionary state, out int used)
{
used = 0;
if (_options.MaxRequestsPerRun <= 0)
return true;
if (!state.TryGetValue(RequestCounterKey, out var existing) || existing is not RequestCounter counter)
{
counter = new RequestCounter();
state[RequestCounterKey] = counter;
}
used = counter.Increment();
return used <= _options.MaxRequestsPerRun;
}
/// True when a response of bytes fits the cap.
public bool IsResponseWithinLimit(long length)
=> _options.MaxResponseBytes <= 0 || length <= _options.MaxResponseBytes;
private sealed class RequestCounter
{
private int _value;
public int Increment() => Interlocked.Increment(ref _value);
}
}