Using exponential backoff in Polly Library with httpclient - c#

I just read about Polly library and I need to handle 3 retries when communicating from a desktop agent to a server.
Currently I like the exponential backoff.
However, I struggle to understand how to implement this in my code. This is what I have done so far:
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
client.DefaultRequestHeaders.Add("TenantId", tenantId);
#if DEBUG
var url = "http://localhost:5001/api/v1/RetrievePaymentDetails?orderpaymentid={orderPaymentId";
#else
//TODO: add URL once the application is in production!
var url = "intent2.api";
#endif
Policy
.Handle<HttpRequestException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(15),
TimeSpan.FromSeconds(30)
});
using (HttpResponseMessage response = await client.GetAsync($"{url}"))
{
using (HttpContent content = response.Content)
{
HttpContentHeaders headers = content.Headers;
var result = JsonConvert.DeserializeObject<PaymentDetailsDto>(response.Content.ReadAsStringAsync().Result);
Currency currencyFromDb = (Currency)Enum.Parse(typeof(Currency), result.Currency);
var result2 = await this.posManager.PayPos(result.Amount, currencyFromDb, result.SumUpUsername, result.SumUpPassword);
if (result2.TransactionCode == null)
{
return this.Request.CreateResponse(HttpStatusCode.BadRequest, result2);
}
return this.Request.CreateResponse(HttpStatusCode.OK, result2);
//return this.Request.CreateResponse<object>(result2);
}
}
}
I turned off my web server so I can simulate this exercise.
Now, every time it hits that URL it throws a HttpRequestException and does not retry at all. How do I go about using this?

Your are not using the policy. Defining one does not apply it to anything. Here is an example of a specialized transient error policy from the HttpPolicyExtensions. But this general pattern applies to any policy:
static AsyncRetryPolicy<HttpResponseMessage> transientRetry3Policy = HttpPolicyExtensions
.HandleTransientHttpError()
.RetryAsync(3);
And here is how you apply it to a request:
var someresult = await transientRetry3Policy
.ExecuteAndCaptureAsync(async () => await client.SendAsync(msg));

Your code could (and should) be improved in so many different ways.
Because we are not on code review that's why I will only focus on the Polly related stuff.
Retry in general
There is really important precondition for retry: the action (which might be called several times) should be idempotent and side-effect free. As an educated guess based on the provided code you are trying to retrieve information about a purchase which has already occurred. I suppose such query is written in an idempotent way.
I quite not sure that your posManager is idempotent and side-effect free (I suppose not). So my point is that you should choose wisely the scope of your resilient strategy, because it can cause damage if it is carried out without care.
Retry and its friends
Retry itself is just a policy not a resilient strategy. We can talk about strategy when we combine multiple policies in order to gain resilient behavior.
For example, imagine a situation when your payment service is highly overloaded and your response times goes insane. Let's say 99 seconds. Are you willing to wait for that? The default timeout of HttpClient is 100 seconds, which is a lot. If you don't control that your system will hang for a while. That's where Timeout comes into the picture.
It can be used for each individual query or as a global timeout for the whole process (including retries). So for example:
local timeout: 5 seconds
global timeout: 90 seconds
Another related policy is called CircuitBreaker which can help you to prevent flooding the downstream system (which is the payment service in your case). If your payment service already has hard time to serve requests then we should not send new ones to it.
CircuitBreaker monitors the successive failures and if a given threshold is exceeded then it will prevent all outgoing requests for a predefined amount of time to help the downstream system to get on foot again.
If you put all of this together then you will have a fairly resilient solution:
Global timeout
Retry
CircuitBreaker
Local timeout
Transient Http Errors
The HandleTransientHttpError method mentioned by Crowcoder handles either HttpRequestException or status code of 408 or 5xx. If your payment service returns another response status, for example 429 (Too Many Request) then it will not handle that. It's worth to bear in mind.

