w4c-workflows-api/Services/Nodes/NodeRequestBudget.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

65 lines
2.5 KiB
C#

using w4c_workflows.Services.Security;
namespace w4c_workflows.Services.Nodes;
/// <summary>
/// Enforces <see cref="NodeQuotaOptions"/> 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
/// <c>Security/</c>.
///
/// The request budget is kept in the run-scoped
/// <c>NodeExecutionContext.State</c> under <see cref="RequestCounterKey"/>, so
/// the count is shared by every HTTP node in the same run and is created fresh
/// per run (and per sub-workflow run).
/// </summary>
public sealed class NodeRequestBudget
{
/// <summary>Run-state key holding the outbound request counter.</summary>
public const string RequestCounterKey = "node.quota.outboundRequests";
private readonly NodeQuotaOptions _options;
public NodeRequestBudget(NodeQuotaOptions options) => _options = options;
/// <summary>Response-body cap in bytes; <c>0</c> means unlimited.</summary>
public long MaxResponseBytes => _options.MaxResponseBytes;
/// <summary>Outbound-request cap per run; <c>0</c> means unlimited.</summary>
public int MaxRequestsPerRun => _options.MaxRequestsPerRun;
/// <summary>
/// Reserves one outbound request from the run budget. Returns <c>false</c>
/// once the run has consumed its allowance; <paramref name="used"/> is the
/// post-increment count so the caller can report it. The counter is a single
/// mutable object in <paramref name="state"/>, so concurrent item execution
/// still reserves exactly once per request.
/// </summary>
public bool TryReserveRequest(IDictionary<string, object?> 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;
}
/// <summary>True when a response of <paramref name="length"/> bytes fits the cap.</summary>
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);
}
}