No service for type 'Polly.Registry.IReadOnlyPolicyRegistry`1[System.String]' has been registered

0

There is an extension method that registers IAccountSearchServiceClient with some policy handlers looks like Polly lib is used

public static IServiceCollection AddAccountSearchServiceClient(this IServiceCollection services)
{
    services.AddHttpClient<AccountSearchServiceClient>()
            .ConfigureHttpClient((sp, client) =>
             {
                 var options = sp.GetRequiredService<IOptions<AccountSearchSettings>>();
                 var settings = options.Value;
                 client.BaseAddress = settings.BaseUrl;
             })
            .ConfigurePrimaryHttpMessageHandler(() =>
             {
                 var handler = new HttpClientHandler
                 {
                     ClientCertificateOptions = ClientCertificateOption.Manual,
                     ServerCertificateCustomValidationCallback = (m, c, cc, pe) => true
                 };
                 return handler;
             })
            .AddPolicyHandler(request => request.Method == HttpMethod.Get ? Policies.ShortTimeout : Policies.LongTimeout)
            .AddPolicyHandlerFromRegistry("circuitBreaker")
            .AddPolicyHandlerFromRegistry("bulkhead")
            .AddPolicyHandlerFromRegistry("RetryPolicy");

    services.AddScoped<IAccountSearchServiceClient>(sp => sp.GetRequiredService<AccountSearchServiceClient>());

    return services;
}

at runtime getting such a DI error:

System.InvalidOperationException HResult=0x80131509 Message=No service for type 'Polly.Registry.IReadOnlyPolicyRegistry`1[System.String]' has been registered.
Source=Microsoft.Extensions.DependencyInjection.Abstractions

the error occurs here

sp.GetRequiredService<AccountSearchServiceClient>()

I'm not very familiar with Polly. Is there something missing?
I've put a break point on a constructor but the ctor is not called error happens earlier after ConfigurePrimaryHttpMessageHandler

the consturctor looks as the following:

public AccountSearchServiceClient(HttpClient httpClient, IOptions<AccountSearchSettings> settings)
{
    _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    _settings = settings?.Value ?? throw new ArgumentNullException(nameof(settings));
}

no direct injections or usages of IReadOnlyPolicyRegistry I guess it's something internal type of Polly

.net-core
dotnet-httpclient
polly
asked on Stack Overflow Jan 5, 2021 by Artem Vertiy • edited Jan 6, 2021 by Peter Csala

2 Answers

0

It turned out the missing part was the policy registration:

var policyRegistry = services.AddPolicyRegistry();

policyRegistry["circuitBreaker"] = HttpPolicyExtensions.HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(20));

policyRegistry["RetryPolicy"] = ...;

//etc.
answered on Stack Overflow Jan 5, 2021 by Artem Vertiy • edited Jan 6, 2021 by Peter Csala
0

Problem

Even though it seems quite appealing to have a registry with a full of policies unfortunately it is error-prone. At the first glace it seems that it provides great flexibility by allowing to the consumer to be able to combine whatever policies he/she wants.

But there is a problem, which is related to the escalation. If the inner policy fails then Polly will escalate the problem to the next outer policy (in the policy chain) which may or may not aware of the inner policy.

Example #1

Let's stick with your example where you have a Retry and a Circuit Breaker policies.
The consumer can register them in the following two orders:

  1. Circuit Breaker >> Retry
  2. Retry >> Circuit Breaker
  • In the former case the retry will not throw any Polly specific exception.
  • But in the latter case Polly might throw BrokenCircuitException.
    • So depending on the use case your retry policy might need to be aware of that.

Example #2

Let's suppose you have a Timeout policy as well. This is where things can get quite complicated.
The Timeout can be used either as a local or a global timeout:

  1. Retry >> Circuit Breaker >> Timeout
  2. Timeout >> Retry >> Circuit Breaker

+1. Timeout >> Retry >> Circuit Breaker >> Timeout

  • In the first case your Timeout should not be aware of the any other Polly policies, but Circuit Breaker and Retry might need to be aware of the TimeoutRejectedException.
  • In the second case your Timeout might need to be aware of BrokenCircuitException...
  • In the bonus case how can you define a single policy which could be reused for local and global timeouts as well?

Design

Fortunately Polly can help us with Wrap. With this you can explicitly combine two or more policies in a given order. This helps you define strategies not just individual policies.

So instead of exposing policies you can expose strategies which combines a set of policies to achieve the desired behaviour.