Crowcoder's answer with exponential backoff policy
// Exponential backoff policy
AsyncRetryPolicy<HttpResponseMessage> exponentialBackoffPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
// Use sleepDurationProvider parameter
.WaitAndRetryAsync(retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// Using policy
var someresult = await exponentialBackoffPolicy
.ExecuteAndCaptureAsync(async () => await client.SendAsync(msg));

Related

Polly Retry : System.ObjectDisposedException: Cannot access a disposed object. Object name: 'Flurl.Http.Content.FileContent'

I'm using flurl and polly as part of my code to post asynchronously - I'm having an issue with the retry policy and the file content getting disposed, on a retry the FileContent option has been disposed.
I will get the following error:
System.ObjectDisposedException: Cannot access a disposed object. Object name: 'Flurl.Http.Content.FileContent'.
If someone could advise where best to place the logic to ensure FileContent is available for the retry
var endpoint = $"api endpoint here";
if (!File.Exists(filePath))
{
return Task.CompletedTask;
}
var fileBytesLength = _fileSystem.FileInfo.FromFileName(filePath).Length;
var httpContent = new Flurl.Http.Content.FileContent(filePath, (int)fileBytesLength);
return _statusRetryPolicy
.StatusAsyncRetryPolicy()
.ExecuteAsync(() => endpoint
.WithOAuthBearerToken(accessToken)
.WithTimeout(TimeSpan.FromMinutes(timeOutMinutes))
.WithHeader("Content-Type", "application/zip")
.OnError((flurlCall) =>
{
_logger.LogError(flurlCall.Exception, flurlCall.Exception.Message);
})
.PostAsync(httpContent));
and the Retry Policy:
public AsyncRetryPolicy StatusAsyncRetryPolicy() => Policy
.Handle<FlurlHttpException>(RetryPolicyHelpers.IsTransientError)
.WaitAndRetryAsync(5, retryAttempt =>
{
var nextAttemptIn = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
_logger.LogWarning($"StatusRetryPolicy: Retry attempt {retryAttempt} to make request. Next try in {nextAttemptIn.TotalSeconds} seconds.");
return nextAttemptIn;
});
I have several suggestions for your code.
!File.Exists
I assume the filePath is provided by the user. If that file does not exist then short-cutting the execution and saying everything went fine << seems to me lazy error handling.
Early exiting / failing fast is a good pattern but please let the caller of your method know that some pre-condition(s) failed prior any real work has been done.
"application/zip"
Since you haven't shared with us all relevant code that's why it is hard to tell, but please make sure that the filePath is pointing to a real zip compressed file (not just checking that the file's extension is .zip).
return ....ExecuteAsync
As it was stated by dropoutcoder you should await the ExecuteAsync to be sure that method-scoped httpContent is not disposed.
.PostAsync(httpContent))
Most of the time those endpoints that can be called with POST verb are not implemented in an idempotent way. In other words it might happen that without request de-duplication you create duplicates or even worse with retries.
Please make sure that retry logic is applicable in your use case.
WithOAuthBearerToken(accessToken)
Creating a retry logic to refresh the accessToken if it is expired seems to me a more reasonable choice for a retry logic. Here you can find two alternative implementations how to achieve that.
WithTimeout(TimeSpan.FromMinutes(timeOutMinutes))
Because you are mixing Flurl resilience features with Polly resilience features it is pretty hard to tell without reading the documentation (or experimenting) that this timeout is for all retry attempts (global) or for each attempt (local).
I would suggest to use Polly's Timeout with Policy.Wrap to define an overarching timeout constraint or a per request timeout limit.
_logger.LogWarning
Last but not least I would suggest to use WaitAndRetryAsync in the way that you perform the logging inside the onRetry delegate
.WaitAndRetryAsync(5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry:(ex, nextAttemptIn, retryAttempt) =>
_logger.LogWarning($"StatusRetryPolicy: Retry attempt {retryAttempt} to make request. Next try in {nextAttemptIn.TotalSeconds} seconds."));
Here you can access the exception which triggers a new retry attempt.

Polly Retries Doesn't Close Existing HttpClient Call

I'm using Polly to handle some scenarios like request throttled and timeouts.
The policies were added directly in the Startup.cs which would be like this :
var retries = //applying the retries, let say I set to 25 times with 10s delay. Total 250s.
serviceCollection
.AddHttpClient<IApplicationApi, ApplicationApi>()
.AddPolicyHandler((services, request) => GetRetryPolicy<ApplicationApi>(retries, services));
The Policy:
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy<T>(List<TimeSpan> retries, IServiceProvider services)
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(retries,
onRetry: (outcome, timespan, retryAttempt, context) =>
{
//do some logging
}
}
In ApplicationApi.cs do something like this:
private readonly HttpClient _httpClient;
public ApplicationApi(HttpClient httpClient)
{
_httpClient = httpClient;
}
public void CallApi()
{
var url = "https://whateverurl.com/someapi"
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var response = await _httpClient.SendAsync(request);
var respMessage = await
response.Content.ReadAsStringAsync();
}
}
Now let say I don't specify the HttpClient.Timeout, which then will use default timeout : 100s.
Now I have a problem with heavy throttling. Polly will retry until the throttling resolved, or it reach the max retry.
But, the program will thrown an exception on the 10th retry since it already more than 100s elapsed on httpclient since the first request it got throttled.
Seems like the first http request that got throttled still on and not closed, or I may be wrong.
What causing this? Is it a normal behavior of Polly retries?
How can I make it close the connection on each retries so I don't have to set a very high HttpClient.Timeout value.
I also implemented the Polly timeout policy to cut request if more than some specified second then retry until it succeed. But the Polly behavior still like this. So I need to set httpclient timeout > total elapsed time on retries
**UPDATE
Code updated. So I just realized there's using statement for the request.
***UPDATE
I've created a repo that reproduce the behavior here : https://github.com/purnadika/PollyTestWebApi
The short answer is that your observed behaviour is due to fact how AddPolicyHandler and PolicyHttpMessageHandler work.
Whenever you register a new Typed HttpClient without any policy (.AddHttpClient) then you basically create a new HttpClient like this:
var handler = new HttpClientHandler();
var client = new HttpClient(handler);
Of course it is much more complicated, but from our topic perspective it works like that.
If you register a new Typed HttpClient with a policy (.AddHttpClient().AddPolicyHandler()) then you create a new HttpClient like this
var handler = new PolicyHttpMessageHandler(yourPolicy);
handler.InnerHandler = new HttpClientHandler();
var client = new HttpClient(handler);
So the outer handler will be the Polly's MessageHandler and the inner is the default ClientHandler.
Polly's MessageHandler has the following documentation comment:
/// <para>
/// Take care when using policies such as Retry or Timeout together as HttpClient provides its own timeout via
/// <see cref="HttpClient.Timeout"/>. When combining Retry and Timeout, <see cref="HttpClient.Timeout"/> will act as a
/// timeout across all tries; a Polly Timeout policy can be configured after a Retry policy in the configuration sequence,
/// to provide a timeout-per-try.
/// </para>
By using the AddPolicyHandler the HttpClient's Timeout will act as a global timeout.
The solution
There is workaround, namely avoiding the usage of AddPolicyHandler.
So, rather than decorating your Typed Client at the registration time you can decorate only the specific HttpClient method call inside your typed client.
Here is a simplified example based on your dummy project:
ConfigureServices
services.AddHttpClient<IApplicationApi, ApplicationApi>(client => client.Timeout = TimeSpan.FromSeconds(whateverLowValue));
_MainRequest
var response = await GetRetryPolicy().ExecuteAsync(async () => await _httpClient.GetAsync(url));
Here I would like to emphasize that you should prefer GetAsync over SendAsync since the HttpRequestMessage can not be reused.
So, if you would write the above code like this
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var response = await GetRetryPolicy().ExecuteAsync(async () => await _httpClient.SendAsync(request));
}
then you would receive the following exception:
InvalidOperationException: The request message was already sent. Cannot send the same request message multiple times.
So, with this workaround the HttpClient's Timeout will not act as a global / overarching timeout over the retry attempts.
Polly works to retry the top-level HttpClient request, so HttpClient's timeout applies to all retries. That's the whole point of using Polly, to retry requests in a way that's transparent to the top-level code.
If retrying for over a minute failed to work, the retry policy isn't good enough. Retrying over and over with a fixed delay will only result in more 429 responses, as all the requests that failed will be retried at the same time. This will result in wave after wave of identical requests hitting the server, resulting in 429s once again.
To avoid this, exponential backoff and jitter are used to introduce an increasing random delay to retries.
From the linked sample:
var delay = Backoff.DecorrelatedJitterBackoffV2(
medianFirstRetryDelay: TimeSpan.FromSeconds(1),
retryCount: 5);
var retryPolicy = Policy
.Handle<FooException>()
.WaitAndRetryAsync(delay);
The Polly page for the Jitter strategy explain how this works. The distribution graph of delays shows that even with 5 retries, the retry intervals don't clamp together.
This means there's less chance of multiple HttpClient calls retrying at the same time, resulting in renewed throttling

