65 lines
2.5 KiB
C#
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);
|
|
}
|
|
}
|