Polly retry with different url
Asked Answered
F

1

5

I am trying to create a solution with polly where I request an other api.
I have a list of URLs to multiple instances of the same service.
I want that when the first request failes, an other should automaticly start with the next url from my list.

Here is an example where i try this behaviour with two static addresses
The Problem with this solution is that the url does not change until i start the next request. I want that the urls changes on every retry

 public static void ConfigureUserServiceClient(this IServiceCollection services)
    {

        _userServiceUri = new Uri("https://localhost:5001");

        services.AddHttpClient("someService", client =>
        {
            client.BaseAddress = _userServiceUri;
            client.DefaultRequestHeaders.Add("Accept", "application/json");
        }).AddPolicyHandler(retryPolicy());
    }

    private static IAsyncPolicy<HttpResponseMessage> retryPolicy()
    {
        return Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.RequestTimeout)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt),
            onRetry: (result, span, ctx) =>
            {
                _userServiceUri = new Uri("https://localhost:5002");
            });
    }
Fibroid answered 29/5, 2020 at 11:1 Comment(0)
C
6

You should consider to use the Fallback policy instead.

Like this:

private static HttpClient client = new HttpClient();
static async Task Main(string[] args)
{
    var addressIterator = GetUrls().GetEnumerator();

    var retryLikePolicy = Policy<string>
        .Handle<HttpRequestException>()
        .FallbackAsync(fallbackAction: async (ct) =>
        {
            if (addressIterator.MoveNext())
               return await GetData(addressIterator.Current);
            return null;
        });

    addressIterator.MoveNext();
    var data = await retryLikePolicy.ExecuteAsync(
       async () => await GetData(addressIterator.Current));

    Console.WriteLine("End");
}

static async Task<string> GetData(string uri)
{
    Console.WriteLine(uri);
    var response = await client.GetAsync(uri);
    return await response.Content.ReadAsStringAsync();
}

static IEnumerable<string> GetUrls()
{
    yield return "http://localhost:5500/index.html";
    yield return "http://localhost:5600/index.html";
    yield return "http://localhost:5700/index.html";
}

Please note that this code is just for demonstration.


UPDATE #1: Multiple fallback

If you have more than one fallback urls then you can alter the above code like this:

private static HttpClient client = new HttpClient();
static async Task Main(string[] args)
{
    var retryInCaseOfHRE = Policy
        .Handle<HttpRequestException>()
        .WaitAndRetryForeverAsync(_ => TimeSpan.FromSeconds(1));

    var response = await retryInCaseOfHRE.ExecuteAsync(
         async () => await GetNewAddressAndPerformRequest());
    
    if (response == null)
    {
        Console.WriteLine("All requests failed");
        Environment.Exit(1);
    }

    Console.WriteLine("End");
}

static IEnumerable<string> GetAddresses()
{
    yield return "http://localhost:5500/index.html";
    yield return "http://localhost:5600/index.html";
    yield return "http://localhost:5700/index.html";
    yield return "http://localhost:5800/index.html";
}

static readonly IEnumerator<string> AddressIterator = GetAddresses().GetEnumerator();

static async Task<string> GetNewAddressAndPerformRequest()
    => AddressIterator.MoveNext() ? await GetData(AddressIterator.Current) : null;

static async Task<string> GetData(string uri)
{
    Console.WriteLine(uri);
    var response = await client.GetAsync(uri);
    return await response.Content.ReadAsStringAsync();
}
  • The trick: the retry policy wraps a method which is responsible to retrieve the next url and then call the GetData
    • In other word we need to move the iteration process into the to be wrapped method (GetNewAddressAndPerformRequest)
  • I've replaced the Fallback policy to Retry since we need to perform (potentially) more than 1 fallback actions
  • I've used null to indicate we have run out of fallback urls but it might be a better solution to use a custom exception for that
Carlo answered 29/5, 2020 at 12:57 Comment(4)
Could this be done with IHttpClientFactory with 2 URLs instead of many, like primary and failover?Sparoid
@VergilC. Yes, absolutely the same approach could be used in that case as well. .FallbackAsync(fallbackAction: async (ct) => await GetData(secondaryAddress)); and retryLikePolicy.ExecuteAsync(async () => await GetData(primaryAddress))Carlo
@Peter Csala what will be the best way to retry for the entire list (at fallback change the url) until one url succeeds. Seems this one only try one fallback.Gunas
@Gunas I've extended my post to show you how to do that. Please check it.Carlo

© 2022 - 2024 — McMap. All rights reserved.