Calls to an API fails after some time when multiple users are logged in and using the application. Is there a way to solve this?

I am making some calls to an api that syncs and saves appointments to/from my application and mail agenda which is most commonly Outlook mails. The calls to the api are made from a web application and it is working fine for some time but then immediately after a few hours, the calls are failed. This continues for some time and starts to work again after some time.
The DefaultConnectionLimit was set to 100 in the beginning and during this scenario, the process stopped working after some time (Say 30 mins to 1 hour). Then DefaultConnectionLimit was set to 20 and in this case, it worked fine for 3-4 hours and stopped working after that. I don't know if this is the actual cause but just mentioning.
The code where the call to the api is made from the web application is mentioned below :
public bool SyncNewAppToExch(string encryptedAppSyncDetails)
{
try
{
string result = string.Empty;
JArray paramList = new JArray();
paramList.Add(encryptedAppSyncDetails);
var emailApiUrl = ConfigurationManager.AppSettings["emailApiUrl"].ToString();
Uri baseAddress = new Uri(emailApiUrl);
var url = "/api/EmailProcess/ManualSaveAppointments";
HttpClient client = new HttpClient();
client.BaseAddress = baseAddress;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Task.Run(() => client.PostAsJsonAsync(url, paramList)).Wait(5000);
var responseMessage = true;
if (responseMessage)
return true;
else
return false;
}
catch (Exception ex)
{
return false;
}
}
The exception that follows the api call failure reads :
Exception : System.AggregateException: One or more errors occurred. System.Threading.Tasks.TaskCanceledException: A task was canceled. --- End of inner exception stack trace --- at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1.get_Result()
If you are making a new HttpClient object each time you want to make a call, you are not using it appropriately. HttpClient may continue to use sockets even if it is no longer being used and this will likely cause socket exhaustion under load. You can see more on this topic here
You should create a single instance and reuse it. If you are using .Net core it would be a better idea to use the HttpClientFactory.
In addition, using Task.Run to call an async method is not wise. You are setting yourself up for a deadlock. It would be much better to mark your method as async and await, if possible. Since you are effectively doing fire and forget, the way you have done this if your App Domain shuts down this work will be lost.
If you need to do this, you should at least consider registering your task as such:
HostingEnvironment.QueueBackgroundWorkItem(ct => SendMailAsync(user.Email));
The short answer is that it is difficult to say what your issue is, it could be one of several problems.

