diff --git a/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs b/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs index 2340febd9..0cacb8a65 100644 --- a/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs +++ b/src/Web/WebMVC/Infrastructure/HttpClientDefaultPolicies.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using Polly; +using Polly.CircuitBreaker; +using Polly.Retry; using System; using System.Net.Http; @@ -7,8 +9,13 @@ namespace WebMVC.Infrastructure { public class HttpClientDefaultPolicies { - const int RETRY_COUNT = 6; - const int EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKER = 5; + //Config for Retries with exponential backoff policy + const int MAX_RETRIES = 6; + const int SECONDS_BASE_FOR_EXPONENTIAL_BACKOFF = 2; + + //Config for Circuit Breaker policy + const int EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKES = 5; + const int DURATION_OF_BREAK_IN_MINUTES = 1; private readonly ILogger _logger; @@ -17,44 +24,51 @@ namespace WebMVC.Infrastructure _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public Policy GetWaitAndRetryPolicy() + public IAsyncPolicy GetWaitAndRetryPolicy() { - return Policy.Handle() - .WaitAndRetryAsync( - // number of retries - RETRY_COUNT, - // exponential backofff - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), - // on retry - (exception, timeSpan, retryCount, context) => - { - var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + - $"of {context.PolicyKey} " + - $"at {context.OperationKey}, " + - $"due to: {exception}."; - _logger.LogWarning(msg); - _logger.LogDebug(msg); - }); + RetryPolicy retryPolicy = + Policy.Handle() + .WaitAndRetryAsync( + // Maximum number of retries + MAX_RETRIES, + // exponential backofff (2sg, 4sg, 8sg, 16sg, 32sg. etc.) + retryAttempt => TimeSpan.FromSeconds(Math.Pow(SECONDS_BASE_FOR_EXPONENTIAL_BACKOFF, retryAttempt)), + // on retry + (exception, timeSpan, retryCount, context) => + { + var msg = $"Retry {retryCount} implemented with Polly's RetryPolicy " + + $"of {context.PolicyKey} " + + $"at {context.OperationKey}, " + + $"due to: {exception}."; + _logger.LogWarning(msg); + _logger.LogDebug(msg); + }); + + return retryPolicy.AsAsyncPolicy(); } - public Policy GetCircuitBreakerPolicy() + public IAsyncPolicy GetCircuitBreakerPolicy() { - return Policy.Handle() - .CircuitBreakerAsync( - // number of exceptions before breaking circuit - EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKER, - // time circuit opened before retry - TimeSpan.FromMinutes(1), - (exception, duration) => - { - // on circuit opened - _logger.LogTrace("Circuit breaker opened"); - }, - () => - { - // on circuit closed - _logger.LogTrace("Circuit breaker reset"); - }); + CircuitBreakerPolicy circuitBreakerPolicy = + Policy.Handle() + .CircuitBreakerAsync( + // Number of exceptions before breaking the circuit + EXCEPTIONS_ALLOWED_BEFORE_CIRCUIT_BREAKES, + // Duration of break + TimeSpan.FromMinutes(DURATION_OF_BREAK_IN_MINUTES), + (exception, duration) => + { + // On circuit opened (Circuit is broken) + _logger.LogTrace("Circuit has been broken"); + }, + () => + { + // on circuit closed + _logger.LogTrace("Circuit has been reset"); + }); + + + return circuitBreakerPolicy.AsAsyncPolicy(); } } } diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index b8be7a4e5..f93532852 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -15,7 +15,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; using Polly; +using Polly.CircuitBreaker; +using Polly.Extensions.Http; using Polly.Registry; +using Polly.Retry; +using Polly.Timeout; using StackExchange.Redis; using System; using System.IdentityModel.Tokens.Jwt; @@ -35,13 +39,13 @@ namespace Microsoft.eShopOnContainers.WebMVC public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. + // This method gets called by the runtime. Use this method to add services to the IoC container. public void ConfigureServices(IServiceCollection services) { services.AddAppInsight(Configuration) .AddHealthChecks(Configuration) .AddCustomMvc(Configuration) - .AddCustomApplicationServices(Configuration) + .AddHttpClientServices(Configuration) .AddCustomAuthentication(Configuration); } @@ -165,28 +169,89 @@ namespace Microsoft.eShopOnContainers.WebMVC return services; } - public static IServiceCollection AddCustomApplicationServices(this IServiceCollection services, IConfiguration configuration) + // Adds all Http client services (like Service-Agents) using resilient Http requests based on HttpClient factory and Polly's policies + public static IServiceCollection AddHttpClientServices(this IServiceCollection services, IConfiguration configuration) { + // (CDLTLL) TEMPORAL COMMENT: Do we need this line of code if using HttpClient factory? services.AddSingleton(); + + // (CDLTLL) I don't think we need this HttpClientDefaultPolicies if using fluent configuration ,as below... + // Create a singleton object with the by default policies services.AddSingleton(); var defaultPolicies = services.BuildServiceProvider().GetService(); - var registry = services.AddPolicyRegistry(); - registry.Add("WaitAndRetry", defaultPolicies.GetWaitAndRetryPolicy()); - registry.Add("CircuitBreaker", defaultPolicies.GetCircuitBreakerPolicy()); + // Create a Polly-Policy-Registry with the by default policies for resilient Http requests + var pollyPolicyRegistry = services.AddPolicyRegistry(); + + //Using fluent client configuration of Polly policies + + var retriesWithExponentialBackoff = HttpPolicyExtensions + .HandleTransientHttpError() + .WaitAndRetryAsync(7, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + ); + //(CDLTLL) Instead of hardcoded values, use: configuration["HttpClientMaxNumberRetries"], configuration["SecondsBaseForExponentialBackoff"] + + var circuitBreaker = HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync(6, + TimeSpan.FromSeconds(30) + ); + //(CDLTLL) Instead of hardcoded values, use: configuration["HttpClientExceptionsAllowedBeforeBreaking"], configuration["DurationOfBreakInMinutes"] + + + pollyPolicyRegistry.Add("DefaultRetriesWithExponentialBackoff", retriesWithExponentialBackoff); + pollyPolicyRegistry.Add("DefaultCircuitBreaker", circuitBreaker); + + + // (CDLTLL) Using "OLD" policies. I don't like it much... + //pollyPolicyRegistry.Add("DefaultRetriesWithExponentialBackoff", defaultPolicies.GetWaitAndRetryPolicy()); // (CDLTLL) TEMPORAL COMMENT: Arguments here would be a good place to propagate the configuration values --> GetWaitAndRetryPolicy(configuration["HttpClientMaxNumberRetries"], configuration["SecondsBaseForExponentialBackoff"]) + //pollyPolicyRegistry.Add("DefaultCircuitBreaker", defaultPolicies.GetCircuitBreakerPolicy()); // (CDLTLL) TEMPORAL COMMENT: Arguments here would be a good place to propagate the configuration values --> GetCircuitBreakerPolicy(configuration["HttpClientExceptionsAllowedBeforeBreaking"], configuration["DurationOfBreakInMinutes"]) + + + + // (CDLTLL) Handlers need to be transient. //(CDLTLL) TEMPORAL COMMENT: This registration was missing, hence the error "InvalidOperationException: No service for type 'WebMVC.Infrastructure.HttpClientAuthorizationDelegatingHandler' has been registered" + services.AddTransient(); + + //Add all Typed-Clients (Service-Agents) through the HttpClient factory to implement Resilient Http requests + // + + //Add BasketService typed client (Service Agent) services.AddHttpClient() - .AddHttpMessageHandler() - .AddPolicyHandlerFromRegistry("WaitAndRetry") - .AddPolicyHandlerFromRegistry("CircuitBreaker"); + .AddHttpMessageHandler() //Additional Authentication-Delegating-Handler to add the OAuth-Bearer-token to the Http headers + .AddPolicyHandlerFromRegistry("DefaultRetriesWithExponentialBackoff") + .AddPolicyHandlerFromRegistry("DefaultCircuitBreaker"); + //Add CatalogService typed client (Service Agent) + //services.AddHttpClient() ... + + //Add OrderingService typed client (Service Agent) + //services.AddHttpClient() ... + + //Add CampaignService typed client (Service Agent) + //services.AddHttpClient() ... + + //Add LocationService typed client (Service Agent) + //services.AddHttpClient() ... + + //Add IdentityParser typed client (Service Agent) + //services.AddHttpClient() ... + + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // (CDLTLL) TEMPORAL COMMENT: The following Typed-Clients (Service-Agents) have to be coded as BasketService by using AddHttpClient<>(), right? + // This code will be deleted services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient, IdentityParser>(); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // TEMPORAL COMMENT: This code will be deleted when using HttpClientFactory, right? if (configuration.GetValue("UseResilientHttp") == bool.TrueString) { services.AddSingleton(sp => @@ -214,6 +279,7 @@ namespace Microsoft.eShopOnContainers.WebMVC { services.AddSingleton(); } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// return services; }