Polly icon indicating copy to clipboard operation
Polly copied to clipboard

how to use it for retry but not fault situation?

Open MannusEtten opened this issue 1 year ago • 10 comments

What are you wanting to achieve?

i am trying to achieve the following:

I do have a request which returns http 200 (ok) The body has content and in the content there is a string "pending"

I would like to implement here a retry for 3 times and than stop it.

So three times I will not get any http-error or so only http 200 and in the content "pending".

Is the circuit breaker a better approach?

What code or approach do you have so far?

I tried the retrypolicy but that did not went well because the outcome of the polly request was always failed.

Additional context

No response

MannusEtten avatar Jan 10 '24 13:01 MannusEtten

A retry strategy is the correct way to address this scenario - a circuit breaker would typically affect all calls to the upstream service, not just a specific request.

Can you share the code for what you did with retries that didn't seem to work?

martincostello avatar Jan 10 '24 13:01 martincostello

Here is an example how to achieve it:

new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new()
    {
        MaxRetryAttempts = 3,
        ShouldHandle = async args => 
        {
            if(args.Outcome.Result is not null)
            {    
                HttpResponseMessage message = args.Outcome.Result;
                if(message.StatusCode == HttpStatusCode.OK) 
                {
                    string responseMessage = await message.Content.ReadAsStringAsync(); 
                    if(responseMessage.Contains("pending"))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
    }).Build();

peter-csala avatar Jan 10 '24 15:01 peter-csala

public static AsyncRetryPolicy<string> BuildRetryPolicyWithContentValidation(string description)
{
    var policy = Policy
        .Handle<FlurlHttpException>(IsTransientError)
        .OrResult<string>(InvalidContent)
        .WaitAndRetryAsync(new[]
            {
                TimeSpan.FromSeconds(2),
                TimeSpan.FromSeconds(5),
                TimeSpan.FromSeconds(10)
            },
            (delegateResult, retryCount) =>
            {
                Log.Logger.Here().Debug($"{description}, retry delegate fired, attempt {retryCount}");
            });
    return policy;
}

private static bool InvalidContent(string content)
{
    var result = content.Contains("error");
    if (result)
    {
        Log.Logger.Here().Debug(content);
    }
    return result;
}

    var policy = PolicyService.BuildRetryPolicyWithContentValidation($"GetServiceDefinition: {service.AdminServiceUrl}");

var response = await policy.ExecuteAndCaptureAsync(() => url.SetQueryParams(new { f = "json", token = _token }).GetStringAsync()).ConfigureAwait(false); if (response.Outcome == OutcomeType.Successful) { return response.Result; }

this is my current code for checking invalidcontent, the given example by Peter is something I can't really follow or understand

MannusEtten avatar Jan 10 '24 17:01 MannusEtten

Sorry I assumed that you are using the V8 API, not the V7. Tomorrow I'll post the V7 version.

peter-csala avatar Jan 10 '24 18:01 peter-csala

yes i use polly v8.2.0

MannusEtten avatar Jan 10 '24 20:01 MannusEtten

The "v7" API is still present in Polly 8.x.x, the code you've shared is the "v7" API.

martincostello avatar Jan 10 '24 20:01 martincostello

ah okay, so please share a V8-example which i can plugin to my code, because i prefer to work with the latest releases

MannusEtten avatar Jan 10 '24 20:01 MannusEtten

The sample Peter shared is using our new API from v8.

martincostello avatar Jan 10 '24 20:01 martincostello

ah okay than V7 would be helpful at this moment because I do not have the time at this moment to rewrite all my code. But I do not use this services/builder-option. So within the context of my code would be the best. thanks a lot!

MannusEtten avatar Jan 10 '24 21:01 MannusEtten

In case of V7 you have several ways to achieve the desired behaviour.

In the below samples I'll use HttpResponseMessage because I'm more familiar with that compared to Flurl. But I think the same concepts could be applied there as well.

Blocking call inside HandleResult

Policy<HttpResponseMessage>
    .HandleResult(response =>
        response.StatusCode == System.Net.HttpStatusCode.InternalServerError
        && response.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("pending")
    .WaitAndRetry(...)

Because there is no HandleResultAsync that's why we have to use .GetAwaiter().GetResult() on the ReadAsStringAsync() instead of await.

This solution assumes that the response body is fairly small: If the request processing is still pending then most probably the response body does not contain too much data.

Using FallbackAsync

var fallback = Policy<HttpResponseMessage>
    .HandleResult(res => res.StatusCode == HttpStatusCode.OK)
    .FallbackAsync(async (dr, ctx, ct) =>
    {
        //await dr.Result.Content.LoadIntoBufferAsync(); //depending on the .NET version you might need to call this
        var responseBody = await dr.Result.Content.ReadAsStringAsync(ct);
        if(responseBody.Contains("pending"))
        {
            throw new ShouldRetryException();
        }
        return dr.Result;
    },  onFallbackAsync: (_, __) => Task.CompletedTask);
    
var retry =  Policy<HttpResponseMessage>
        .Handle<ShouldRetryException>()
        .WaitAndRetryAsync(...);

var policy = Policy.WrapAsync(retry, fallback);    

Here the trick is that a Fallback is used to determine whether the action should be retried or not. There is a FallbackAsync which can read the HttpResponseMessage and based on your conditions either does nothing (return dr.Result) or throws a custom exception. The retry is defined as the outer policy which is aware of the custom exception.

Move the response check inside the ExecuteAsync

var res = await retry.ExecuteAsync(async ct => 
{
    var response = await client.GetAsync("...", ct);  
    
    if(response.StatusCode != HttpStatusCode.OK)
        return response;

    //await response.Content.LoadIntoBufferAsync();
    var responseBody = await response.Content.ReadAsStringAsync(ct);
    if(responseBody.Contains("pending"))
    {
        throw new ShouldRetryException();
    }

    return response;
}, CancellationToken.None);

Rather than defining a Fallback policy the FallbackAsync's logic could be placed inside the ExecuteAsync as well.

peter-csala avatar Jan 11 '24 08:01 peter-csala