Using Polly to retry after HttpStatusCode.Unauthorized

I'm making calls to an external API and want to deal with the event that a call returns an Unauthorized HttpResponseMessage. When this happens I want to refresh the access token and make the call again.
I'm trying to use Polly with the following code:
public async Task<HttpResponseMessage> MakeGetRequestAsync()
{
var retryPolicy = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Unauthorized)
.Retry(1, (exception, retryCount) =>
{
RefreshAccessToken();
});
var result = await retryPolicy.ExecuteAsync(() => CallApiAsync());
return result;
}
private async Task<HttpResponseMessage> CallApiAsync()
{
var url = Options.ResourceSandboxUrl;
var httpClient = new HttpClient();
SetRequestHeaders(httpClient);
var response = await httpClient.GetAsync(url);
response.StatusCode = HttpStatusCode.Unauthorized;
return response;
}
I put breakpoints on the ExecuteAsync statement and in DoSomethingAsync - when I step over ExecuteAsync DoSomethingAsync is not called and control is returned to the function that called MakeGetRequestAsync
I don't understand why DoSomethingAsync is not called - can anyone help me with what I'm trying to achieve?
I've looked at the Polly documentation & Polly questions on Stack Overflow but I can't figure out what's going on..
To use ExecuteAsync() you must declare the policy as .RetryAsync(...), not .Retry(...).
If your actual code reads exactly as the code sample above, the .ExecuteAsync(...) will be throwing for the mismatch between .Retry(...) [a sync policy] and .ExecuteAsync(...) [an async execution]. Since this exception is thrown, CallApiAsync() is indeed never invoked. You should be able to see the thrown exception, when calling MakeGetRequestAsync()
Overall code approach looks good tho: this retry-refreshing-authentication is a proven pattern with Polly!
I'm replying to this old question just to point out the Polly wiki page where this pattern was official documented:
retry-to-refresh-authorization
In particular this is the code snippet suggested:
var authorisationEnsuringPolicy = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(
retryCount: 1, // Consider how many retries. If auth lapses and you have valid credentials, one should be enough; too many tries can cause some auth systems to blacklist.
onRetryAsync: async (outcome, retryNumber, context) => FooRefreshAuthorizationAsync(context),
/* more configuration */);
var response = authorisationEnsuringPolicy.ExecuteAsync(context => DoSomethingThatRequiresAuthorization(context), cancellationToken);
The FooRefreshAuthorizationAsync(...) method can obtain a new authorization token and pass it to the delegate executed through the policy using Polly.Context.
I might be late to the game but I leave here a post for future readers. I have created two slightly different solutions for this refresh token with retry problem.
Retry policy, Delegating Handler and Custom exception
Here is sequence diagram which depicts the communication flow
Here is the full source code
Retry policy, Delegating Handler and Polly.Context
This version separates the responsibilities in a different way:
Here is sequence diagram which depicts the communication flow
Here is the full source code
this is how async await work in .net, when the execution of your code reaches an await, two things will happened
the current thread of your code should release to make your code async, that means, your method should return
when the task you await is complete, your method should continue from where it used to be

