Polly
Polly copied to clipboard
how to use it for retry but not fault situation?
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
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?
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();
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
Sorry I assumed that you are using the V8 API, not the V7. Tomorrow I'll post the V7 version.
yes i use polly v8.2.0
The "v7" API is still present in Polly 8.x.x, the code you've shared is the "v7" API.
ah okay, so please share a V8-example which i can plugin to my code, because i prefer to work with the latest releases
The sample Peter shared is using our new API from v8.
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!
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.