Search code examples
c#.net-8.0pollyretry-logiccircuit-breaker

Leveraging AddStandardResilienceHandler and AddStandardHedgingHandler in .NET 8 for Resiliency


I have a .NET 8 API using Polly for resiliency with external services like Azure SQL and Microsoft Graph. My current implementation uses custom retry and circuit breaker policies as shown below:

PollyExtensions.cs:

public static class PollyExtensions
{
    public static AsyncRetryPolicy CreateRetryPolicy(this ILogger logger, string infraService)
    {
        var delay = Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 5);

        return Policy
            .Handle<SqlException>()
            .Or<Exception>()
            .Or<TimeoutException>()
            .WaitAndRetryAsync(delay,
                onRetry: (exception, timespan, retryAttempt, context) =>
                {
                    logger.LogWarning(exception, "Error talking to {infraService}, Error: {Message}, will retry after {timeSpan}. Retry attempt {retryCount} ",
                        infraService, exception.Message, timespan, retryAttempt);
                });
    }

    public static AsyncCircuitBreakerPolicy CreateCircuitBreakerPolicy()
    {
        return Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1)
            );
    }
}

While exploring new features in .NET 8, I came across AddStandardResilienceHandler and AddStandardHedgingHandler for implementing resiliency.

References :

https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/

https://juliocasal.com/blog/Building-Microservices-With-Dotnet-8

Can someone provide code samples demonstrating how to use AddStandardResilienceHandler and AddStandardHedgingHandler in the context of my GraphSvcClient class for external dependency calls?

Are there any specific considerations when using these new functionalities compared to custom policies?

GraphSvcClient.cs

public sealed class GraphSvcClient(ICacheProvider cacheProvider,
    IAzureAuthTokenService azureAuthTokenService,    
    ILogger<GraphSvcClient> logger) : IGraphSvcClient
{
    private readonly ICacheProvider _cacheProvider = cacheProvider.IsNotNull();
    private readonly IAzureAuthTokenService _azureAuthTokenService = azureAuthTokenService.IsNotNull();
    private readonly ILogger<GraphSvcClient> _logger = logger.IsNotNull();
    
    /// <summary>
    /// Get Microsoft Graph graphClient
    /// </summary>
    /// <returns></returns>
    public async Task<GraphServiceClient> GetGraphServiceClientAsync()
    {
        var accessToken = await GetAccessTokenAsync();

        var retryPolicy = _logger.CreateRetryPolicy(infraService: "MicrosoftGraph");
        var circuitBreakerPolicy = PollyExtensions.CreateCircuitBreakerPolicy();

        var combinedPolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);

        var graphServiceClient = await combinedPolicy.ExecuteAsync(async () =>
        {
            var client = new GraphServiceClient(new DelegateAuthenticationProvider(async (req) =>
            {
                req.Headers.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, accessToken);
                await Task.CompletedTask;
            }));
            return client;
        });

        return graphServiceClient;
    }
}

I'm considering updating Polly and its related nuget packages to their latest compatible versions to leverage the latest features in .NET 8.

List of Nuget Packages used in the project :

<PackageReference Include="Polly" Version="7.2.4" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />

Solution

  • I can see some misunderstandings so, let me try to clarify stuff.

    Microsoft.Extensions.Http.XYZ packages

    Polly by design is domain-agnostic. In other words, it can be used for HTTP calls related transient fault handing as well as for mathematical calculations. The policies (in case of V7) are generic enough to be used for a huge variety of use cases.

    One common use case is the HttpClient based network communication. Because of its popularity, first the Polly community then Microsoft has released several helper structures and methods to ease the integration and usage of Polly with HttpClient.

    For the V7 API Microsoft has released the Microsoft.Extensions.Http.Polly which helps you to decorate all HttpClient calls with arbitrary resiliency policies via the AddPolicyHandler extension method.

    For the V8 API Microsoft has released the Microsoft.Extensions.Http.Resilience package which helps you to decorate all HttpClient calls with either predefined resiliency pipelines or any arbitrary resiliency strategies. The former can be done via the AddStandardResilienceHandler, AddStandardHedgingHandler extension methods, whereas the latter via the AddResilienceHandler.

    Migrating your code to V8

    Retry

    Your retry strategy can be easily migrated to V8. Gladly the exponential backoff algorithm is supported natively. That means you don't have to rely on some contrib dependency.

    return new ResiliencePipelineBuilder().AddRetry(new()
    {
        ShouldHandle = new PredicateBuilder()
            .Handle<SqlException>()
            .Handle<TimeoutException>()
            .Handle<Exception>(),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        MaxRetryAttempts = 5,
        Delay = TimeSpan.FromSeconds(1),
        OnRetry = args =>
        {
            logger...
            return default;
        }
    })
    .Build();
    

    Please note that if you have an Handle<Exception>/Or<Exception> clause in case of V7 or Handle<Exception> clause in case of V8 then all the other Handle/Or clauses are not necessary.

    Circuit Breaker

    Currently the V8 API does not support this standard/classic Circuit Breaker model which is based on the successive failure counting. V8 support the sampling based model which is similar to the V7's Advanced Circuit Breaker.

    So, in short you can't migrate this as is. You have to use the sampling based approach if you want to use the V8 API.

    Policy.Wrap

    Combining multiple policies in the V7 API could be done in several ways, but the suggested solution was the usage of Policy.Wrap{Async}.

    In case of V8 you have to use the ResiliencePipelineBuilder to add multiple strategies to your pipeline.

    ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
        .AddRetry(new()
        {
            ...
        })
        .AddCircuitBreaker(new()
        {
            ...
        })
        .Build();
    

    In case of V8 the registration order is also vital, just like in case of V7.

    Your Graph Service

    Your original policies were not Http related. Your V8 strategies also don't have to be Http related. That means the usage of AddStandardResilienceHandler or AddStandardHedgingHandler is not needed.

    Based on the shared code fragment I can't tell whether the GraphServiceClient is a typed HttpClient or not (but I assume it's not). So, you just have to migrate your retry and circuit breaker policies to V8 to be able to harvest the V8 goodnesses.