How do I remove the delay between HTTP Requests when using Asynchronous actions in ASP.NET?

I am using HttpClient to send a GET request to a server inside of a while loop
while (cycle < maxcycle)
{
var searchParameters = new ASearchParameters
{
Page = cycle++,
id = getid
};
var searchResponse = await Client.SearchAsync(searchParameters);
}
and the SearchAsync contains
public async Task<AuctionResponse> SearchAsync()
{
var uriString = "Contains a https url with parameters"
var searchResponseMessage = await HttpClient.GetAsync(uriString);
return await Deserialize<AuctionResponse>(searchResponseMessage);
}
The thing is after every request there is a delay before the next request is started.
you can see this in fiddler timeline and also in fiddler there is "Tunnel To" example.com:443 before every request
Question : Why is there a delay and how to remove it ?
I see two things that are happening here. First, depending on the deserializer, it may take a while to translate your response back into an object. You might want to time that step and see if that's not the majority of your time spent. Second, the SSL handshake (the origin of your "tunnel to") does require a round trip to establish the SSL channel. I thought HttpClient sent a Keep-Alive header by default, but you may want to see if it is A) not being sent or B) being rejected. If you are re-establishing an SSL channel for each request, that could easily take on the order of a hundred ms all by itself (depending upon the server/network load).
If you're using Fiddler, you can enable the ability to inspect SSL traffic to see what the actual request/response headers are.
I believe you see this delay for a couple of reasons. Based on the code you provided, all other actions besides the request itself take up some fraction of the time between requests. So deserializing the response will add to a delay.
Also, the delay might be tied to the amount of data that is being returned and processed further down the stack. I tried to recreate the scenario you describe in your question with the following code:
const int MaxNumberOfCycles = 10;
static void Main()
{
Start().Wait();
}
async Task Start()
{
var client = new Client();
var cycle = 0;
while (cycle < MaxNumberOfCycles)
{
var response = await client.SearchAsync(cycle++);
}
}
class Client
{
public async Task<HttpResponseMessage> SearchAsync(int n)
{
// parameter 'n' used to vary web service response data
var url = ... // url removed for privacy
using (var client = new HttpClient())
using (var response = await client.GetAsync(url))
{
return response;
}
}
}
With small response sizes I saw no delay between requests. As response sizes increased I began to see slightly longer delays. Here's a screenshot for a series of requests returning 1MB responses:
One thing I noticed about your scenario is that your transfer activity graph shows a solid black line at the end of each request. This line indicates the "time to first byte", meaning that response processing did not even start until the very end of your request.
Another issue you might consider is that Fiddler is that causing these delays. I noticed that your responses aren't being streamed by Fiddler, which probably impacts the results. You can read more about response streaming in Fiddler.
I hope some of this information helps...

Categories