Let's stick with the Retry >> Circuit Breaker >> Timeout example:

  1. Timeout aware of: -
  2. Circuit breaker aware of: HttpRequestExcetion, TimeotRejectedException
  3. Retry aware of: HttpRequestExcetion, TimeotRejectedException, BrokenCircuitException

Solution

Let's suppose we have the following abstraction to specify the parameters of the above three policies:

public class ResilienceSettings
{
    public string BaseAddress { get; set; }

    public int HttpRequestTimeoutInMilliseconds { get; set; }

    public int HttpRequestRetrySleepDurationInMilliseconds { get; set; }

    public int HttpRequestRetryCount { get; set; }

    public int HttpRequestCircuitBreakerFailCountInCloseState { get; set; }

    public int HttpRequestCircuitBreakerDelayInMillisecondsBetweenOpenAndHalfOpenStates { get; set; }
}

Now, let's see the policies:

private static IAsyncPolicy<HttpResponseMessage> TimeoutPolicy(ResilienceSettings settings)
    => Policy
        .TimeoutAsync<HttpResponseMessage>( //Catches TaskCanceledException and throws instead TimeoutRejectedException
            timeout: TimeSpan.FromMilliseconds(settings.HttpRequestTimeoutInMilliseconds));

private static IAsyncPolicy<HttpResponseMessage> CircuitBreakerPolicy(ResilienceSettings settings)
    => HttpPolicyExtensions
        .HandleTransientHttpError() //Catches HttpRequestException or checks the status code: 5xx or 408
        .Or<TimeoutRejectedException>() //Catches TimeoutRejectedException, which can be thrown by an inner TimeoutPolicy
        .CircuitBreakerAsync( //Monitors consecutive failures
            handledEventsAllowedBeforeBreaking: settings
                .HttpRequestCircuitBreakerFailCountInCloseState, //After this amount of consecutive failures it will break
            durationOfBreak: TimeSpan.FromMilliseconds(settings
                .HttpRequestCircuitBreakerDelayInMillisecondsBetweenOpenAndHalfOpenStates)); //After this amount of delay it will give it a try

private static IAsyncPolicy<HttpResponseMessage> RetryPolicy(ResilienceSettings settings)
    => HttpPolicyExtensions
        .HandleTransientHttpError() //Catches HttpRequestException or checks the status code: 5xx or 408
        .Or<BrokenCircuitException>() //Catches BrokenCircuitException, so whenever the broker is open then it refuses new requests
        .Or<TimeoutRejectedException>() //Catches TimeoutRejectedException, which can be thrown by an inner TimeoutPolicy
        .WaitAndRetryAsync( //Monitors the above anomalies
            retryCount: settings.HttpRequestRetryCount, //After (this amount + 1) attempts it gives up
            sleepDurationProvider: _ =>
                TimeSpan.FromMilliseconds(settings.HttpRequestRetrySleepDurationInMilliseconds)); //After a failed attempt it delays the next try with this amount of time

And finally the extension method for registration:

public static class ResilientHttpClientRegister
{
    public static IServiceCollection AddXYZResilientStrategyToHttpClientProxy<TInterface, TImplementation>
        (this IServiceCollection services, ResilienceSettings settings)
        where TInterface: class
        where TImplementation: class, TInterface
    {
        var (serviceUri, combinedPolicy) = CreateParametersForXYZStrategy<TInterface>(settings);

        services.AddHttpClient<TInterface, TImplementation>(
                client => { client.BaseAddress = serviceUri; })
            .AddPolicyHandler(combinedPolicy); //Retry > Circuit Breaker > Timeout (outer > inner)

        return services;
    }

    private static (Uri, IAsyncPolicy<HttpResponseMessage>) CreateParametersForXYZStrategy<TInterface>(ResilienceSettings settings)
    {
        Uri serviceUri = Uri.TryCreate(settings.BaseAddress, UriKind.Absolute, out serviceUri)
            ? serviceUri
            : throw new UriFormatException(
                $"Invalid url was set for the '{typeof(TInterface).Name}' resilient http client. " +
                $"Its value was '{HttpUtility.UrlEncode(settings.BaseAddress)}'");

        var combinedPolicy = Policy.WrapAsync(RetryPolicy(settings), CircuitBreakerPolicy(settings), TimeoutPolicy(settings));

        return (serviceUri, combinedPolicy);
    }
}
answered on Stack Overflow Jan 11, 2021 by Peter Csala • edited Jan 11, 2021 by Peter Csala

User contributions licensed under CC BY-SA 3.0