< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Providers.TieredProviderSelector<T1, T2>
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/TieredProviderSelector.cs
Line coverage
100%
Covered lines: 40
Uncovered lines: 0
Coverable lines: 40
Total lines: 107
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%22100%
.ctor(...)100%88100%
ExecuteAsync()100%66100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/TieredProviderSelector.cs

#LineLine coverage
 1using NexusLabs.Needlr.AgentFramework.Context;
 2
 3namespace NexusLabs.Needlr.AgentFramework.Providers;
 4
 5/// <summary>
 6/// Default <see cref="ITieredProviderSelector{TQuery, TResult}"/> that iterates providers
 7/// in ascending <see cref="ITieredProvider{TQuery, TResult}.Priority"/> order, gated by
 8/// an <see cref="IQuotaGate"/>. On <see cref="ProviderUnavailableException"/> the selector
 9/// falls through to the next provider.
 10/// </summary>
 11/// <remarks>
 12/// <para>
 13/// The quota partition key is resolved from the ambient
 14/// <see cref="IAgentExecutionContextAccessor"/> using a <see cref="QuotaPartitionSelector"/>.
 15/// By default, <see cref="IAgentExecutionContext.UserId"/> is used as the partition.
 16/// Consumers that need a different partitioning strategy (e.g., tenant ID, API key)
 17/// can provide a custom <see cref="QuotaPartitionSelector"/> via the constructor.
 18/// </para>
 19/// <para>
 20/// When no execution context is active (e.g., during integration tests that don't
 21/// establish a scope), the partition is <see langword="null"/> and quota is global.
 22/// </para>
 23/// </remarks>
 24[DoNotAutoRegister]
 25public sealed class TieredProviderSelector<TQuery, TResult> : ITieredProviderSelector<TQuery, TResult>
 26{
 27    private readonly IReadOnlyList<ITieredProvider<TQuery, TResult>> _providers;
 28    private readonly IQuotaGate _quotaGate;
 29    private readonly IAgentExecutionContextAccessor _contextAccessor;
 30    private readonly QuotaPartitionSelector _partitionSelector;
 31
 32    /// <summary>
 33    /// The default partition selector: uses <see cref="IAgentExecutionContext.UserId"/>
 34    /// from the ambient context.
 35    /// </summary>
 136    public static readonly QuotaPartitionSelector DefaultPartitionSelector =
 1237        context => context?.UserId;
 38
 39    /// <param name="providers">All registered providers (filtering and ordering is handled internally).</param>
 40    /// <param name="quotaGate">Quota gate for reservation/release. Use <see cref="AlwaysGrantQuotaGate"/> for no-op.</p
 41    /// <param name="contextAccessor">Accessor for ambient execution context (provides partition identity).</param>
 42    /// <param name="partitionSelector">
 43    /// Custom partition selector. Defaults to <see cref="DefaultPartitionSelector"/>
 44    /// (<see cref="IAgentExecutionContext.UserId"/>).
 45    /// </param>
 1646    public TieredProviderSelector(
 1647        IEnumerable<ITieredProvider<TQuery, TResult>> providers,
 1648        IQuotaGate quotaGate,
 1649        IAgentExecutionContextAccessor contextAccessor,
 1650        QuotaPartitionSelector? partitionSelector = null)
 51    {
 1652        ArgumentNullException.ThrowIfNull(providers);
 1553        ArgumentNullException.ThrowIfNull(quotaGate);
 1454        ArgumentNullException.ThrowIfNull(contextAccessor);
 55
 1356        _providers = providers
 2057            .Where(p => p.IsEnabled)
 1358            .OrderBy(p => p.Priority)
 1359            .ThenBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
 1360            .ToList();
 61
 1362        _quotaGate = quotaGate;
 1363        _contextAccessor = contextAccessor;
 1364        _partitionSelector = partitionSelector ?? DefaultPartitionSelector;
 1365    }
 66
 67    /// <inheritdoc />
 68    public async Task<TResult> ExecuteAsync(TQuery query, CancellationToken cancellationToken)
 69    {
 1370        if (_providers.Count == 0)
 171            throw new InvalidOperationException("No enabled providers are registered.");
 72
 1273        var partition = _partitionSelector(_contextAccessor.Current);
 1274        var attempts = new List<string>();
 75
 4576        foreach (var provider in _providers)
 77        {
 1678            if (!await _quotaGate.TryReserveAsync(provider.Name, partition, cancellationToken)
 1679                .ConfigureAwait(false))
 80            {
 181                attempts.Add($"{provider.Name}: quota denied");
 182                continue;
 83            }
 84
 85            try
 86            {
 1587                var result = await provider.ExecuteAsync(query, cancellationToken)
 1588                    .ConfigureAwait(false);
 89
 1190                await _quotaGate.ReleaseAsync(provider.Name, partition, succeeded: true, cancellationToken)
 1191                    .ConfigureAwait(false);
 92
 1193                return result;
 94            }
 95            catch (ProviderUnavailableException ex)
 96            {
 497                attempts.Add($"{provider.Name}: {ex.Message}");
 98
 499                await _quotaGate.ReleaseAsync(provider.Name, partition, succeeded: false, cancellationToken)
 4100                    .ConfigureAwait(false);
 101            }
 4102        }
 103
 1104        throw new InvalidOperationException(
 1105            $"All providers failed. Attempts: [{string.Join("; ", attempts)}]");
 11106    }
 107}