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