csk
csk

Reputation: 73

Polly retry with different url

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");
            });
    }

Upvotes: 5

Views: 3160

Answers (1)

Peter Csala
Peter Csala

Reputation: 22819

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

Upvotes: 6

Related Questions