File: Internal\RetryBudgetStrategy.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Http.Resilience\Microsoft.Extensions.Http.Resilience.csproj (Microsoft.Extensions.Http.Resilience)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;
using Polly.Telemetry;
 
namespace Microsoft.Extensions.Http.Resilience.Internal;
 
/// <summary>
/// Records one request per execution in the retry budget and exposes the budget to the retry strategy that it wraps.
/// </summary>
/// <remarks>
/// This strategy is added immediately before (outside) the retry strategy. While the execution is in progress,
/// the strategy is stored in <see cref="ResilienceContext.Properties"/> so that the retry predicate created by
/// <see cref="WrapShouldHandle"/> can consult the budget of the pipeline instance that is executing.
/// </remarks>
internal sealed class RetryBudgetStrategy : ResilienceStrategy
{
    internal const string BudgetExhaustedEventName = "OnRetryBudgetExhausted";
 
    private static readonly ResiliencePropertyKey<RetryBudgetStrategy?> _budgetKey = new("Resilience.Http.RetryBudget");
    private static readonly ResilienceEvent _budgetExhaustedEvent = new(ResilienceEventSeverity.Warning, BudgetExhaustedEventName);
 
    private readonly RetryBudget _budget;
    private readonly ResilienceStrategyTelemetry _telemetry;
 
    public RetryBudgetStrategy(RetryBudget budget, ResilienceStrategyTelemetry telemetry)
    {
        _budget = budget;
        _telemetry = telemetry;
    }
 
    public static Func<RetryPredicateArguments<HttpResponseMessage>, ValueTask<bool>> WrapShouldHandle(
        Func<RetryPredicateArguments<HttpResponseMessage>, ValueTask<bool>> shouldHandle,
        int maxRetryAttempts)
    {
        return args =>
        {
            var handled = shouldHandle(args);
 
            if (handled.IsCompletedSuccessfully)
            {
#pragma warning disable VSTHRD002 // The value task has already completed, so reading its result doesn't block.
                return new ValueTask<bool>(EvaluateBudget(handled.Result, args, maxRetryAttempts));
#pragma warning restore VSTHRD002
            }
 
            return EvaluateBudgetAsync(handled, args, maxRetryAttempts);
        };
 
        static async ValueTask<bool> EvaluateBudgetAsync(ValueTask<bool> handled, RetryPredicateArguments<HttpResponseMessage> args, int maxRetryAttempts)
            => EvaluateBudget(await handled.ConfigureAwait(args.Context.ContinueOnCapturedContext), args, maxRetryAttempts);
    }
 
    protected override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
        Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
        ResilienceContext context,
        TState state)
    {
        _budget.RecordRequest();
 
        // Restore the previous value so that nested pipelines with their own budget don't leak into this one.
        _ = context.Properties.TryGetValue(_budgetKey, out var previous);
        context.Properties.Set(_budgetKey, this);
 
        try
        {
            return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext);
        }
        finally
        {
            context.Properties.Set(_budgetKey, previous);
        }
    }
 
    private static bool EvaluateBudget(bool handled, RetryPredicateArguments<HttpResponseMessage> args, int maxRetryAttempts)
    {
        // Polly evaluates the predicate after the last attempt too; that evaluation must not consume the budget
        // because no retry follows it.
        if (!handled || args.AttemptNumber >= maxRetryAttempts)
        {
            return handled;
        }
 
        if (!args.Context.Properties.TryGetValue(_budgetKey, out var strategy) || strategy is null)
        {
            return true;
        }
 
        return strategy.TryAcquireRetry(args);
    }
 
    private bool TryAcquireRetry(RetryPredicateArguments<HttpResponseMessage> args)
    {
        if (_budget.TryAcquireRetry())
        {
            return true;
        }
 
        _telemetry.Report(_budgetExhaustedEvent, args.Context, args.Outcome, new OnRetryBudgetExhaustedArguments(args.AttemptNumber));
        return false;
    }
}