using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.HttpResilience.Policies; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.HttpResilience { /// <summary> /// HttpClient wrapper that integrates Retry and Circuit /// breaker policies when invoking HTTP services. /// Based on Polly library: https://github.com/App-vNext/Polly /// </summary> public class ResilientHttpClient : IHttpClient { private HttpClient _client; private PolicyWrap _policyWrapper; private ILogger<ResilientHttpClient> _logger; public HttpClient Inst => _client; public ResilientHttpClient(List<Policy> policies, ILogger<ResilientHttpClient> logger) { _client = new HttpClient(); _logger = logger; // Add Policies to be applied _policyWrapper = Policy.WrapAsync(policies.ToArray()); } public ResilientHttpClient(List<ResilientPolicy> policies, ILogger<ResilientHttpClient> logger) { _client = new HttpClient(); _logger = logger; // Add Policies to be applied _policyWrapper = Policy.WrapAsync(GeneratePolicies(policies)); } private Policy[] GeneratePolicies(IList<ResilientPolicy> policies) { var pollyPolicies = new List<Policy>(); foreach (var policy in policies) { switch (policy) { case RetryPolicy retryPolicy: pollyPolicies.Add( CreateRetryPolicy( retryPolicy.Retries, retryPolicy.BackoffSeconds, retryPolicy.ExponentialBackoff)); break; case CircuitBreakerPolicy circuitPolicy: pollyPolicies.Add( CreateCircuitBreakerPolicy( circuitPolicy.ExceptionsAllowedBeforeBreaking, circuitPolicy.DurationOfBreakInMinutes)); break; } } return pollyPolicies.ToArray(); } private Policy CreateRetryPolicy(int retries, int backoffSeconds, bool exponentialBackoff) => Policy.Handle<HttpRequestException>() .WaitAndRetryAsync( retries, retryAttempt => exponentialBackoff ? TimeSpan.FromSeconds(Math.Pow(backoffSeconds, retryAttempt)) : TimeSpan.FromSeconds(backoffSeconds), (exception, timeSpan, retryCount, context) => { var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + $"of {context.PolicyKey} " + $"at {context.ExecutionKey}, " + $"due to: {exception}."; _logger.LogWarning(msg); _logger.LogDebug(msg); } ); private Policy CreateCircuitBreakerPolicy(int exceptionsAllowedBeforeBreaking, int durationOfBreakInMinutes) => Policy.Handle<HttpRequestException>() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking, TimeSpan.FromMinutes(durationOfBreakInMinutes), (exception, duration) => { // on circuit opened _logger.LogTrace("Circuit breaker opened"); }, () => { // on circuit closed _logger.LogTrace("Circuit breaker reset"); } ); public Task<string> GetStringAsync(string uri) => HttpInvoker(() => _client.GetStringAsync(uri)); public Task<HttpResponseMessage> PostAsync<T>(string uri, T item) => // a new StringContent must be created for each retry // as it is disposed after each call HttpInvoker(() => { var response = _client.PostAsync(uri, new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json")); // raise exception if HttpResponseCode 500 // needed for circuit breaker to track fails if (response.Result.StatusCode == HttpStatusCode.InternalServerError) { throw new HttpRequestException(); } return response; }); public Task<HttpResponseMessage> DeleteAsync(string uri) => HttpInvoker(() => _client.DeleteAsync(uri)); private Task<T> HttpInvoker<T>(Func<Task<T>> action) => // Executes the action applying all // the policies defined in the wrapper _policyWrapper.ExecuteAsync(() => action()); } }