w4c-workflows-api/Services/Security/NodeQuotaPolicy.cs
2026-09-12 01:02:46 +03:00

58 lines
2.2 KiB
C#

namespace w4c_workflows.Services.Security;
/// <summary>
/// Enforces <see cref="NodeQuotaOptions"/> during a run. 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 NodeQuotaPolicy
{
/// <summary>Run-state key holding the outbound request counter.</summary>
public const string RequestCounterKey = "node.quota.outboundRequests";
private readonly NodeQuotaOptions _options;
public NodeQuotaPolicy(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);
}
}