diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs index 43bae55a908..769b4e1b12b 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs @@ -61,7 +61,7 @@ public static ServiceProvider InitializeServiceProvider(HedgingClientType client private static IServiceCollection AddHedging(this IServiceCollection services, HedgingClientType clientType) { var clientBuilder = services.AddHttpClient(clientType.ToString(), client => client.Timeout = Timeout.InfiniteTimeSpan); - var hedgingBuilder = clientBuilder.AddStandardHedgingHandler().SelectPipelineByAuthority(SimpleClassifications.PublicData); + var hedgingBuilder = clientBuilder.AddStandardHedgingHandler().SelectStrategyByAuthority(SimpleClassifications.PublicData); _ = clientBuilder.AddHttpMessageHandler(); int routes = clientType.HasFlag(HedgingClientType.ManyRoutes) ? 50 : 2; diff --git a/eng/Packages/General.props b/eng/Packages/General.props index 9870058fd01..4ed59aff187 100644 --- a/eng/Packages/General.props +++ b/eng/Packages/General.props @@ -34,10 +34,10 @@ - - - - + + + + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/ContextExtensions.cs similarity index 93% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/ContextExtensions.cs index 325c2d85a65..7ff798724cc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/PolicyContextExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/ContextExtensions.cs @@ -9,10 +9,10 @@ namespace Microsoft.Extensions.Http.Resilience.FaultInjection; /// -/// Provides extension methods for . +/// Provides extension methods for . /// [Experimental] -public static class PolicyContextExtensions +public static class ContextExtensions { private const string CallingRequestMessage = "CallingRequestMessage"; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs index 86764f8e973..a4d5e91c5c8 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/HttpClientFaultInjectionExtensions.cs @@ -10,8 +10,8 @@ using Microsoft.Extensions.Http.Resilience.FaultInjection.Internal; using Microsoft.Extensions.Options; using Microsoft.Extensions.Resilience.FaultInjection; -using Microsoft.Extensions.Resilience.Internal; using Microsoft.Shared.Diagnostics; +using Polly; namespace Microsoft.Extensions.Http.Resilience.FaultInjection; @@ -205,18 +205,17 @@ public static IHttpClientBuilder AddWeightedFaultInjectionPolicyHandlers(this IH private static IHttpClientBuilder AddChaosMessageHandler(this IHttpClientBuilder httpClientBuilder) { - _ = httpClientBuilder - .AddResilienceHandler("chaos") - .AddPolicy((pipelineBuilder, services) => - { - var chaosPolicyFactory = services.GetRequiredService(); - var httpClientChaosPolicyFactory = services.GetRequiredService(); - _ = pipelineBuilder - .AddPolicy(httpClientChaosPolicyFactory.CreateHttpResponsePolicy()) - .AddPolicy(chaosPolicyFactory.CreateExceptionPolicy()) - .AddPolicy(chaosPolicyFactory.CreateLatencyPolicy()); - }); + return httpClientBuilder.AddHttpMessageHandler(serviceProvider => + { + var chaosPolicyFactory = serviceProvider.GetRequiredService(); + var httpClientChaosPolicyFactory = serviceProvider.GetRequiredService(); - return httpClientBuilder; + var policy = Policy.WrapAsync( + chaosPolicyFactory.CreateLatencyPolicy(), + chaosPolicyFactory.CreateExceptionPolicy().AsAsyncPolicy(), + httpClientChaosPolicyFactory.CreateHttpResponsePolicy()); + + return new PolicyHttpMessageHandler(policy); + }); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs index 9302272c2f3..e3285bb2fd4 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/FaultInjection/Internal/Log.cs @@ -12,7 +12,7 @@ internal static partial class Log "Fault-injection group name: {groupName}. " + "Fault type: {faultType}. " + "Injected value: {injectedValue}. " + - "Http content key: {httpContentKey}. ")] + "Http content key: {httpContentKey}.")] public static partial void LogInjection( ILogger logger, string groupName, diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs index 85f7ab563ee..d3b73a59afa 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HedgingEndpointOptions.cs @@ -3,50 +3,58 @@ using System; using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; using Microsoft.Extensions.Options.Validation; -using Microsoft.Extensions.Resilience.Options; +using Polly.Timeout; namespace Microsoft.Extensions.Http.Resilience; /// -/// Options for resilient pipeline of policies assigned to a particular endpoint. It is using three chained layers in this order (from the outermost to the innermost): -/// Bulkhead -> Circuit Breaker -> Attempt Timeout. +/// Options for the pipeline of resilience strategies assigned to a particular endpoint. /// +/// +/// It is using three chained layers in this order (from the outermost to the innermost): Bulkhead -> Circuit Breaker -> Attempt Timeout. +/// public class HedgingEndpointOptions { - private static readonly TimeSpan _timeoutInterval = TimeSpan.FromSeconds(10); - /// /// Gets or sets the bulkhead options for the endpoint. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpBulkheadPolicyOptions BulkheadOptions { get; set; } = new(); + public HttpRateLimiterStrategyOptions RateLimiterOptions { get; set; } = new HttpRateLimiterStrategyOptions + { + StrategyName = StandardHedgingStrategyNames.RateLimiter + }; /// /// Gets or sets the circuit breaker options for the endpoint. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpCircuitBreakerPolicyOptions CircuitBreakerOptions { get; set; } = new(); + public HttpCircuitBreakerStrategyOptions CircuitBreakerOptions { get; set; } = new HttpCircuitBreakerStrategyOptions + { + StrategyName = StandardHedgingStrategyNames.CircuitBreaker + }; /// - /// Gets or sets the options for the timeout policy applied per each request attempt. + /// Gets or sets the options for the timeout resilience strategy applied per each request attempt. /// /// - /// By default it is initialized with a unique instance of - /// using a custom of 10 seconds. + /// By default it is initialized with a unique instance of + /// using a custom of 10 seconds. /// [Required] [ValidateObjectMembers] - public HttpTimeoutPolicyOptions TimeoutOptions { get; set; } = new() + public HttpTimeoutStrategyOptions TimeoutOptions { get; set; } = new() { - TimeoutInterval = _timeoutInterval, + Timeout = TimeSpan.FromSeconds(10), + StrategyName = StandardHedgingStrategyNames.AttemptTimeout }; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs index b969fbc1dd6..0da3f8cab12 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs @@ -2,22 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Net.Http; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Http.Resilience.Internal.Routing; using Microsoft.Extensions.Http.Resilience.Internal.Validators; using Microsoft.Extensions.Http.Resilience.Routing.Internal; -using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Options.Validation; using Microsoft.Shared.Diagnostics; +using Polly; namespace Microsoft.Extensions.Http.Resilience; public static partial class HttpClientBuilderExtensions { + internal const string StandardInnerHandlerPostfix = "standard-hedging-endpoint"; + private const string StandardHandlerPostfix = "standard-hedging"; - private const string StandardInnerHandlerPostfix = "standard-hedging-endpoint"; /// /// Adds a standard hedging handler which wraps the execution of the request with a standard hedging mechanism. @@ -28,12 +29,14 @@ public static partial class HttpClientBuilderExtensions /// A builder that can be used to configure the standard hedging behavior. /// /// - /// The standard hedging uses a pipeline pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. + /// The standard hedging uses a pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. /// By default, the selection from pool is based on the URL Authority (scheme + host + port). - /// - /// It is recommended that you configure the way the pipelines are selected by calling 'SelectPipelineByAuthority' extensions on top of returned . - /// - /// See for more details about the policies inside the pipeline. + /// It is recommended that you configure the way the strategies are selected by calling + /// + /// extensions. + /// + /// See for more details about the used resilience strategies. + /// /// public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder, Action configure) { @@ -55,12 +58,14 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt /// A builder that can be used to configure the standard hedging behavior. /// /// - /// The standard hedging uses a pipeline pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. + /// The standard hedging uses a pool of circuit breakers to ensure that unhealthy endpoints are not hedged against. /// By default, the selection from pool is based on the URL Authority (scheme + host + port). - /// - /// It is recommended that you configure the way the pipelines are selected by calling 'SelectPipelineByAuthority' extensions on top of returned . - /// - /// See for more details about the policies inside the pipeline. + /// It is recommended that you configure the way the strategies are selected by calling + /// + /// extensions. + /// + /// See for more details about the used resilience strategies. + /// /// public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder) { @@ -68,32 +73,71 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt var optionsName = builder.Name; var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services); - _ = builder.Services.AddRequestCloner(); + builder.Services.TryAddSingleton(); + _ = builder.Services.AddValidatedOptions(optionsName); + _ = builder.Services.AddValidatedOptions(optionsName); + _ = builder.Services.PostConfigure(optionsName, options => + { + options.HedgingOptions.HedgingActionGenerator = args => + { + if (!args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RequestSnapshot, out var snapshot)) + { + Throw.InvalidOperationException("Request message snapshot is not attached to the resilience context."); + } + + if (!args.PrimaryContext.Properties.TryGetValue(ResilienceKeys.RoutingStrategy, out var routingStrategy)) + { + Throw.InvalidOperationException("Routing strategy is not attached to the resilience context."); + } + + if (!routingStrategy.TryGetNextRoute(out var route)) + { + // no routes left, stop hedging + return null; + } + + var requestMessage = snapshot.Create().ReplaceHost(route); + + // replace the request message + args.ActionContext.Properties.Set(ResilienceKeys.RequestMessage, requestMessage); + + return () => args.Callback(args.ActionContext); + }; + }); // configure outer handler - var outerHandler = builder.AddResilienceHandler(StandardHandlerPostfix); - _ = outerHandler - .AddRoutingPolicy(serviceProvider => serviceProvider.GetRoutingFactory(routingBuilder.Name)) - .AddRequestMessageSnapshotPolicy() - .AddPolicy( - optionsName, - options => { }, - (builder, options, _) => builder - .AddTimeoutPolicy(StandardHedgingPolicyNames.TotalRequestTimeout, options.TotalRequestTimeoutOptions) - .AddHedgingPolicy(StandardHedgingPolicyNames.Hedging, CreateHedgedTaskProvider(outerHandler.PipelineName), options.HedgingOptions)); + var outerHandler = builder.AddResilienceHandler(StandardHandlerPostfix, (builder, context) => + { + var options = context.GetOptions(optionsName); + context.EnableReloads(optionsName); + + _ = builder + .AddStrategy(new RoutingResilienceStrategy(context.ServiceProvider.GetRoutingFactory(routingBuilder.Name))) + .AddStrategy(new RequestMessageSnapshotStrategy(context.ServiceProvider.GetRequiredService())) + .AddTimeout(options.TotalRequestTimeoutOptions) + .AddHedging(options.HedgingOptions); + }); // configure inner handler - var innerBuilder = builder.AddResilienceHandler(StandardInnerHandlerPostfix); - _ = innerBuilder - .SelectPipelineByAuthority(new DataClassification("FIXME", 1)) - .AddPolicy( - optionsName, - options => { }, - (builder, options, _) => builder - .AddBulkheadPolicy(StandardHedgingPolicyNames.Bulkhead, options.EndpointOptions.BulkheadOptions) - .AddCircuitBreakerPolicy(StandardHedgingPolicyNames.CircuitBreaker, options.EndpointOptions.CircuitBreakerOptions) - .AddTimeoutPolicy(StandardHedgingPolicyNames.AttemptTimeout, options.EndpointOptions.TimeoutOptions)); - - return new StandardHedgingHandlerBuilder(builder.Name, builder.Services, routingBuilder, innerBuilder); + var innerBuilder = builder.AddResilienceHandler( + StandardInnerHandlerPostfix, + (builder, context) => + { + var options = context.GetOptions(optionsName); + context.EnableReloads(optionsName); + + _ = builder + .AddRateLimiter(options.EndpointOptions.RateLimiterOptions) + .AddAdvancedCircuitBreaker(options.EndpointOptions.CircuitBreakerOptions) + .AddTimeout(options.EndpointOptions.TimeoutOptions); + }) + .SelectStrategyByAuthority(DataClassification.Unknown); + + return new StandardHedgingHandlerBuilder(builder.Name, builder.Services, routingBuilder); } + + private record StandardHedgingHandlerBuilder( + string Name, + IServiceCollection Services, + IRoutingStrategyBuilder RoutingStrategyBuilder) : IStandardHedgingHandlerBuilder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.cs deleted file mode 100644 index 65e26c53390..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Resilience; - -namespace Microsoft.Extensions.Http.Resilience; - -public static partial class HttpClientBuilderExtensions -{ - internal static HedgedTaskProvider CreateHedgedTaskProvider(string pipelineName) - { - var invokerProvider = Internal.ContextExtensions.CreateMessageInvokerProvider(pipelineName); - var routingStrategyProvider = HedgingContextExtensions.CreateRoutingStrategyProvider(pipelineName); - var snapshotProvider = HedgingContextExtensions.CreateRequestMessageSnapshotProvider(pipelineName); - - return (HedgingTaskProviderArguments args, out Task? result) => - { - // retrieve active routing strategy that was attached by RoutingPolicy - var strategy = routingStrategyProvider(args.Context)!; - if (!strategy.TryGetNextRoute(out var route)) - { - result = null; - - // Stryker disable once Boolean - return false; - } - - var snapshot = snapshotProvider(args.Context)!; - var request = snapshot.Create(); - request.RequestUri = request.RequestUri!.ReplaceHost(route); - var invoker = invokerProvider(args.Context)!; - - // send cloned request to a inner delegating handler - result = invoker.SendAsync(request, args.CancellationToken); - - return true; - }; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs index 62c3102b11d..a00f4d2aad0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientHedgingResiliencePredicates.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net.Http; using Microsoft.Shared.Diagnostics; +using Polly; using Polly.CircuitBreaker; namespace Microsoft.Extensions.Http.Resilience; @@ -26,4 +28,14 @@ _ when HttpClientResiliencePredicates.IsTransientHttpException(exception) => tru _ => false, }; }; + + /// + /// Determines whether an outcome should be treated by hedging as a transient failure. + /// + public static readonly Predicate> IsTransientHttpOutcome = outcome => outcome switch + { + { Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true, + { Exception: { } exception } when IsTransientHttpException(exception) => true, + _ => false, + }; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingStrategyOptions.cs similarity index 52% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingStrategyOptions.cs index d619e0fff60..24695dfa8e9 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingPolicyOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpHedgingStrategyOptions.cs @@ -2,25 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; -using Microsoft.Extensions.Resilience.Options; +using System.Threading.Tasks; +using Polly.Hedging; namespace Microsoft.Extensions.Http.Resilience; /// -/// Implementation of the for results. +/// Implementation of the for results. /// -public class HttpHedgingPolicyOptions : HedgingPolicyOptions +public class HttpHedgingStrategyOptions : HedgingStrategyOptions { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// By default the options is set to handle only transient failures, /// i.e. timeouts, 5xx responses and exceptions. /// - public HttpHedgingPolicyOptions() + public HttpHedgingStrategyOptions() { - ShouldHandleResultAsError = HttpClientResiliencePredicates.IsTransientHttpFailure; - ShouldHandleException = HttpClientHedgingResiliencePredicates.IsTransientHttpException; + ShouldHandle = args => new ValueTask(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(args.Outcome)); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs index b18a420d066..7f4f560b2d4 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpStandardHedgingResilienceOptions.cs @@ -1,53 +1,67 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Http.Resilience.Internal.Validators; using Microsoft.Extensions.Options.Validation; namespace Microsoft.Extensions.Http.Resilience; /// -/// Options for resilient pipeline of policies for usage in hedging HTTP scenarios. It is using 5 chained layers in this order (from the outermost to the innermost): -/// Total Request Timeout -> Hedging -> Bulkhead (per endpoint) -> Circuit Breaker (per endpoint) -> Attempt Timeout (per endpoint). +/// Options for the pipeline of resilience strategies for usage in hedging HTTP scenarios. /// /// /// -/// The configuration of each policy is initialized with the default options per type. The request goes through these policies: -/// -/// 1. Total request timeout policy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. -/// 2. The hedging policy executes the requests against multiple endpoints in case the dependency is slow or returns a transient error. -/// 3. The bulkhead policy limits the maximum number of concurrent requests being send to the dependency. -/// 4. The circuit breaker blocks the execution if too many direct failures or timeouts are detected. -/// 5. The attempt timeout policy limits each request attempt duration and throws if its exceeded. -/// -/// The last three policies are assigned to each individual endpoint. The selection of endpoint can be customized by -/// or -/// extensions. -/// +/// These options represents configuration for 5 chained layers in this order (from the outermost to the innermost): +/// +/// Total Request Timeout -> Hedging -> Bulkhead (per endpoint) -> Circuit Breaker (per endpoint) -> Attempt Timeout (per endpoint). +/// +/// The configuration of each resilience strategy is initialized with the default options per type. The request goes through these resilience strategies: +/// +/// +/// Total request timeout strategy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. +/// The hedging strategy executes the requests against multiple endpoints in case the dependency is slow or returns a transient error. +/// The bulkhead policy limits the maximum number of concurrent requests being send to the dependency. +/// The circuit breaker blocks the execution if too many direct failures or timeouts are detected. +/// The attempt timeout strategy limits each request attempt duration and throws if its exceeded. +/// +/// +/// The last three strategies are assigned to each individual endpoint. The selection of endpoint can be customized by +/// or +/// extensions. +/// /// By default, the endpoint is selected by authority (scheme + host + port). +/// /// public class HttpStandardHedgingResilienceOptions { /// - /// Gets or sets the timeout policy options for the total timeout applied on the request execution. + /// Gets or sets the timeout strategy options for the total timeout applied on the request execution. /// /// - /// By default it is initialized with a unique instance of + /// By default it is initialized with a unique instance of /// using default properties values. /// [Required] [ValidateObjectMembers] - public HttpTimeoutPolicyOptions TotalRequestTimeoutOptions { get; set; } = new(); + public HttpTimeoutStrategyOptions TotalRequestTimeoutOptions { get; set; } = new HttpTimeoutStrategyOptions + { + StrategyName = StandardHedgingStrategyNames.TotalRequestTimeout + }; /// - /// Gets or sets the hedging policy options. + /// Gets or sets the hedging strategy options. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpHedgingPolicyOptions HedgingOptions { get; set; } = new(); + public HttpHedgingStrategyOptions HedgingOptions { get; set; } = new HttpHedgingStrategyOptions + { + StrategyName = StandardHedgingStrategyNames.Hedging + }; /// /// Gets or sets the hedging endpoint options. diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs index 066508b55b9..cff4e452f6a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/IStandardHedgingHandlerBuilder.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Http.Resilience; @@ -25,11 +24,4 @@ public interface IStandardHedgingHandlerBuilder /// Gets the builder for the routing strategy. /// IRoutingStrategyBuilder RoutingStrategyBuilder { get; } - - /// - /// Gets for endpoint pipeline. - /// - /// This property is for internal use only. - [EditorBrowsable(EditorBrowsableState.Never)] - IHttpResiliencePipelineBuilder EndpointResiliencePipelineBuilder { get; } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs deleted file mode 100644 index 8bd9f6fc922..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingConstants.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal static class HedgingConstants -{ - public const string DeprecatedMessage = "Deprecated since 1.23.0 and will be removed in 1.32.0. " + - "Use standard hedging instead. If something prevents you from switching to standard hedging contact R9 team with your scenario " + - "and we can either extend the standard hedging or postpone the deletion of this API and making it official."; -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs deleted file mode 100644 index f237ef15551..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HedgingContextExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Shared.Diagnostics; -using Polly; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal static class HedgingContextExtensions -{ - private const string RoutingStrategyKey = "Hedging.RoutingStrategy"; - private const string SnapshotKey = "Hedging.RequestMessageSnapshot"; - - internal static Func CreateRequestMessageSnapshotProvider(string pipelineName) - { - _ = Throw.IfNullOrEmpty(pipelineName); - - var key = $"{SnapshotKey}-{pipelineName}"; - - return (context) => - { - if (context.TryGetValue(key, out var val)) - { - return (IHttpRequestMessageSnapshot)val; - } - - return null; - }; - } - - internal static Action CreateRequestMessageSnapshotSetter(string pipelineName) - { - var key = $"{SnapshotKey}-{pipelineName}"; - - return (context, snapshot) => context[key] = snapshot; - } - - internal static Func CreateRoutingStrategyProvider(string pipelineName) - { - _ = Throw.IfNullOrEmpty(pipelineName); - - var key = $"{RoutingStrategyKey}-{pipelineName}"; - - return (context) => - { - if (context.TryGetValue(key, out var val)) - { - return (IRequestRoutingStrategy)val; - } - - return null; - }; - } - - internal static Action CreateRoutingStrategySetter(string pipelineName) - { - var key = $"{RoutingStrategyKey}-{pipelineName}"; - - return (context, invoker) => context[key] = invoker; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs deleted file mode 100644 index 5f520d844c9..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/HttpResiliencePipelineBuilderExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Routing.Internal; -using Microsoft.Extensions.Resilience.Internal; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal static class HttpResiliencePipelineBuilderExtensions -{ - public static IHttpResiliencePipelineBuilder AddRequestMessageSnapshotPolicy(this IHttpResiliencePipelineBuilder builder) - { - var pipelineName = builder.PipelineName; - - _ = builder.AddPolicy((builder, serviceProvider) => builder.AddPolicy(ActivatorUtilities.CreateInstance(serviceProvider, pipelineName))); - - return builder; - } - - public static IHttpResiliencePipelineBuilder AddRoutingPolicy( - this IHttpResiliencePipelineBuilder builder, - Func factory) - { - var pipelineName = builder.PipelineName; - - _ = builder.AddPolicy((builder, serviceProvider) => builder.AddPolicy(new RoutingPolicy(pipelineName, factory(serviceProvider)))); - - return builder; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs deleted file mode 100644 index b1b9fe8355d..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotPolicy.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Polly; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// This policy creates a snapshot of before executing the hedging to prevent race conditions when cloning and modifying the message at the same time. -/// This way, all hedged requests will have an unique instance of the message available from snapshot without the need to access the original one for cloning. -/// -internal sealed class RequestMessageSnapshotPolicy : AsyncPolicy -{ - private readonly IRequestClonerInternal _requestCloner; - private readonly Func _requestProvider; - private readonly Action _snapshotSetter; - - public RequestMessageSnapshotPolicy(string pipelineName, IRequestClonerInternal requestCloner) - { - _requestCloner = requestCloner; - _requestProvider = ContextExtensions.CreateRequestMessageProvider(pipelineName); - _snapshotSetter = HedgingContextExtensions.CreateRequestMessageSnapshotSetter(pipelineName); - } - - protected override async Task ImplementationAsync( - Func> action, - Context context, - CancellationToken cancellationToken, - bool continueOnCapturedContext) - { - using var snapshot = _requestCloner.CreateSnapshot(_requestProvider(context)!); - _snapshotSetter(context, snapshot); - - return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs new file mode 100644 index 00000000000..c4c99e95cbf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// This strategy creates a snapshot of before executing the hedging to prevent race conditions when cloning and modifying the message at the same time. +/// This way, all hedged requests will have an unique instance of the message available from snapshot without the need to access the original one for cloning. +/// +internal sealed class RequestMessageSnapshotStrategy : ResilienceStrategy +{ + private readonly IRequestCloner _requestCloner; + + public RequestMessageSnapshotStrategy(IRequestCloner requestCloner) + { + _requestCloner = requestCloner; + } + + protected override async ValueTask> ExecuteCoreAsync( + Func>> callback, + ResilienceContext context, + TState state) + { + if (!context.Properties.TryGetValue(ResilienceKeys.RequestMessage, out var request) || request is null) + { + Throw.InvalidOperationException("The HTTP request message was not found in the resilience context."); + } + + using var snapshot = _requestCloner.CreateSnapshot(request); + + context.Properties.Set(ResilienceKeys.RequestSnapshot, snapshot); + + return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs deleted file mode 100644 index f11a3b762a9..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingHandlerBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Http.Resilience.Internal.Routing; - -internal sealed class StandardHedgingHandlerBuilder : IStandardHedgingHandlerBuilder -{ - public StandardHedgingHandlerBuilder(string name, IServiceCollection services, IRoutingStrategyBuilder routingStrategyBuilder, IHttpResiliencePipelineBuilder endpointResiliencePipelineBuilder) - { - Name = name; - Services = services; - RoutingStrategyBuilder = routingStrategyBuilder; - EndpointResiliencePipelineBuilder = endpointResiliencePipelineBuilder; - } - - public string Name { get; } - - public IServiceCollection Services { get; } - - public IRoutingStrategyBuilder RoutingStrategyBuilder { get; } - - public IHttpResiliencePipelineBuilder EndpointResiliencePipelineBuilder { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingStrategyNames.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingStrategyNames.cs index c37b8277729..b4dac845036 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingPolicyNames.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/StandardHedgingStrategyNames.cs @@ -3,11 +3,11 @@ namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; -internal static class StandardHedgingPolicyNames +internal static class StandardHedgingStrategyNames { public const string CircuitBreaker = "StandardHedging-CircuitBreaker"; - public const string Bulkhead = "StandardHedging-Bulkhead"; + public const string RateLimiter = "StandardHedging-RateLimiter"; public const string Hedging = "StandardHedging-Hedging"; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs index f5370aade5b..4590ee0c889 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Validators/HttpStandardHedgingResilienceOptionsCustomValidator.cs @@ -26,20 +26,20 @@ public ValidateOptionsResult Validate(string? name, HttpStandardHedgingResilienc builder.AddError($"The hedging routing is not configured for '{name}' HTTP client."); } - if (options.EndpointOptions.TimeoutOptions.TimeoutInterval > options.TotalRequestTimeoutOptions.TimeoutInterval) + if (options.EndpointOptions.TimeoutOptions.Timeout > options.TotalRequestTimeoutOptions.Timeout) { - builder.AddError($"Total request timeout policy must have a greater timeout than the attempt timeout policy. " + - $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + - $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalSeconds}s"); + builder.AddError($"Total request timeout strategy must have a greater timeout than the attempt timeout strategy. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.Timeout.TotalSeconds}s, " + + $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.Timeout.TotalSeconds}s"); } - var timeout = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * CircuitBreakerTimeoutMultiplier); + var timeout = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.Timeout.TotalMilliseconds * CircuitBreakerTimeoutMultiplier); if (options.EndpointOptions.CircuitBreakerOptions.SamplingDuration < timeout) { - builder.AddError("The sampling duration of circuit breaker policy needs to be at least double of " + - $"an attempt timeout policy’s timeout interval, in order to be effective. " + + builder.AddError("The sampling duration of circuit breaker strategy needs to be at least double of " + + $"an attempt timeout strategy’s timeout interval, in order to be effective. " + $"Sampling Duration: {options.EndpointOptions.CircuitBreakerOptions.SamplingDuration.TotalSeconds}s," + - $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalSeconds}s"); + $"Attempt Timeout: {options.EndpointOptions.TimeoutOptions.Timeout.TotalSeconds}s"); } // if generator is specified we cannot calculate the max hedging delay @@ -48,10 +48,10 @@ public ValidateOptionsResult Validate(string? name, HttpStandardHedgingResilienc var maxHedgingDelay = TimeSpan.FromMilliseconds((options.HedgingOptions.MaxHedgedAttempts - 1) * options.HedgingOptions.HedgingDelay.TotalMilliseconds); // Stryker disable once Equality - if (maxHedgingDelay > options.TotalRequestTimeoutOptions.TimeoutInterval) + if (maxHedgingDelay > options.TotalRequestTimeoutOptions.Timeout) { - builder.AddError($"The cumulative delay of the hedging policy is larger than total request timeout interval. " + - $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + + builder.AddError($"The cumulative delay of the hedging strategy is larger than total request timeout interval. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.Timeout.TotalSeconds}s, " + $"Cumulative Hedging Delay: {maxHedgingDelay.TotalSeconds}s"); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs index 101d40b1449..acfe72386ae 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/StandardHedgingHandlerBuilderExtensions.cs @@ -3,23 +3,27 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Net.Http; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Resilience; +#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods + /// /// Extensions for . /// public static class StandardHedgingHandlerBuilderExtensions { /// - /// Configures the for the standard hedging pipeline. + /// Configures the for the standard hedging strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The section that the options will bind against. /// The same builder instance. [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpStandardHedgingResilienceOptions))] @@ -42,14 +46,12 @@ public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHand } /// - /// Configures the for the standard hedging pipeline. + /// Configures the for the standard hedging strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The configure method. /// The same builder instance. -#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHandlerBuilder builder, Action configure) -#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods { _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); @@ -58,15 +60,13 @@ public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHand } /// - /// Configures the for the standard hedging pipeline. + /// Configures the for the standard hedging strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The configure method. /// The same builder instance. -#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods [Experimental] public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHandlerBuilder builder, Action configure) -#pragma warning restore S3872 // Parameter names should not duplicate the names of their methods { _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); @@ -77,34 +77,38 @@ public static IStandardHedgingHandlerBuilder Configure(this IStandardHedgingHand } /// - /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// Instructs the underlying strategy builder to select the strategy instance by redacted authority (scheme + host + port). /// /// The builder instance. /// The data class associated with the authority. /// The same builder instance. /// The authority is redacted using retrieved for . - public static IStandardHedgingHandlerBuilder SelectPipelineByAuthority(this IStandardHedgingHandlerBuilder builder, DataClassification classification) + public static IStandardHedgingHandlerBuilder SelectStrategyByAuthority(this IStandardHedgingHandlerBuilder builder, DataClassification classification) { _ = Throw.IfNull(builder); - _ = builder.EndpointResiliencePipelineBuilder.SelectPipelineByAuthority(classification); + var strategyName = StrategyNameHelper.GetName(builder.Name, HttpClientBuilderExtensions.StandardInnerHandlerPostfix); + + StrategyKeyProviderHelper.SelectStrategyByAuthority(builder.Services, strategyName, classification); return builder; } /// - /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// Instructs the underlying strategy builder to select the strategy instance by custom selector. /// /// The builder instance. - /// The factory that returns selector. + /// The factory that returns key selector. /// The same builder instance. - /// The pipeline key is used in metrics and logs, do not return any sensitive value. - public static IStandardHedgingHandlerBuilder SelectPipelineBy(this IStandardHedgingHandlerBuilder builder, Func selectorFactory) + /// The strategy key is used in metrics and logs, do not return any sensitive value. + public static IStandardHedgingHandlerBuilder SelectStrategyBy(this IStandardHedgingHandlerBuilder builder, Func> selectorFactory) { _ = Throw.IfNull(builder); _ = Throw.IfNull(selectorFactory); - _ = builder.EndpointResiliencePipelineBuilder.SelectPipelineBy(selectorFactory); + var strategyName = StrategyNameHelper.GetName(builder.Name, HttpClientBuilderExtensions.StandardInnerHandlerPostfix); + + StrategyKeyProviderHelper.SelectStrategyBy(builder.Services, strategyName, selectorFactory); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/HttpKey.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/HttpKey.cs new file mode 100644 index 00000000000..9098109c08d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/HttpKey.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal readonly record struct HttpKey(string Name, string Key) +{ + public static readonly IEqualityComparer BuilderComparer = new BuilderEqualityComparer(); + + private class BuilderEqualityComparer : IEqualityComparer + { + public bool Equals(HttpKey x, HttpKey y) => StringComparer.Ordinal.Equals(x.Name, y.Name); + + public int GetHashCode([DisallowNull] HttpKey obj) => StringComparer.Ordinal.GetHashCode(obj.Name); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/HttpRequestMessageExtensions.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpRequestMessageExtensions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/HttpRequestMessageExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs index d9be95049e4..c7a401a8fba 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IHttpRequestMessageSnapshot.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// -/// The snapshot of created by . +/// The snapshot of created by . /// internal interface IHttpRequestMessageSnapshot : IDisposable { diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/IRandomizer.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs similarity index 93% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs index f56e2f85d3d..b3aca149237 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IRequestClonerInternal.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// /// Internal interface for cloning an instance. /// -internal interface IRequestClonerInternal +internal interface IRequestCloner { /// /// Creates a snapshot of that can be then used for cloning. diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/Randomizer.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs similarity index 89% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs index 433b0394b3d..1c297b96edc 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/DefaultRequestCloner.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs @@ -11,20 +11,15 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// -/// Default implementation of interface for cloning requests. +/// Default implementation of interface for cloning requests. /// /// /// The request content is only copied, not deeply cloned. /// If the request is cloned outside the middlewares, the content must be cloned as well. /// -internal sealed class DefaultRequestCloner : IRequestClonerInternal +internal sealed class RequestCloner : IRequestCloner { - public IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request) - { - _ = Throw.IfNull(request); - - return new Snapshot(request); - } + public IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request) => new Snapshot(request); private sealed class Snapshot : IHttpRequestMessageSnapshot { @@ -42,7 +37,7 @@ public Snapshot(HttpRequestMessage request) { if (request.Content is StreamContent) { - Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned using the {nameof(DefaultRequestCloner)}."); + Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned using the {nameof(RequestCloner)}."); } _method = request.Method; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs new file mode 100644 index 00000000000..636ef482b8e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Extensions.Http.Telemetry; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class ResilienceKeys +{ + public static readonly ResiliencePropertyKey RequestMessage = new("Resilience.Http.RequestMessage"); + + public static readonly ResiliencePropertyKey RoutingStrategy = new("Resilience.Http.RequestRoutingStrategy"); + + public static readonly ResiliencePropertyKey RequestSnapshot = new("Resilience.Http.Snapshot"); + + public static readonly ResiliencePropertyKey RequestMetadata = new(TelemetryConstants.RequestMetadataKey); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RetryAfterHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RetryAfterHelper.cs new file mode 100644 index 00000000000..f5975a9653f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RetryAfterHelper.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class RetryAfterHelper +{ + /// + /// Parses Retry-After value from the relevant HTTP response header. + /// If not found then it will return . + /// + /// . + internal static bool TryParse(HttpResponseMessage response, TimeProvider timeProvider, out TimeSpan retryAfter) + { + var (parsed, delay) = response.Headers.RetryAfter switch + { + { Date: { } date } => (true, date - timeProvider.GetUtcNow()), + { Delta: { } delta } => (true, delta), + _ => (false, default) + }; + + // It can happen that the server returns a point in time in the past. + // This indicates that retry can happen immediately. + if (parsed && delay < TimeSpan.Zero) + { + delay = TimeSpan.Zero; + } + + retryAfter = delay; + return parsed; + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/UriExtensions.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/UriExtensions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/UriExtensions.cs diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ValidationHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ValidationHelper.cs new file mode 100644 index 00000000000..5162c071bf7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ValidationHelper.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Polly; +using Polly.Retry; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class ValidationHelper +{ + public static TimeSpan GetAggregatedDelay(RetryStrategyOptions options) + { + // Instead of re-implementing the calculations of delays we can just + // execute the retry strategy and aggregate the delays by using the RetryDelayGenerator + // callback that receives the delay hint for each attempt. + + try + { + var aggregatedDelay = TimeSpan.Zero; + var builder = new ResilienceStrategyBuilder + { + Randomizer = () => 1.0 // disable randomization so the output is always the same + }; + + builder.AddRetry(new() + { + RetryCount = options.RetryCount, + BaseDelay = options.BaseDelay, + BackoffType = options.BackoffType, + ShouldHandle = _ => PredicateResult.True, // always retry until all retries are exhausted + RetryDelayGenerator = args => + { + // the delay hint is calculated for this attempt by the retry strategy + aggregatedDelay += args.Arguments.DelayHint; + + // return zero delay, so no waiting + return new ValueTask(TimeSpan.Zero); + }, + }) + .Build() + .Execute(static () => { }); // this executes all retries and we aggregate the delays immediately + + return aggregatedDelay; + } + catch (OverflowException) + { + return TimeSpan.MaxValue; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index 51c3cb0cefd..b63bf4ab1c4 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -1,8 +1,9 @@ - + Microsoft.Extensions.Http.Resilience Resilience mechanisms for HTTP Client. Resilience + $(NoWarn);R9A049 @@ -19,7 +20,7 @@ normal - 99 + 98 100 @@ -44,4 +45,5 @@ + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerStrategyOptions.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerStrategyOptions.cs index 6f316216d75..e08086eaee9 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerPolicyOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpCircuitBreakerStrategyOptions.cs @@ -2,25 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; -using Microsoft.Extensions.Resilience.Options; +using System.Threading.Tasks; +using Polly.CircuitBreaker; namespace Microsoft.Extensions.Http.Resilience; /// -/// Implementation of the for results. +/// Implementation of the for results. /// -public class HttpCircuitBreakerPolicyOptions : CircuitBreakerPolicyOptions +public class HttpCircuitBreakerStrategyOptions : AdvancedCircuitBreakerStrategyOptions { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// By default the options is set to handle only transient failures, /// i.e. timeouts, 5xx responses and exceptions. /// - public HttpCircuitBreakerPolicyOptions() + public HttpCircuitBreakerStrategyOptions() { - ShouldHandleResultAsError = result => HttpClientResiliencePredicates.IsTransientHttpFailure(result); - ShouldHandleException = exp => HttpClientResiliencePredicates.IsTransientHttpException(exp); + ShouldHandle = args => new ValueTask(HttpClientResiliencePredicates.IsTransientHttpOutcome(args.Outcome)); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs deleted file mode 100644 index 6f014c349ce..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResilienceGenerators.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Resilience.Options; - -namespace Microsoft.Extensions.Http.Resilience; - -/// -/// Static generators used within the current package. -/// -public static class HttpClientResilienceGenerators -{ - /// - /// Gets the generator that is able to generate delay based on the "Retry-After" response header. - /// - public static readonly Func, TimeSpan> HandleRetryAfterHeader = RetryAfterHelper.Generator; -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs index b50e721edc4..a60149d7788 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpClientResiliencePredicates.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http; using Microsoft.Shared.Diagnostics; +using Polly; using Polly.Timeout; namespace Microsoft.Extensions.Http.Resilience; @@ -15,7 +16,7 @@ namespace Microsoft.Extensions.Http.Resilience; public static class HttpClientResiliencePredicates { /// - /// Determines whether an exception should be treated by policies as a transient failure. + /// Determines whether an exception should be treated by resilience strategies as a transient failure. /// public static readonly Predicate IsTransientHttpException = exception => { @@ -43,6 +44,16 @@ public static class HttpClientResiliencePredicates }; + /// + /// Determines whether an outcome should be treated by resilience strategies as a transient failure. + /// + public static readonly Predicate> IsTransientHttpOutcome = outcome => outcome switch + { + { Result: { } response } when IsTransientHttpFailure(response) => true, + { Exception: { } exception } when IsTransientHttpException(exception) => true, + _ => false + }; + private const int InternalServerErrorCode = (int)HttpStatusCode.InternalServerError; private const int TooManyRequests = 429; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRateLimiterStrategyOptions.cs similarity index 54% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRateLimiterStrategyOptions.cs index ee9d1071a8f..774987d0628 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutPolicyOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRateLimiterStrategyOptions.cs @@ -1,13 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Resilience.Options; +using Polly.RateLimiting; namespace Microsoft.Extensions.Http.Resilience; /// -/// Implementation of the for HTTP scenarios. +/// Implementation of the for HTTP scenarios. /// -public class HttpTimeoutPolicyOptions : TimeoutPolicyOptions +public class HttpRateLimiterStrategyOptions : RateLimiterStrategyOptions { } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs deleted file mode 100644 index e6e6ae37756..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryPolicyOptions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using Microsoft.Extensions.Resilience.Options; - -namespace Microsoft.Extensions.Http.Resilience; - -/// -/// Implementation of the for results. -/// -public class HttpRetryPolicyOptions : RetryPolicyOptions -{ - private bool _shouldRetryAfterHeader; - - /// - /// Gets or sets a value indicating whether to retry after header. - /// - /// - /// The default value is false. - /// - /// - /// If the property is set to true, then the DelayGenerator will maximize - /// based on the RetryAfter header rules, otherwise it will remain . - /// - public bool ShouldRetryAfterHeader - { - get => _shouldRetryAfterHeader; - set - { - _shouldRetryAfterHeader = value; - RetryDelayGenerator = _shouldRetryAfterHeader ? HttpClientResilienceGenerators.HandleRetryAfterHeader : null; - } - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// By default, the options are set to handle only transient failures, - /// that is, timeouts, 5xx responses, and exceptions. - /// - public HttpRetryPolicyOptions() - { - ShouldHandleResultAsError = result => HttpClientResiliencePredicates.IsTransientHttpFailure(result); - ShouldHandleException = exp => HttpClientResiliencePredicates.IsTransientHttpException(exp); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs new file mode 100644 index 00000000000..f27e4d49d84 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpRetryStrategyOptions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Resilience.Internal; +using Polly.Retry; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// Implementation of the for results. +/// +public class HttpRetryStrategyOptions : RetryStrategyOptions +{ + private bool _shouldRetryAfterHeader; + + /// + /// Initializes a new instance of the class. + /// + /// + /// By default, the options are set to handle only transient failures, + /// that is, timeouts, 5xx responses, and exceptions. + /// + public HttpRetryStrategyOptions() + { + ShouldHandle = args => new ValueTask(HttpClientResiliencePredicates.IsTransientHttpOutcome(args.Outcome)); + BackoffType = RetryBackoffType.ExponentialWithJitter; + ShouldRetryAfterHeader = true; + } + + /// + /// Gets or sets a value indicating whether to use the Retry-After header for the retry delays. + /// + /// + /// Defaults to . + /// + /// + /// If the property is set to then the generator will resolve the delay + /// based on the Retry-After header rules, otherwise it will return + /// that was suggested by the retry strategy. + /// + public bool ShouldRetryAfterHeader + { + get => _shouldRetryAfterHeader; + set + { + _shouldRetryAfterHeader = value; + + if (_shouldRetryAfterHeader) + { + RetryDelayGenerator = args => args.Outcome.Result switch + { + HttpResponseMessage response when RetryAfterHelper.TryParse(response, TimeProvider.System, out var retryAfter) => new ValueTask(retryAfter), + _ => new ValueTask(args.Arguments.DelayHint) + }; + } + else + { + RetryDelayGenerator = null; + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutStrategyOptions.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutStrategyOptions.cs index d522e58fc0d..67a562f2be8 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpBulkheadPolicyOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/HttpTimeoutStrategyOptions.cs @@ -1,13 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Resilience.Options; +using Polly.Timeout; namespace Microsoft.Extensions.Http.Resilience; /// -/// Implementation of the for HTTP scenarios. +/// Implementation of the for HTTP scenarios. /// -public class HttpBulkheadPolicyOptions : BulkheadPolicyOptions +public class HttpTimeoutStrategyOptions : TimeoutStrategyOptions { } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs deleted file mode 100644 index cf80be62226..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/HttpPolicyFactoryServiceCollectionExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.ExceptionSummarization; -using Microsoft.Extensions.Http.Telemetry; -using Microsoft.Extensions.Resilience; -using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.Text; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Extension class for the Service Collection DI container. -/// -[ExcludeFromCodeCoverage] -internal static class HttpPolicyFactoryServiceCollectionExtensions -{ - private static readonly ServiceDescriptor _serviceDescriptor = ServiceDescriptor.Singleton(); - - /// - /// Configures the failure result dimensions that will be emitted for Http failures, by exploring the inner exceptions and their properties. - /// - /// The services. - /// The input . - public static IServiceCollection ConfigureHttpFailureResultContext(this IServiceCollection services) - { - _ = Throw.IfNull(services); - - // don't add any new service if this method is called multiple times - if (services.Contains(_serviceDescriptor)) - { - return services; - } - - services.Add(_serviceDescriptor); - - return services - .AddExceptionSummarizer(b => b.AddHttpProvider()) - .ConfigureFailureResultContext((response) => - { - if (response != null) - { - var statusCodeName = response.StatusCode.ToInvariantString(); - if (string.IsNullOrEmpty(statusCodeName) || char.IsDigit(statusCodeName[0])) - { - statusCodeName = TelemetryConstants.Unknown; - } - - return FailureResultContext.Create(failureReason: ((int)response.StatusCode).ToInvariantString(), additionalInformation: statusCodeName); - } - - return FailureResultContext.Create(); - }); - } - - private sealed class Marker - { - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs deleted file mode 100644 index 865fedd4066..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Polly/Internal/RetryAfterHelper.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using Microsoft.Extensions.Resilience.Options; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal static class RetryAfterHelper -{ - public static TimeSpan Generator(RetryDelayArguments args) - { - if (args.Result?.Result is HttpResponseMessage response) - { - return ParseRetryAfterHeader(response, TimeProvider.System); - } - - return TimeSpan.Zero; - } - - /// - /// Parses Retry-After value from the relevant HTTP response header. - /// If not found then it will return . - /// - /// HTTP response message. - /// Current time provider for conversion of absolute values. - /// The delay according to the Retry-After header. - /// . - internal static TimeSpan ParseRetryAfterHeader(HttpResponseMessage httpResponse, TimeProvider timeProvider) - { - var headers = httpResponse?.Headers; - if (headers?.RetryAfter != null) - { - if (headers.RetryAfter.Date.HasValue) - { - // An absolute point in time - return headers.RetryAfter.Date.Value - timeProvider.GetUtcNow(); - } - else if (headers.RetryAfter.Delta.HasValue) - { - // A relative number of seconds - return headers.RetryAfter.Delta.Value; - } - } - - return TimeSpan.Zero; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs index c8abdf825d5..a119b667fa2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs @@ -2,84 +2,166 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.ExceptionSummarization; using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.Resilience; using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Text; using Polly; +using Polly.Registry; namespace Microsoft.Extensions.Http.Resilience; public static partial class HttpClientBuilderExtensions { /// - /// Adds a that uses a named inline resilience pipeline configured by returned . + /// Adds a resilience strategy handler that uses a named inline resilience strategy. /// /// The builder instance. - /// The custom identifier for the pipeline, used in the name of the pipeline. - /// The HTTP pipeline builder instance. + /// The custom identifier for the resilience strategy, used in the name of the strategy. + /// The callback that configures the strategy. + /// The HTTP strategy builder instance. /// - /// The final pipeline name is combination of and . - /// Use pipeline identifier if your HTTP client contains multiple resilience handlers. + /// The final strategy name is combination of and . + /// Use strategy name identifier if your HTTP client contains multiple resilience handlers. /// - public static IHttpResiliencePipelineBuilder AddResilienceHandler(this IHttpClientBuilder builder, string pipelineIdentifier) + public static IHttpResilienceStrategyBuilder AddResilienceHandler( + this IHttpClientBuilder builder, + string strategyName, + Action> configure) { _ = Throw.IfNull(builder); - _ = Throw.IfNullOrEmpty(pipelineIdentifier); + _ = Throw.IfNullOrEmpty(strategyName); + _ = Throw.IfNull(configure); - var pipelineBuilder = builder.AddHttpResiliencePipeline(pipelineIdentifier); + return builder.AddResilienceHandler(strategyName, ConfigureBuilder); + + void ConfigureBuilder(ResilienceStrategyBuilder builder, ResilienceHandlerContext context) => configure(builder); + } + + /// + /// Adds a resilience strategy handler that uses a named inline resilience strategy. + /// + /// The builder instance. + /// The custom identifier for the resilience strategy, used in the name of the strategy. + /// The callback that configures the strategy. + /// The HTTP strategy builder instance. + /// + /// The final strategy name is combination of and . + /// Use strategy name identifier if your HTTP client contains multiple resilience handlers. + /// + public static IHttpResilienceStrategyBuilder AddResilienceHandler( + this IHttpClientBuilder builder, + string strategyName, + Action, ResilienceHandlerContext> configure) + { + _ = Throw.IfNull(builder); + _ = Throw.IfNullOrEmpty(strategyName); + _ = Throw.IfNull(configure); + + var strategyBuilder = builder.AddHttpResilienceStrategy(strategyName, configure); _ = builder.AddHttpMessageHandler(serviceProvider => { - var selector = CreatePipelineSelector(serviceProvider, pipelineBuilder.PipelineName); - return new ResilienceHandler(pipelineBuilder.PipelineName, selector); + var selector = CreateStrategySelector(serviceProvider, strategyBuilder.StrategyName); + var provider = serviceProvider.GetRequiredService>(); + + return new ResilienceHandler(selector); }); - return pipelineBuilder; + return strategyBuilder; } - private static Func> CreatePipelineSelector(IServiceProvider serviceProvider, string pipelineName) + private static Func> CreateStrategySelector(IServiceProvider serviceProvider, string strategyName) { - var resilienceProvider = serviceProvider.GetRequiredService(); - var pipelineKeyProvider = serviceProvider.GetPipelineKeyProvider(pipelineName); + var resilienceProvider = serviceProvider.GetRequiredService>(); + var strategyKeyProvider = serviceProvider.GetStrategyKeyProvider(strategyName); - if (pipelineKeyProvider == null) + if (strategyKeyProvider == null) { - var pipeline = resilienceProvider.GetPipeline(pipelineName); - return _ => pipeline; + var strategy = resilienceProvider.GetStrategy(new HttpKey(strategyName, string.Empty)); + return _ => strategy; } else { - TouchPipelineKey(pipelineKeyProvider); + TouchStrategyKey(strategyKeyProvider); return request => { - var pipelineKey = pipelineKeyProvider.GetPipelineKey(request); - return resilienceProvider.GetPipeline(pipelineName, pipelineKey); + var key = strategyKeyProvider.GetStrategyKey(request); + return resilienceProvider.GetStrategy(new HttpKey(strategyName, key)); }; } } - private static void TouchPipelineKey(IPipelineKeyProvider provider) + private static void TouchStrategyKey(IStrategyKeyProvider provider) { - // this piece of code eagerly checks that the pipeline key provider is correctly configured + // this piece of code eagerly checks that the strategy key provider is correctly configured // combined with HttpClient auto-activation we can detect any issues on startup - if (provider is ByAuthorityPipelineKeyProvider) + if (provider is ByAuthorityStrategyKeyProvider) { #pragma warning disable S1075 // URIs should not be hardcoded - this URL is not used for any real request, nor in any telemetry using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:123"); #pragma warning restore S1075 // URIs should not be hardcoded - _ = provider.GetPipelineKey(request); + _ = provider.GetStrategyKey(request); } } - private static HttpResiliencePipelineBuilder AddHttpResiliencePipeline(this IHttpClientBuilder builder, string pipelineIdentifier) + private static IHttpResilienceStrategyBuilder AddHttpResilienceStrategy( + this IHttpClientBuilder builder, + string name, + Action, ResilienceHandlerContext> configure) + { + var strategyName = StrategyNameHelper.GetName(builder.Name, name); + var key = new HttpKey(strategyName, string.Empty); + + _ = builder.Services.AddResilienceStrategy(key, (builder, context) => configure(builder, new ResilienceHandlerContext(context))); + + ConfigureHttpServices(builder.Services); + + return new HttpResilienceStrategyBuilder(strategyName, builder.Services); + } + + private static void ConfigureHttpServices(IServiceCollection services) { - _ = builder.Services.ConfigureHttpFailureResultContext(); - var pipelineName = PipelineNameHelper.GetPipelineName(builder.Name, pipelineIdentifier); - var pipelineBuilder = builder.Services.AddResiliencePipeline(pipelineName); + // don't add any new service if this method is called multiple times + if (services.Contains(Marker.ServiceDescriptor)) + { + return; + } + + services.Add(Marker.ServiceDescriptor); - return new HttpResiliencePipelineBuilder(pipelineBuilder); + // This code configure the multi-instance support of the registry + _ = services.Configure>(options => + { + options.BuilderNameFormatter = key => key.Name; + options.StrategyKeyFormatter = key => key.Key; + options.BuilderComparer = HttpKey.BuilderComparer; + }); + + _ = services + .AddExceptionSummarizer(b => b.AddHttpProvider()) + .ConfigureFailureResultContext((response) => + { + if (response != null) + { + return FailureResultContext.Create( + failureReason: ((int)response.StatusCode).ToInvariantString(), + additionalInformation: response.StatusCode.ToInvariantString()); + } + + return FailureResultContext.Create(); + }); } + + private sealed class Marker + { + public static readonly ServiceDescriptor ServiceDescriptor = ServiceDescriptor.Singleton(); + } + + private record HttpResilienceStrategyBuilder(string StrategyName, IServiceCollection Services) : IHttpResilienceStrategyBuilder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs index 856b0297f9b..4692c6c603f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.StandardResilience.cs @@ -2,15 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Net.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.Http.Resilience.Internal.Validators; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Options.Validation; -using Microsoft.Extensions.Resilience; -using Microsoft.Extensions.Resilience.Internal; using Microsoft.Shared.Diagnostics; +using Polly; namespace Microsoft.Extensions.Http.Resilience; @@ -19,16 +18,16 @@ public static partial class HttpClientBuilderExtensions private const string StandardIdentifier = "standard"; /// - /// Adds a that uses a standard resilience pipeline with default options to send the requests and handle any transient errors. - /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// Adds a standard resilience handler that uses a multiple resilience strategies with default options to send the requests and handle any transient errors. /// /// The builder instance. /// The section that the options will bind against. - /// The HTTP pipeline builder instance. + /// The HTTP resilience handler builder instance. /// - /// See for more details about the individual policies configured by this method. + /// The resilience strategy combines multiple strategies that are configured based on HTTP-specific options with recommended defaults. + /// See for more details about the individual resilience strategies configured by this method. /// - public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, IConfigurationSection section) + public static IHttpStandardResilienceStrategyBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, IConfigurationSection section) { _ = Throw.IfNull(builder); _ = Throw.IfNull(section); @@ -37,16 +36,16 @@ public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandle } /// - /// Adds a that uses a standard resilience pipeline with default options to send the requests and handle any transient errors. - /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// Adds a standard resilience handler that uses a multiple resilience strategies with default options to send the requests and handle any transient errors. /// /// The builder instance. - /// The action that configures the resilience options. - /// The HTTP pipeline builder instance. + /// The callback that configures the options. + /// The HTTP resilience handler builder instance. /// - /// See for more details about the individual policies configured by this method. + /// The resilience strategy combines multiple strategies that are configured based on HTTP-specific options with recommended defaults. + /// See for more details about the individual resilience strategies configured by this method. /// - public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, Action configure) + public static IHttpStandardResilienceStrategyBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder, Action configure) { _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); @@ -55,40 +54,40 @@ public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandle } /// - /// Adds a that uses a standard resilience pipeline with default - /// to send the requests and handle any transient errors. - /// The pipeline combines multiple policies that are configured based on HTTP-specific options with recommended defaults. + /// Adds a standard resilience handler that uses a multiple resilience strategies with default options to send the requests and handle any transient errors. /// /// The builder instance. - /// The HTTP pipeline builder instance. + /// The HTTP resilience handler builder instance. /// - /// See for more details about the individual policies configured by this method. + /// The resilience strategy combines multiple strategies that are configured based on HTTP-specific options with recommended defaults. + /// See for more details about the individual resilience strategies configured by this method. /// - public static IHttpStandardResiliencePipelineBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder) + public static IHttpStandardResilienceStrategyBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder) { _ = Throw.IfNull(builder); - _ = builder.Services.ConfigureHttpFailureResultContext(); + var optionsName = StrategyNameHelper.GetName(builder.Name, StandardIdentifier); - return new HttpStandardResiliencePipelineBuilder(builder.AddResilienceHandler(StandardIdentifier).AddStandardPipeline()); - } + _ = builder.Services.AddValidatedOptions(optionsName); + _ = builder.Services.AddValidatedOptions(optionsName); - private static HttpResiliencePipelineBuilder AddStandardPipeline(this IResiliencePipelineBuilder builder) - { - var resilienceBuilder = - builder.AddPolicy( - builder.PipelineName, - options => { }, - (builder, options, _) => - builder - .AddBulkheadPolicy(StandardPolicyNames.Bulkhead, options.BulkheadOptions) - .AddTimeoutPolicy(StandardPolicyNames.TotalRequestTimeout, options.TotalRequestTimeoutOptions) - .AddRetryPolicy(StandardPolicyNames.Retry, options.RetryOptions) - .AddCircuitBreakerPolicy(StandardPolicyNames.CircuitBreaker, options.CircuitBreakerOptions) - .AddTimeoutPolicy(StandardPolicyNames.AttemptTimeout, options.AttemptTimeoutOptions)); + _ = builder.AddResilienceHandler(StandardIdentifier, (builder, context) => + { + context.EnableReloads(optionsName); - _ = builder.Services.AddValidatedOptions(builder.PipelineName); + var monitor = context.ServiceProvider.GetRequiredService>(); + var options = monitor.Get(optionsName); - return new HttpResiliencePipelineBuilder(resilienceBuilder); + _ = builder + .AddRateLimiter(options.RateLimiterOptions) + .AddTimeout(options.TotalRequestTimeoutOptions) + .AddRetry(options.RetryOptions) + .AddAdvancedCircuitBreaker(options.CircuitBreakerOptions) + .AddTimeout(options.AttemptTimeoutOptions); + }); + + return new HttpStandardResilienceStrategyBuilder(optionsName, builder.Services); } + + private record HttpStandardResilienceStrategyBuilder(string StrategyName, IServiceCollection Services) : IHttpStandardResilienceStrategyBuilder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpRequestMessageExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpRequestMessageExtensions.cs new file mode 100644 index 00000000000..ee03d1f7199 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpRequestMessageExtensions.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// The resilience extensions for . +/// +public static class HttpRequestMessageExtensions +{ +#if NET8_0_OR_GREATER + private static readonly HttpRequestOptionsKey _resilienceContextKey = new("Resilience.Http.ResilienceContext"); +#else + private const string ResilienceContextKey = "Resilience.Http.ResilienceContext"; +#endif + + /// + /// Gets the from the request message. + /// + /// The request. + /// An instance of or . + public static ResilienceContext? GetResilienceContext(this HttpRequestMessage requestMessage) + { + _ = Throw.IfNull(requestMessage); + +#if NET8_0_OR_GREATER + if (requestMessage.Options.TryGetValue(_resilienceContextKey, out var context)) + { + return context; + } +#else + if (requestMessage.Properties.TryGetValue(ResilienceContextKey, out var contextRaw) && contextRaw is ResilienceContext context) + { + return context; + } +#endif + + return null; + } + + /// + /// Sets the on the request message. + /// + /// The request. + /// An instance of . + public static void SetResilienceContext(this HttpRequestMessage requestMessage, ResilienceContext? resilienceContext) + { + _ = Throw.IfNull(requestMessage); +#if NET8_0_OR_GREATER + requestMessage.Options.Set(_resilienceContextKey, resilienceContext); +#else + requestMessage.Properties[ResilienceContextKey] = resilienceContext; +#endif + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResilienceStrategyBuilderExtensions.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResilienceStrategyBuilderExtensions.cs index b03721c219f..e92c000c81a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResiliencePipelineBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpResilienceStrategyBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net.Http; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Http.Resilience.Internal; @@ -10,39 +11,39 @@ namespace Microsoft.Extensions.Http.Resilience; /// -/// Extensions for . +/// Extensions for . /// -public static class HttpResiliencePipelineBuilderExtensions +public static class HttpResilienceStrategyBuilderExtensions { /// - /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// Instructs the underlying builder to select the strategy instance by redacted authority (scheme + host + port). /// /// The builder instance. /// The data class associated with the authority. /// The same builder instance. /// The authority is redacted using retrieved for . - public static IHttpResiliencePipelineBuilder SelectPipelineByAuthority(this IHttpResiliencePipelineBuilder builder, DataClassification classification) + public static IHttpResilienceStrategyBuilder SelectStrategyByAuthority(this IHttpResilienceStrategyBuilder builder, DataClassification classification) { _ = Throw.IfNull(builder); - PipelineKeyProviderHelper.SelectPipelineByAuthority(builder.Services, builder.PipelineName, classification); + StrategyKeyProviderHelper.SelectStrategyByAuthority(builder.Services, builder.StrategyName, classification); return builder; } /// - /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// Instructs the underlying builder to select the strategy instance by custom selector. /// /// The builder instance. - /// The factory that returns selector. + /// The factory that returns a key selector. /// The same builder instance. - /// The pipeline key is used in metrics and logs, do not return any sensitive value. - public static IHttpResiliencePipelineBuilder SelectPipelineBy(this IHttpResiliencePipelineBuilder builder, Func selectorFactory) + /// The strategy key is used in metrics and logs, do not return any sensitive value. + public static IHttpResilienceStrategyBuilder SelectStrategyBy(this IHttpResilienceStrategyBuilder builder, Func> selectorFactory) { _ = Throw.IfNull(builder); _ = Throw.IfNull(selectorFactory); - PipelineKeyProviderHelper.SelectPipelineBy(builder.Services, builder.PipelineName, selectorFactory); + StrategyKeyProviderHelper.SelectStrategyBy(builder.Services, builder.StrategyName, selectorFactory); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceBuilderBuilderExtensions.cs similarity index 63% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceBuilderBuilderExtensions.cs index 25e15a6dd27..ce8acf3c861 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResiliencePipelineBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceBuilderBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Net.Http; using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Configuration; @@ -13,25 +14,25 @@ namespace Microsoft.Extensions.Http.Resilience; /// -/// Extensions for . +/// Extensions for . /// -public static class HttpStandardResiliencePipelineBuilderExtensions +public static class HttpStandardResilienceBuilderBuilderExtensions { /// - /// Configures the for the standard pipeline. + /// Configures the for the standard resilience strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The section that the options will bind against. /// The same builder instance. [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HttpStandardResilienceOptions))] - public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, IConfigurationSection section) + public static IHttpStandardResilienceStrategyBuilder Configure(this IHttpStandardResilienceStrategyBuilder builder, IConfigurationSection section) { _ = Throw.IfNull(builder); _ = Throw.IfNull(section); var options = Throw.IfNull(section.Get()); _ = builder.Services.Configure( - builder.PipelineName, + builder.StrategyName, section, #if NET6_0_OR_GREATER o => o.ErrorOnUnknownConfiguration = true); @@ -42,13 +43,13 @@ public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandar } /// - /// Configures the for the standard pipeline. + /// Configures the for the standard resilience strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The configure method. /// The same builder instance. #pragma warning disable S3872 // Parameter names should not duplicate the names of their methods - public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, Action configure) + public static IHttpStandardResilienceStrategyBuilder Configure(this IHttpStandardResilienceStrategyBuilder builder, Action configure) #pragma warning restore S3872 // Parameter names should not duplicate the names of their methods { _ = Throw.IfNull(builder); @@ -58,53 +59,53 @@ public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandar } /// - /// Configures the for the standard pipeline. + /// Configures the for the standard resilience strategy. /// - /// The pipeline builder. + /// The strategy builder. /// The configure method. /// The same builder instance. #pragma warning disable S3872 // Parameter names should not duplicate the names of their methods [Experimental] - public static IHttpStandardResiliencePipelineBuilder Configure(this IHttpStandardResiliencePipelineBuilder builder, Action configure) + public static IHttpStandardResilienceStrategyBuilder Configure(this IHttpStandardResilienceStrategyBuilder builder, Action configure) #pragma warning restore S3872 // Parameter names should not duplicate the names of their methods { _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - _ = builder.Services.AddOptions(builder.PipelineName).Configure(configure); + _ = builder.Services.AddOptions(builder.StrategyName).Configure(configure); return builder; } /// - /// Instructs the underlying pipeline builder to select the pipeline instance by redacted authority (scheme + host + port). + /// Instructs the underlying builder to select the strategy instance by redacted authority (scheme + host + port). /// - /// The builder instance. + /// The strategy builder. /// The data class associated with the authority. /// The same builder instance. /// The authority is redacted using retrieved for . - public static IHttpStandardResiliencePipelineBuilder SelectPipelineByAuthority(this IHttpStandardResiliencePipelineBuilder builder, DataClassification classification) + public static IHttpStandardResilienceStrategyBuilder SelectStrategyByAuthority(this IHttpStandardResilienceStrategyBuilder builder, DataClassification classification) { _ = Throw.IfNull(builder); - PipelineKeyProviderHelper.SelectPipelineByAuthority(builder.Services, builder.PipelineName, classification); + StrategyKeyProviderHelper.SelectStrategyByAuthority(builder.Services, builder.StrategyName, classification); return builder; } /// - /// Instructs the underlying pipeline builder to select the pipeline instance by custom selector. + /// Instructs the underlying builder to select the strategy instance by custom selector. /// - /// The builder instance. - /// The factory that returns selector. + /// The strategy builder. + /// The factory that returns a key selector. /// The same builder instance. /// The pipeline key is used in metrics and logs, do not return any sensitive value. - public static IHttpStandardResiliencePipelineBuilder SelectPipelineBy(this IHttpStandardResiliencePipelineBuilder builder, Func selectorFactory) + public static IHttpStandardResilienceStrategyBuilder SelectStrategyBy(this IHttpStandardResilienceStrategyBuilder builder, Func> selectorFactory) { _ = Throw.IfNull(builder); _ = Throw.IfNull(selectorFactory); - PipelineKeyProviderHelper.SelectPipelineBy(builder.Services, builder.PipelineName, selectorFactory); + StrategyKeyProviderHelper.SelectStrategyBy(builder.Services, builder.StrategyName, selectorFactory); return builder; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs index 968ae1a39d4..b3c58506e13 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpStandardResilienceOptions.cs @@ -3,79 +3,95 @@ using System; using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.Options.Validation; -using Microsoft.Extensions.Resilience.Options; +using Polly.Timeout; namespace Microsoft.Extensions.Http.Resilience; /// -/// Options for resilient pipeline of policies for usage in HTTP scenarios. It is using five chained layers in this order (from the outermost to the innermost): -/// Bulkhead -> Total Request Timeout -> Retry -> Circuit Breaker -> Attempt Timeout. +/// Options for resilience strategies for usage in HTTP scenarios. /// -/// /// -/// The configuration of each policy is initialized with the default options per type. The request goes through these policies: -/// 1. Total request timeout policy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. -/// 2. The retry policy retries the request in case the dependency is slow or returns a transient error. -/// 3. The bulkhead policy limits the maximum number of concurrent requests being send to the dependency. -/// 4. The circuit breaker blocks the execution if too many direct failures or timeouts are detected. -/// 5. The attempt timeout policy limits each request attempt duration and throws if its exceeded. +/// +/// These options represent configuration for five chained resilience strategies in this order (from the outermost to the innermost): +/// +/// Bulkhead -> Total Request Timeout -> Retry -> Circuit Breaker -> Attempt Timeout. +/// +/// The configuration of each Strategy is initialized with the default options per type. The request goes through these strategies: +/// +/// Total request timeout Strategy applies an overall timeout to the execution, ensuring that the request including hedging attempts does not exceed the configured limit. +/// The retry Strategy retries the request in case the dependency is slow or returns a transient error. +/// The bulkhead Strategy limits the maximum number of concurrent requests being send to the dependency. +/// The circuit breaker blocks the execution if too many direct failures or timeouts are detected. +/// The attempt timeout Strategy limits each request attempt duration and throws if its exceeded. +/// /// public class HttpStandardResilienceOptions { - private static readonly TimeSpan _attemptTimeoutInterval = TimeSpan.FromSeconds(10); - /// /// Gets or sets the bulkhead options. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpBulkheadPolicyOptions BulkheadOptions { get; set; } = new(); + public HttpRateLimiterStrategyOptions RateLimiterOptions { get; set; } = new HttpRateLimiterStrategyOptions + { + StrategyName = StandardStrategyNames.RateLimiter + }; /// - /// Gets or sets the timeout policy options for the total timeout applied on the request's execution. + /// Gets or sets the timeout Strategy options for the total timeout applied on the request's execution. /// /// - /// By default it is initialized with a unique instance of - /// using default properties values. + /// By default it is initialized with a unique instance of . /// [Required] [ValidateObjectMembers] - public HttpTimeoutPolicyOptions TotalRequestTimeoutOptions { get; set; } = new(); + public HttpTimeoutStrategyOptions TotalRequestTimeoutOptions { get; set; } = new HttpTimeoutStrategyOptions + { + StrategyName = StandardStrategyNames.TotalRequestTimeout + }; /// - /// Gets or sets the retry policy Options. + /// Gets or sets the retry Strategy Options. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpRetryPolicyOptions RetryOptions { get; set; } = new(); + public HttpRetryStrategyOptions RetryOptions { get; set; } = new HttpRetryStrategyOptions + { + StrategyName = StandardStrategyNames.Retry + }; /// /// Gets or sets the circuit breaker options. /// /// - /// By default it is initialized with a unique instance of using default properties values. + /// By default it is initialized with a unique instance of using default properties values. /// [Required] [ValidateObjectMembers] - public HttpCircuitBreakerPolicyOptions CircuitBreakerOptions { get; set; } = new(); + public HttpCircuitBreakerStrategyOptions CircuitBreakerOptions { get; set; } = new HttpCircuitBreakerStrategyOptions + { + StrategyName = StandardStrategyNames.CircuitBreaker + }; /// - /// Gets or sets the options for the timeout policy applied per each request attempt. + /// Gets or sets the options for the timeout Strategy applied per each request attempt. /// /// - /// By default it is initialized with a unique instance of - /// using custom of 10 seconds. + /// By default it is initialized with a unique instance of + /// using custom of 10 seconds. /// [Required] [ValidateObjectMembers] - public HttpTimeoutPolicyOptions AttemptTimeoutOptions { get; set; } = new() + public HttpTimeoutStrategyOptions AttemptTimeoutOptions { get; set; } = new() { - TimeoutInterval = _attemptTimeoutInterval, + Timeout = TimeSpan.FromSeconds(10), + StrategyName = StandardStrategyNames.AttemptTimeout }; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs deleted file mode 100644 index 73de8b166ae..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResiliencePipelineBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using Microsoft.Extensions.Resilience; - -namespace Microsoft.Extensions.Http.Resilience; - -/// -/// The builder for configuring the HTTP client resilience pipeline. -/// -#pragma warning disable S4023 // Interfaces should not be empty -public interface IHttpResiliencePipelineBuilder : IResiliencePipelineBuilder -#pragma warning restore S4023 // Interfaces should not be empty -{ -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResilienceStrategyBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResilienceStrategyBuilder.cs new file mode 100644 index 00000000000..3684400b0ba --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpResilienceStrategyBuilder.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// The builder for configuring the HTTP client resilience strategy. +/// +public interface IHttpResilienceStrategyBuilder +{ + /// + /// Gets the name of the resilience strategy configured by this builder. + /// + string StrategyName { get; } + + /// + /// Gets the application service collection. + /// + IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResilienceStrategyBuilder.cs similarity index 66% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResilienceStrategyBuilder.cs index dc4ad8807cb..3a924f9d620 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResiliencePipelineBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/IHttpStandardResilienceStrategyBuilder.cs @@ -6,14 +6,14 @@ namespace Microsoft.Extensions.Http.Resilience; /// -/// The builder for the standard HTTP pipeline. +/// The builder for the standard HTTP resilience strategy. /// -public interface IHttpStandardResiliencePipelineBuilder +public interface IHttpStandardResilienceStrategyBuilder { /// - /// Gets the name of the pipeline configured by this builder. + /// Gets the name of the resilience strategy configured by this builder. /// - string PipelineName { get; } + string StrategyName { get; } /// /// Gets the application service collection. diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs similarity index 59% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs index e9f7539506b..4ddf7af5d90 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityPipelineKeyProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs @@ -4,46 +4,45 @@ using System; using System.Collections.Concurrent; using System.Net.Http; -using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.Http.Resilience.Internal; -internal sealed class ByAuthorityPipelineKeyProvider : IPipelineKeyProvider +internal sealed class ByAuthorityStrategyKeyProvider : IStrategyKeyProvider { private readonly Redactor _redactor; private readonly ConcurrentDictionary<(string scheme, string host, int port), string> _cache = new(); - public ByAuthorityPipelineKeyProvider(Redactor redactor, DataClassification _) + public ByAuthorityStrategyKeyProvider(Redactor redactor) { _redactor = redactor; } - public string GetPipelineKey(HttpRequestMessage requestMessage) + public string GetStrategyKey(HttpRequestMessage requestMessage) { var url = requestMessage.RequestUri ?? throw new InvalidOperationException("The request message must have a URL specified."); var key = (url.Scheme, url.Host, url.Port); // We could use GetOrAdd for simplification but that would force us to allocate the lambda for every call. - if (_cache.TryGetValue(key, out var pipelineKey)) + if (_cache.TryGetValue(key, out var strategyKey)) { - return pipelineKey; + return strategyKey; } - pipelineKey = url.GetLeftPart(UriPartial.Authority); - pipelineKey = _redactor.Redact(pipelineKey); + strategyKey = url.GetLeftPart(UriPartial.Authority); + strategyKey = _redactor.Redact(strategyKey); - if (string.IsNullOrEmpty(pipelineKey)) + if (string.IsNullOrEmpty(strategyKey)) { Throw.InvalidOperationException( - $"The redacted pipeline is an empty string and cannot be used for pipeline selection. Is redaction correctly configured?"); + "The redacted strategy key is an empty string and cannot be used for the strategy selection. Is redaction correctly configured?"); } // sometimes this can be called twice (multiple concurrent requests), but we don't care - _cache[key] = pipelineKey!; + _cache[key] = strategyKey!; - return pipelineKey!; + return strategyKey!; } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs deleted file mode 100644 index 3e17755ab01..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorPipelineKeyProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal sealed class ByCustomSelectorPipelineKeyProvider : IPipelineKeyProvider -{ - private readonly PipelineKeySelector _selector; - - public ByCustomSelectorPipelineKeyProvider(PipelineKeySelector selector) - { - _selector = selector; - } - - public string GetPipelineKey(HttpRequestMessage requestMessage) - { - return _selector(requestMessage); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs new file mode 100644 index 00000000000..be2dea427f9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class ByCustomSelectorStrategyKeyProvider : IStrategyKeyProvider +{ + private readonly Func _selector; + + public ByCustomSelectorStrategyKeyProvider(Func selector) + { + _selector = selector; + } + + public string GetStrategyKey(HttpRequestMessage requestMessage) => _selector(requestMessage); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs deleted file mode 100644 index 678463a1c90..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ContextExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using Microsoft.Extensions.Http.Telemetry; -using Microsoft.Extensions.Telemetry; -using Microsoft.Shared.Diagnostics; -using Polly; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Various extensions for . -/// -internal static class ContextExtensions -{ - private const string RequestMessageKey = "Resilience.ContextExtensions.Request"; - - private const string MessageInvokerKey = "Resilience.ContextExtensions.MessageInvoker"; - - /// - /// Sets the request metadata to the context. - /// - /// The context. - /// The request. - public static void SetRequestMetadata(this Context context, HttpRequestMessage request) - { - _ = Throw.IfNull(request); - _ = Throw.IfNull(context); - - if (!context.ContainsKey(TelemetryConstants.RequestMetadataKey) && request.GetRequestMetadata() is RequestMetadata requestMetadata) - { - context[TelemetryConstants.RequestMetadataKey] = requestMetadata; - } - } - - /// - /// Gets the assigned to the context. - /// - /// A . - public static Func CreateMessageInvokerProvider(string pipelineName) - { - _ = Throw.IfNullOrEmpty(pipelineName); - - var key = $"{MessageInvokerKey}-{pipelineName}"; - - return (context) => - { - if (context.TryGetValue(key, out var val)) - { - return ((Lazy)val).Value; - } - - return null; - }; - } - - /// - /// Gets the assigned to the context. - /// - /// A . - public static Func CreateRequestMessageProvider(string pipelineName) - { - _ = Throw.IfNullOrEmpty(pipelineName); - - var key = $"{RequestMessageKey}-{pipelineName}"; - - return (context) => - { - if (context.TryGetValue(key, out var val)) - { - return (HttpRequestMessage)val; - } - - return null; - }; - } - - internal static Action> CreateMessageInvokerSetter(string pipelineName) - { - var key = $"{MessageInvokerKey}-{pipelineName}"; - - return (context, invoker) => context[key] = invoker; - } - - internal static Action CreateRequestMessageSetter(string pipelineName) - { - var key = $"{RequestMessageKey}-{pipelineName}"; - - return (context, invoker) => context[key] = invoker; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs deleted file mode 100644 index 273bbba1783..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpResiliencePipelineBuilder.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Resilience; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal sealed class HttpResiliencePipelineBuilder : IHttpResiliencePipelineBuilder -{ - public HttpResiliencePipelineBuilder(IResiliencePipelineBuilder builder) - { - PipelineName = builder.PipelineName; - Services = builder.Services; - } - - public string PipelineName { get; } - - public IServiceCollection Services { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs deleted file mode 100644 index 04b4a9e2f24..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/HttpStandardResiliencePipelineBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal sealed class HttpStandardResiliencePipelineBuilder : IHttpStandardResiliencePipelineBuilder -{ - public HttpStandardResiliencePipelineBuilder(IHttpResiliencePipelineBuilder builder) - { - PipelineName = builder.PipelineName; - Services = builder.Services; - } - - public string PipelineName { get; } - - public IServiceCollection Services { get; } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs similarity index 57% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs index 8fd9414c19f..fce2abc4f7a 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IPipelineKeyProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs @@ -6,14 +6,14 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// -/// The provider that returns the pipeline key from the request message. +/// The provider that returns the strategy key from the request message. /// -internal interface IPipelineKeyProvider +internal interface IStrategyKeyProvider { /// - /// Returns the pipeline key from the request message. + /// Returns the strategy key from the request message. /// /// The request message. - /// The pipeline key. - string GetPipelineKey(HttpRequestMessage requestMessage); + /// The strategy key. + string GetStrategyKey(HttpRequestMessage requestMessage); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs deleted file mode 100644 index e27aa83efd6..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineKeyProviderHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Extensions.Compliance.Classification; -using Microsoft.Extensions.Compliance.Redaction; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal static class PipelineKeyProviderHelper -{ - public static void SelectPipelineByAuthority(IServiceCollection services, string pipelineName, DataClassification classification) - { - UsePipelineKeyProvider(services, pipelineName, serviceProvider => - { - var redactor = serviceProvider.GetRequiredService().GetRedactor(classification); - - return ActivatorUtilities.CreateInstance(serviceProvider, redactor, classification); - }); - } - - public static void SelectPipelineBy(IServiceCollection services, string pipelineName, Func selectorFactory) - { - UsePipelineKeyProvider(services, pipelineName, serviceProvider => - { - var selector = selectorFactory(serviceProvider); - - return ActivatorUtilities.CreateInstance(serviceProvider, selector); - }); - } - - public static IPipelineKeyProvider? GetPipelineKeyProvider(this IServiceProvider provider, string pipelineName) - { - return provider.GetService>()?.GetService(pipelineName); - } - - private static void UsePipelineKeyProvider(IServiceCollection services, string pipelineName, Func factory) - { - _ = services.AddNamedSingleton(pipelineName, factory); - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs index 513a72d7641..af4c211c8df 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ResilienceHandler.cs @@ -5,54 +5,86 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry; using Polly; namespace Microsoft.Extensions.Http.Resilience.Internal; /// -/// Base class for resilience handler, i.e. handlers that use resilience pipelines to send the requests. +/// Base class for resilience handler, i.e. handlers that use resilience strategies to send the requests. /// -internal sealed class ResilienceHandler : PolicyHttpMessageHandler +internal sealed class ResilienceHandler : DelegatingHandler { - private readonly Lazy _invoker; - private readonly Action> _invokerSetter; - private readonly Action _requestSetter; + private readonly Func> _strategyProvider; - public ResilienceHandler(string pipelineName, Func> policySelector) - : base(policySelector) + public ResilienceHandler(Func> strategyProvider) { - // Stryker disable once boolean : no means to test this - _invoker = new Lazy(() => new HttpMessageInvoker(InnerHandler!), true); - _invokerSetter = ContextExtensions.CreateMessageInvokerSetter(pipelineName); - _requestSetter = ContextExtensions.CreateRequestMessageSetter(pipelineName); + _strategyProvider = strategyProvider; } /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + var strategy = _strategyProvider(request); var created = false; - if (request.GetPolicyExecutionContext() is not Context context) + if (request.GetResilienceContext() is not ResilienceContext context) { - context = new Context(); - request.SetPolicyExecutionContext(context); + context = ResilienceContext.Get(); created = true; + request.SetResilienceContext(context); } - // set common properties to the context - context.SetRequestMetadata(request); - _invokerSetter(context, _invoker); - _requestSetter(context, request); + if (request.GetRequestMetadata() is RequestMetadata requestMetadata) + { + context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata); + } + + context.Properties.Set(ResilienceKeys.RequestMessage, request); + var previousToken = context.CancellationToken; + context.CancellationToken = cancellationToken; try { - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var outcome = await strategy.ExecuteOutcomeAsync( + static async (context, state) => + { + var request = context.Properties.GetValue(ResilienceKeys.RequestMessage, state.request); + + try + { + var response = await state.instance.SendCoreAsync(request, context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + return Outcome.FromResult(response); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) + { + return Outcome.FromException(e); + } +#pragma warning restore CA1031 // Do not catch general exception types + }, + context, + (instance: this, request)) + .ConfigureAwait(context.ContinueOnCapturedContext); + + outcome.EnsureSuccess(); + + return outcome.Result!; } finally { if (created) { - request.SetPolicyExecutionContext(null); + ResilienceContext.Return(context); + request.SetResilienceContext(null); + } + else + { + context.CancellationToken = previousToken; } } } + + private Task SendCoreAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken) + => base.SendAsync(requestMessage, cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs deleted file mode 100644 index 1000cbb2950..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Resilience based extensions for . -/// -internal static class ServiceCollectionExtensions -{ - /// - /// Adds a implementation of to services. - /// - /// The services. - /// The same services instances. - public static IServiceCollection AddRequestCloner(this IServiceCollection services) - { - services.TryAddSingleton(); - return services; - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardStrategyNames.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardStrategyNames.cs index 1f6b0707a1a..21e6949398f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardPolicyNames.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StandardStrategyNames.cs @@ -3,11 +3,11 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; -internal static class StandardPolicyNames +internal static class StandardStrategyNames { public const string CircuitBreaker = "Standard-CircuitBreaker"; - public const string Bulkhead = "Standard-Bulkhead"; + public const string RateLimiter = "Standard-RateLimiter"; public const string Retry = "Standard-Retry"; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs new file mode 100644 index 00000000000..0ed8b754186 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal static class StrategyKeyProviderHelper +{ + public static void SelectStrategyByAuthority(IServiceCollection services, string strategyName, DataClassification classification) + { + UseStrategyKeyProvider(services, strategyName, serviceProvider => + { + var redactor = serviceProvider.GetRequiredService().GetRedactor(classification); + + return new ByAuthorityStrategyKeyProvider(redactor); + }); + } + + public static void SelectStrategyBy(IServiceCollection services, string strategyName, Func> selectorFactory) + { + UseStrategyKeyProvider(services, strategyName, serviceProvider => + { + return new ByCustomSelectorStrategyKeyProvider(selectorFactory(serviceProvider)); + }); + } + + public static IStrategyKeyProvider? GetStrategyKeyProvider(this IServiceProvider provider, string strategyName) + { + return provider.GetService>()?.GetService(strategyName); + } + + private static void UseStrategyKeyProvider(IServiceCollection services, string strategyName, Func factory) + { + _ = services.AddNamedSingleton(strategyName, factory); + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyNameHelper.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyNameHelper.cs index 8ca87a434c1..d7592f834d5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/PipelineNameHelper.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyNameHelper.cs @@ -3,10 +3,7 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; -internal static class PipelineNameHelper +internal static class StrategyNameHelper { - public static string GetPipelineName(string httpClientName, string pipelineIdentifier) - { - return $"{httpClientName}-{pipelineIdentifier}"; - } + public static string GetName(string httpClientName, string strategyIdentifier) => $"{httpClientName}-{strategyIdentifier}"; } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs deleted file mode 100644 index c928ce00522..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpCircuitBreakerPolicyOptionsValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Options.Validation; - -namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; - -[OptionsValidator] -internal sealed partial class HttpCircuitBreakerPolicyOptionsValidator : IValidateOptions -{ -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs deleted file mode 100644 index d4bed96dcdb..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpRetryPolicyOptionsValidator.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Options.Validation; - -namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; - -[OptionsValidator] -internal sealed partial class HttpRetryPolicyOptionsValidator : IValidateOptions -{ -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs index f88c189cefa..c5ef4af3cef 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/HttpStandardResilienceOptionsCustomValidator.cs @@ -3,7 +3,7 @@ using System; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Resilience.Options; +using Polly.Retry; namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; @@ -15,30 +15,30 @@ public ValidateOptionsResult Validate(string? name, HttpStandardResilienceOption { var builder = new ValidateOptionsResultBuilder(); - if (options.AttemptTimeoutOptions.TimeoutInterval > options.TotalRequestTimeoutOptions.TimeoutInterval) + if (options.AttemptTimeoutOptions.Timeout > options.TotalRequestTimeoutOptions.Timeout) { - builder.AddError($"Total request timeout policy must have a greater timeout than the attempt timeout policy. " + - $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s, " + - $"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + builder.AddError($"Total request timeout resilience strategy must have a greater timeout than the attempt resilience strategy. " + + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.Timeout.TotalSeconds}s, " + + $"Attempt Timeout: {options.AttemptTimeoutOptions.Timeout.TotalSeconds}s"); } - if (options.CircuitBreakerOptions.SamplingDuration < TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * CircuitBreakerTimeoutMultiplier)) + if (options.CircuitBreakerOptions.SamplingDuration < TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.Timeout.TotalMilliseconds * CircuitBreakerTimeoutMultiplier)) { - builder.AddError("The sampling duration of circuit breaker policy needs to be at least double of " + - $"an attempt timeout policy’s timeout interval, in order to be effective. " + + builder.AddError("The sampling duration of circuit breaker strategy needs to be at least double of " + + $"an attempt timeout strategy’s timeout interval, in order to be effective. " + $"Sampling Duration: {options.CircuitBreakerOptions.SamplingDuration.TotalSeconds}s," + - $"Attempt Timeout: {options.AttemptTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + $"Attempt Timeout: {options.AttemptTimeoutOptions.Timeout.TotalSeconds}s"); } - if (options.RetryOptions.RetryCount != RetryPolicyOptions.InfiniteRetry) + if (options.RetryOptions.RetryCount > 0) { - TimeSpan retrySum = options.RetryOptions.GetRetryPolicyDelaySum(); + TimeSpan retrySum = ValidationHelper.GetAggregatedDelay(options.RetryOptions); - if (retrySum > options.TotalRequestTimeoutOptions.TimeoutInterval) + if (retrySum > options.TotalRequestTimeoutOptions.Timeout) { - builder.AddError($"The cumulative delay of the retry policy cannot be larger than total request timeout policy interval. " + + builder.AddError($"The cumulative delay of the retry strategy cannot be larger than total request timeout policy interval. " + $"Cumulative Delay: {retrySum.TotalSeconds}s," + - $"Total Request Timeout: {options.TotalRequestTimeoutOptions.TimeoutInterval.TotalSeconds}s"); + $"Total Request Timeout: {options.TotalRequestTimeoutOptions.Timeout.TotalSeconds}s"); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs deleted file mode 100644 index 2ea2bd439e0..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/Validators/ValidationHelper.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Linq; -using Microsoft.Extensions.Resilience; -using Microsoft.Extensions.Resilience.Options; - -namespace Microsoft.Extensions.Http.Resilience.Internal.Validators; - -internal static class ValidationHelper -{ - public static TimeSpan GetRetryPolicyDelaySum(this RetryPolicyOptions retryPolicyOptions) - { - return retryPolicyOptions.BackoffType == BackoffType.ExponentialWithJitter ? - retryPolicyOptions.GetExponentialWithJitterDeterministicDelay() : - retryPolicyOptions.GetDelays().Aggregate((accumulated, current) => accumulated + current); - } - - /// - /// Calculates the upper-bound cumulated delay of the given retry policy options by using the algorithm defined in - /// https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/blob/master/src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs, - /// with the randomized jitter factor (a value between 0 and 1) replaced with 1. - /// - /// The retry policy options. - /// The calculated upper-bound cumulated delay of the retry policy. - public static TimeSpan GetExponentialWithJitterDeterministicDelay(this RetryPolicyOptions options) - { - var totalDelay = TimeSpan.Zero; - - const double Factor = 4.0; - const double ScalingFactor = 1 / 1.4d; - var maxTimeSpanDouble = TimeSpan.MaxValue.Ticks - 1000; - var targetTicksFirstDelay = options.BaseDelay.Ticks; - - var prev = 0.0; - for (int i = 0; EvaluateRetry(i, options.RetryCount); i++) - { - var t = i + 1.0; - var next = Math.Pow(2, t) * Math.Tanh(Math.Sqrt(Factor * t)); - - var formulaIntrinsicValue = next - prev; - var diff = (long)Math.Min(formulaIntrinsicValue * ScalingFactor * targetTicksFirstDelay, maxTimeSpanDouble); - - try - { - totalDelay += TimeSpan.FromTicks(diff); - } - catch (OverflowException) - { - return TimeSpan.FromTicks(maxTimeSpanDouble); - } - - prev = next; - } - - return totalDelay; - } - - private static bool EvaluateRetry(int retry, int maxRetryCount) => retry < maxRetryCount || maxRetryCount == RetryPolicyOptions.InfiniteRetry; -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs deleted file mode 100644 index 7e3f5997aa2..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/PipelineKeySelector.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; - -namespace Microsoft.Extensions.Http.Resilience; - -/// -/// A function that returns a pipeline key extracted from the request message. -/// -/// The request message that the pipeline key is extracted from. -/// A pipeline key. -/// -/// The pipeline key is used by metrics and telemetry. Make sure it does not contain any sensitive data. -/// -public delegate string PipelineKeySelector(HttpRequestMessage requestMessage); - diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandlerContext.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandlerContext.cs new file mode 100644 index 00000000000..6a6cd420edc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandlerContext.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Http.Resilience.Internal; +using Polly.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http.Resilience; + +/// +/// The context used when building a resilience strategy HTTP handler. +/// +public sealed class ResilienceHandlerContext +{ + private readonly AddResilienceStrategyContext _context; + + internal ResilienceHandlerContext(AddResilienceStrategyContext context) + { + _context = context; + } + + /// + /// Gets the service provider. + /// + public IServiceProvider ServiceProvider => _context.ServiceProvider; + + /// + /// Gets the name of the builder being built. + /// + public string BuilderName => _context.StrategyKey.Name; + + /// + /// Gets the strategy key of the resilience strategy being built. + /// + public string StrategyKey => _context.StrategyKey.Key; + + /// + /// Enables dynamic reloading of the resilience strategy whenever the options are changed. + /// + /// The options type to listen to. + /// The named options, if any. + /// + /// You can decide based on the to listen for changes in global options or named options. + /// If is then the global options are listened to. + /// + /// You can listen for changes only for single options. If you call this method multiple times, the preceding calls are ignored and only the last one wins. + /// + /// + public void EnableReloads(string? name = null) => _context.EnableReloads(name); + + internal T GetOptions(string name) => _context.GetOptions(name); +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs index b40c4a6c2fd..e086c6c5416 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.Http.Resilience; @@ -11,14 +10,12 @@ namespace Microsoft.Extensions.Http.Resilience; /// Defines a strategy for retrieval of route URLs, /// used to route one request across a set of different endpoints. /// -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IRequestRoutingStrategy +internal interface IRequestRoutingStrategy { /// /// Gets the next route Uri. /// /// Holds next route value, or . /// if next route available, otherwise. - [EditorBrowsable(EditorBrowsableState.Never)] bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs index 26212244279..21951240a07 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs @@ -1,20 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; - namespace Microsoft.Extensions.Http.Resilience; /// /// Defines a factory for creation of request routing strategies. /// -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IRequestRoutingStrategyFactory +internal interface IRequestRoutingStrategyFactory { /// /// Creates a new instance of . /// /// The RequestRoutingStrategy for providing the routes. - [EditorBrowsable(EditorBrowsableState.Never)] IRequestRoutingStrategy CreateRoutingStrategy(); + + /// + /// Returns the strategy instance to the pool. + /// + /// The strategy instance. + void ReturnRoutingStrategy(IRequestRoutingStrategy strategy); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/DefaultRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/DefaultRoutingStrategyFactory.cs deleted file mode 100644 index 72a490c9b9d..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/DefaultRoutingStrategyFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; - -internal sealed class DefaultRoutingStrategyFactory : IRequestRoutingStrategyFactory - where TRoutingStrategy : IRequestRoutingStrategy -{ - private readonly string _clientId; - private readonly IServiceProvider _serviceProvider; - private readonly ObjectFactory _factory; - - public DefaultRoutingStrategyFactory(string clientId, IServiceProvider serviceProvider) - { - _clientId = clientId; - _serviceProvider = serviceProvider; - _factory = ActivatorUtilities.CreateFactory(typeof(TRoutingStrategy), new[] { typeof(string) }); - } - - public IRequestRoutingStrategy CreateRoutingStrategy() => (IRequestRoutingStrategy)_factory(_serviceProvider, new object[] { _clientId }); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/IPooledRequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/IPooledRequestRoutingStrategyFactory.cs deleted file mode 100644 index a67a535824b..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/IPooledRequestRoutingStrategyFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; - -/// -internal interface IPooledRequestRoutingStrategyFactory : IRequestRoutingStrategyFactory -{ - /// - /// Returns the strategy instance to the pool. - /// - /// The strategy instance. - void ReturnRoutingStrategy(IRequestRoutingStrategy strategy); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs index aba0d0eb9a9..ac19625a694 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.OrderedGroups; -internal sealed class OrderedGroupsRoutingStrategyFactory : PooledRoutingStrategyFactory +internal sealed class OrderedGroupsRoutingStrategyFactory : RequestRoutingStrategyFactory { public OrderedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) : base(clientId, pool, optionsMonitor) diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/PooledRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs similarity index 83% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/PooledRoutingStrategyFactory.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs index a061383ba31..634d06ee69f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/PooledRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs @@ -7,13 +7,13 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; -internal abstract class PooledRoutingStrategyFactory : IPooledRequestRoutingStrategyFactory +internal abstract class RequestRoutingStrategyFactory : IRequestRoutingStrategyFactory where T : class, IRequestRoutingStrategy, IResettable { private readonly ObjectPool _pool; private TOptions _options; - protected PooledRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) + protected RequestRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) { _pool = pool; _options = optionsMonitor.Get(clientId); diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingPolicy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingPolicy.cs deleted file mode 100644 index edd8e6d3dfe..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingPolicy.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Shared.Diagnostics; -using Polly; - -namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; - -/// -/// Adds routing support to an inner policy. -/// -internal sealed class RoutingPolicy : AsyncPolicy -{ - private readonly IRequestRoutingStrategyFactory _factory; - private readonly Func _requestProvider; - private readonly Action _routingStrategySetter; - - public RoutingPolicy(string pipelineName, IRequestRoutingStrategyFactory factory) - { - _factory = factory; - _requestProvider = ContextExtensions.CreateRequestMessageProvider(pipelineName); - _routingStrategySetter = HedgingContextExtensions.CreateRoutingStrategySetter(pipelineName); - } - - protected override async Task ImplementationAsync( - Func> action, - Context context, - CancellationToken cancellationToken, - bool continueOnCapturedContext) - { - var strategy = _factory.CreateRoutingStrategy(); - - // if there are not routes we cannot continue - if (!strategy.TryGetNextRoute(out var route)) - { - Throw.InvalidOperationException("The routing strategy did not provide any route URL on the first attempt."); - } - - _routingStrategySetter(context, strategy); - - var request = _requestProvider(context)!; - - // for primary request, use retrieved route - request.RequestUri = request.RequestUri!.ReplaceHost(route!); - - try - { - return await action(context, cancellationToken).ConfigureAwait(false); - } - finally - { - if (_factory is IPooledRequestRoutingStrategyFactory pooled) - { - pooled.ReturnRoutingStrategy(strategy); - } - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs new file mode 100644 index 00000000000..0a27be086f7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Shared.Diagnostics; +using Polly; + +namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; + +/// +/// Adds routing support to an inner strategy. +/// +internal sealed class RoutingResilienceStrategy : ResilienceStrategy +{ + private readonly IRequestRoutingStrategyFactory _factory; + + public RoutingResilienceStrategy(IRequestRoutingStrategyFactory factory) + { + _factory = factory; + } + + protected override async ValueTask> ExecuteCoreAsync( + Func>> callback, + ResilienceContext context, + TState state) + { + if (!context.Properties.TryGetValue(ResilienceKeys.RequestMessage, out var request)) + { + Throw.InvalidOperationException("The HTTP request message was not found in the resilience context."); + } + + var strategy = _factory.CreateRoutingStrategy(); + + // if there are not routes we cannot continue + if (!strategy.TryGetNextRoute(out var route)) + { + Throw.InvalidOperationException("The routing strategy did not provide any route URL on the first attempt."); + } + + context.Properties.Set(ResilienceKeys.RoutingStrategy, strategy); + + // for primary request, use retrieved route + request.RequestUri = request.RequestUri!.ReplaceHost(route!); + + try + { + return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + finally + { + _factory.ReturnRoutingStrategy(strategy); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingStrategyBuilder.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingStrategyBuilder.cs index d31491ec9f6..b01bf41d2a0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingStrategyBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingStrategyBuilder.cs @@ -5,15 +5,4 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; -internal sealed class RoutingStrategyBuilder : IRoutingStrategyBuilder -{ - public RoutingStrategyBuilder(string name, IServiceCollection services) - { - Name = name; - Services = services; - } - - public string Name { get; } - - public IServiceCollection Services { get; } -} +internal sealed record RoutingStrategyBuilder(string Name, IServiceCollection Services) : IRoutingStrategyBuilder; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs index a7b6cf06707..00fd018cad7 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.WeightedGroups; -internal sealed class WeightedGroupsRoutingStrategyFactory : PooledRoutingStrategyFactory +internal sealed class WeightedGroupsRoutingStrategyFactory : RequestRoutingStrategyFactory { public WeightedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) : base(clientId, pool, optionsMonitor) diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs deleted file mode 100644 index 6ba9e9b5a59..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/ConfigurationStubFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; - -namespace Microsoft.Extensions.Http.Resilience.Tests.Helpers; - -public sealed class ConfigurationStubFactory -{ - public static IConfiguration Create(Dictionary collection) - { - return new ConfigurationBuilder() - .AddInMemoryCollection(collection) - .Build(); - } - - public static IConfiguration CreateEmpty() - { - return new ConfigurationBuilder().Build(); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs deleted file mode 100644 index e27521c6412..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.BySelector.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; -using Microsoft.Extensions.Compliance.Classification; -using Microsoft.Extensions.Compliance.Redaction; -using Microsoft.Extensions.Compliance.Testing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Resilience; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test; - -public sealed partial class HttpClientBuilderExtensionsTests -{ - [InlineData(true, "https://dummy:21/path", "https://dummy:21")] - [InlineData(true, "https://dummy", "https://dummy")] - [InlineData(false, "https://dummy:21/path", "https://dummy:21")] - [InlineData(false, "https://dummy", "https://dummy")] - [Theory] - public void SelectPipelineByAuthority_Ok(bool standardResilience, string url, string expectedPipelineKey) - { - _builder.Services.AddFakeRedaction(); - - var pipelineName = standardResilience ? - _builder.AddStandardResilienceHandler().SelectPipelineByAuthority(DataClassification.Unknown).PipelineName : - _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(DataClassification.Unknown).AddRetryPolicy("test").PipelineName; - - var provider = _builder.Services.BuildServiceProvider().GetPipelineKeyProvider(pipelineName)!; - - using var request = new HttpRequestMessage(HttpMethod.Head, url); - - var key = provider.GetPipelineKey(request); - - Assert.Equal(expectedPipelineKey, key); - Assert.Same(provider.GetPipelineKey(request), provider.GetPipelineKey(request)); - } - - [Fact] - public void SelectPipelineByAuthority_Ok_NullURL_Throws() - { - _builder.Services.AddFakeRedaction(); - var builder = _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(DataClassification.Unknown).AddRetryPolicy("test"); - var provider = PipelineKeyProviderHelper.GetPipelineKeyProvider(builder.Services.BuildServiceProvider(), builder.PipelineName)!; - - using var request = new HttpRequestMessage(); - - Assert.Throws(() => provider.GetPipelineKey(request)); - } - - [Fact] - public void SelectPipelineByAuthority_ErasingRedactor_InvalidOperationException() - { - _builder.Services.AddRedaction(); - var builder = _builder.AddResilienceHandler("dummy").SelectPipelineByAuthority(SimpleClassifications.PrivateData).AddRetryPolicy("test"); - var provider = PipelineKeyProviderHelper.GetPipelineKeyProvider(builder.Services.BuildServiceProvider(), builder.PipelineName)!; - - using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); - - Assert.Throws(() => provider.GetPipelineKey(request)); - } - - [InlineData(true, "https://dummy:21/path", "https://")] - [InlineData(true, "https://dummy", "https://")] - [InlineData(false, "https://dummy:21/path", "https://")] - [InlineData(false, "https://dummy", "https://")] - [Theory] - public void SelectPipelineBy_Ok(bool standardResilience, string url, string expectedPipelineKey) - { - _builder.Services.AddFakeRedaction(); - - string? pipelineName = null; - - if (standardResilience) - { - pipelineName = _builder - .AddResilienceHandler("dummy") - .SelectPipelineBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)) - .AddRetryPolicy("test").PipelineName; - } - else - { - pipelineName = _builder - .AddStandardResilienceHandler() - .SelectPipelineBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)).PipelineName; - } - - var provider = _builder.Services.BuildServiceProvider().GetPipelineKeyProvider(pipelineName)!; - - using var request = new HttpRequestMessage(HttpMethod.Head, url); - - var key = provider.GetPipelineKey(request); - - Assert.Equal(expectedPipelineKey, key); - Assert.NotSame(provider.GetPipelineKey(request), provider.GetPipelineKey(request)); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs deleted file mode 100644 index 21e1adf191b..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Resilience.cs +++ /dev/null @@ -1,250 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Compliance.Classification; -using Microsoft.Extensions.Compliance.Redaction; -using Microsoft.Extensions.Compliance.Testing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Resilience; -using Microsoft.Extensions.Resilience.Internal; -using Microsoft.Extensions.Resilience.Options; -using Microsoft.Extensions.Telemetry.Metering; -using Moq; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test; - -public sealed partial class HttpClientBuilderExtensionsTests -{ - private const string DefaultPolicyName = "dummy-policy-name"; - - [Fact] - public void AddResilienceHandler_ArgumentValidation() - { - var services = new ServiceCollection(); - IHttpClientBuilder? builder = services.AddHttpClient("client"); - - Assert.Throws(() => builder.AddResilienceHandler(null!)); - Assert.Throws(() => builder.AddResilienceHandler(string.Empty)); - - builder = null; - Assert.Throws(() => builder!.AddResilienceHandler("pipeline-name")); - } - - [Fact] - public void AddResilienceHandler_EnsureOtherPipelineDefaultsNotAffected() - { - var called = false; - var services = new ServiceCollection().RegisterMetering().AddLogging(); - - services.ConfigureAll>(options => - { - Assert.NotEqual(HttpClientResiliencePredicates.IsTransientHttpFailure, options.ShouldHandleResultAsError); - }); - - services - .AddHttpClient("client") - .AddResilienceHandler("test").AddRetryPolicy(DefaultPolicyName); - - services - .AddResiliencePipeline("test2") - .AddRetryPolicy( - DefaultPolicyName, - options => - { - Assert.NotEqual(HttpClientResiliencePredicates.IsTransientHttpFailure, options.ShouldHandleResultAsError); - called = true; - }); - - var provider = services.BuildServiceProvider(); - var pipelineProvider = provider.GetRequiredService(); - - pipelineProvider.GetPipeline("client-test"); - pipelineProvider.GetPipeline("test2"); - - Assert.True(called); - } - - [Fact] - public void AddResilienceHandler_EnsureCorrectServicesRegistered() - { - var services = new ServiceCollection(); - IHttpClientBuilder? builder = services.AddHttpClient("client"); - - builder.AddResilienceHandler("test"); - - // add twice intentionally - builder.AddResilienceHandler("test"); - - Assert.Contains(services, s => s.ServiceType == typeof(IPolicyFactory)); - } - - public enum PolicyType - { - Fallback, - Retry, - CircuitBreaker, - } - - [InlineData(PolicyType.Fallback)] - [InlineData(PolicyType.CircuitBreaker)] - [InlineData(PolicyType.Retry)] - [Theory] - public async Task AddResilienceHandler_IndividialPolicies_EnsureProperDelegatesRegistered(PolicyType policyType) - { - // arrange - var called = false; - var services = new ServiceCollection().AddLogging().RegisterMetering(); - var builder = services.AddHttpClient("client"); - - ConfigureAndAssertPolicies(policyType, builder.AddResilienceHandler("test-pipeline"), () => called = true); - - builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); - services.ConfigureHttpFailureResultContext(); - - var provider = services.BuildServiceProvider(); - var client = provider.GetRequiredService().CreateClient("client"); - var pipelineProvider = provider.GetRequiredService(); - - // act - await client.GetAsync("https://dummy"); - - // assert - Assert.True(called); - - Assert.NotNull(pipelineProvider.GetPipeline("client-test-pipeline")); - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task AddResilienceHandler_EnsureProperPipelineInstanceRetrieved(bool bySelector) - { - // arrange - var resilienceProvider = new Mock(MockBehavior.Strict); - var services = new ServiceCollection().AddLogging().RegisterMetering().AddFakeRedaction(); - services.AddSingleton(resilienceProvider.Object); - var builder = services.AddHttpClient("client"); - var pipelineBuilder = builder.AddResilienceHandler("dummy"); - var expectedPipelineName = "client-dummy"; - if (bySelector) - { - pipelineBuilder.SelectPipelineByAuthority(DataClassification.Unknown); - } - - pipelineBuilder.AddRetryPolicy("test"); - builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); - - var provider = services.BuildServiceProvider(); - if (bySelector) - { - resilienceProvider.Setup(v => v.GetPipeline(expectedPipelineName, "https://dummy1")).Returns(Policy.NoOpAsync()); - } - else - { - resilienceProvider.Setup(v => v.GetPipeline(expectedPipelineName)).Returns(Policy.NoOpAsync()); - } - - var client = provider.GetRequiredService().CreateClient("client"); - - // act - await client.GetAsync("https://dummy1"); - - // assert - resilienceProvider.VerifyAll(); - } - - [Fact] - public async Task AddResilienceHandlerBySelector_EnsurePolicyProviderCalled() - { - // arrange - var services = new ServiceCollection().AddLogging().RegisterMetering(); - var providerMock = new Mock(MockBehavior.Strict); - services.AddSingleton(providerMock.Object); - var pipelineName = string.Empty; - - pipelineName = "client-my-pipeline"; - var clientBuilder = services.AddHttpClient("client"); - clientBuilder - .AddResilienceHandler("my-pipeline") - .AddRetryPolicy(DefaultPolicyName); - clientBuilder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); - - providerMock - .Setup(v => v.GetPipeline(pipelineName)) - .Returns(Policy.NoOpAsync()) - .Verifiable(); - - var provider = services.BuildServiceProvider(); - var client = provider.GetRequiredService().CreateClient("client"); - var pipelineProvider = provider.GetRequiredService(); - - // act - await client.GetAsync("https://dummy1"); - - // assert - providerMock.VerifyAll(); - } - - [Fact] - public void AddResilienceHandler_AuthoritySelectorAndNotConfiguredRedaction_EnsureValidated() - { - // arrange - var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() - .AddHttpClient("my-client") - .AddResilienceHandler("my-pipeline") - .SelectPipelineByAuthority(SimpleClassifications.PrivateData) - .AddRetryPolicy(DefaultPolicyName); - - var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); - - var error = Assert.Throws(() => factory.CreateClient("my-client")); - Assert.Equal("The redacted pipeline is an empty string and cannot be used for pipeline selection. Is redaction correctly configured?", error.Message); - } - - [Fact] - public void AddResilienceHandler_AuthorityByCustomSelector_NotValidated() - { - // arrange - var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() - .AddHttpClient("my-client") - .AddResilienceHandler("my-pipeline") - .SelectPipelineBy(_ => _ => string.Empty) - .AddRetryPolicy(DefaultPolicyName); - - var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); - - Assert.NotNull(factory.CreateClient("my-client")); - } - - private static void ConfigureAndAssertPolicies(PolicyType policyType, IResiliencePipelineBuilder builder, Action onCalled) - { - var optionsName = $"{builder.PipelineName}-{policyType}-{DefaultPolicyName}"; - - if (policyType == PolicyType.Fallback) - { - builder.AddFallbackPolicy( - DefaultPolicyName, - args => Task.FromResult(new HttpResponseMessage()), - options => onCalled()); - } - else if (policyType == PolicyType.Retry) - { - builder.AddRetryPolicy(DefaultPolicyName, options => onCalled()); - } - else if (policyType == PolicyType.CircuitBreaker) - { - builder.AddCircuitBreakerPolicy(DefaultPolicyName, options => onCalled()); - } - else - { - throw new NotSupportedException(); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs deleted file mode 100644 index ea56e26a414..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpStandardResilienceOptionsTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Polly; - -public class HttpStandardResilienceOptionsTests -{ - private readonly HttpStandardResilienceOptions _defaultInstance; - - public HttpStandardResilienceOptionsTests() - { - _defaultInstance = new HttpStandardResilienceOptions(); - } - - [Fact] - public void TimeoutSettings_Ok() - { - Assert.True(_defaultInstance.AttemptTimeoutOptions.TimeoutInterval < _defaultInstance.TotalRequestTimeoutOptions.TimeoutInterval); - } - - [Fact] - public void PropertiesNotNull() - { - Assert.NotNull(_defaultInstance.RetryOptions); - Assert.NotNull(_defaultInstance.AttemptTimeoutOptions); - Assert.NotNull(_defaultInstance.TotalRequestTimeoutOptions); - Assert.NotNull(_defaultInstance.CircuitBreakerOptions); - Assert.NotNull(_defaultInstance.BulkheadOptions); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs deleted file mode 100644 index 668db2302a1..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ContextExtensionsTest.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net; -using System.Net.Http; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Http.Telemetry; -using Microsoft.Extensions.Telemetry; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; - -public class ContextExtensionsTest -{ - [Fact] - public void ArgumentValidation_Ok() - { - Assert.Throws(() => Resilience.Internal.ContextExtensions.SetRequestMetadata(null!, new HttpRequestMessage())); - Assert.Throws(() => Resilience.Internal.ContextExtensions.SetRequestMetadata(new Context(), null!)); - } - - [InlineData("A", null, "A")] - [InlineData("A", "B", "B")] - [InlineData(null, "B", "B")] - [InlineData(null, null, null)] - [Theory] - public void SetRequestMetadata_EnsureCorrectBehavior(string? requestMetadata, string? contextMetadata, string? expectedMetadata) - { - var context = new Context(); - using var request = new HttpRequestMessage(); - - if (requestMetadata != null) - { - request.SetRequestMetadata(new RequestMetadata { DependencyName = requestMetadata }); - } - - if (contextMetadata != null) - { - context[TelemetryConstants.RequestMetadataKey] = new RequestMetadata { DependencyName = contextMetadata }; - } - - context.SetRequestMetadata(request); - - context.TryGetValue(TelemetryConstants.RequestMetadataKey, out var val); - - Assert.Equal(expectedMetadata, (val as RequestMetadata)?.DependencyName); - } - - [Fact] - public void RequestMessageProviderAndSetter_EnsureCorrectBehavior() - { - using var message = new HttpRequestMessage(); - var context = new Context(); - var setter = Resilience.Internal.ContextExtensions.CreateRequestMessageSetter("my-pipeline"); - var provider = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("my-pipeline"); - var providerOther = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("my-pipeline-other"); - - setter(context, message); - - Assert.Equal(message, provider(context)); - Assert.NotEqual(message, providerOther(context)); - Assert.Null(providerOther(context)); - } - - [Fact] - public void InvokerProviderAndSetter_EnsureCorrectBehavior() - { - using var handler = new TestHandlerStub(HttpStatusCode.OK); - using var invoker = new HttpMessageInvoker(handler); - var context = new Context(); - var setter = Resilience.Internal.ContextExtensions.CreateMessageInvokerSetter("my-pipeline"); - var provider = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("my-pipeline"); - var providerOther = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("my-pipeline-other"); - - setter(context, new Lazy(() => invoker)); - - Assert.Equal(invoker, provider(context)); - Assert.NotEqual(invoker, providerOther(context)); - Assert.Null(providerOther(context)); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs deleted file mode 100644 index 00f1ac20509..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpResiliencePipelineBuilderTest.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Resilience; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; -public class HttpResiliencePipelineBuilderTest -{ - [Fact] - public void Ctor_Ok() - { - var services = new ServiceCollection(); - var builder = services.AddResiliencePipeline("test"); - - var httpBuilder = new HttpResiliencePipelineBuilder(builder); - - Assert.Equal(services, httpBuilder.Services); - Assert.Equal("test", httpBuilder.PipelineName); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs deleted file mode 100644 index fa51b67b75e..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/ResilienceHandlerTest.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Http.Telemetry; -using Microsoft.Extensions.Telemetry; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; - -public class ResilienceHandlerTest -{ - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureRequestMetadataFlows(bool executionContextSet) - { - using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); - using var invoker = new HttpMessageInvoker(handler); - using var request = new HttpRequestMessage(); - - if (executionContextSet) - { - request.SetPolicyExecutionContext(new Context()); - } - - request.SetRequestMetadata(new RequestMetadata()); - - handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); - - await invoker.SendAsync(request, default); - - if (executionContextSet) - { - Assert.NotNull(request.GetPolicyExecutionContext()![TelemetryConstants.RequestMetadataKey]); - } - else - { - Assert.Null(request.GetPolicyExecutionContext()); - } - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) - { - using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); - using var invoker = new HttpMessageInvoker(handler); - using var request = new HttpRequestMessage(); - - if (executionContextSet) - { - request.SetPolicyExecutionContext(new Context()); - } - - handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); - - await invoker.SendAsync(request, default); - - if (executionContextSet) - { - Assert.NotNull(request.GetPolicyExecutionContext()); - } - else - { - Assert.Null(request.GetPolicyExecutionContext()); - } - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureInvoker(bool executionContextSet) - { - using var handler = new ResilienceHandler("dummy", _ => Policy.NoOpAsync()); - using var invoker = new HttpMessageInvoker(handler); - using var request = new HttpRequestMessage(); - - if (executionContextSet) - { - request.SetPolicyExecutionContext(new Context()); - } - - handler.InnerHandler = new TestHandlerStub((r, _) => - { - var invokerProvider = Resilience.Internal.ContextExtensions.CreateMessageInvokerProvider("dummy"); - var requestProvider = Resilience.Internal.ContextExtensions.CreateRequestMessageProvider("dummy"); - - Assert.NotNull(invokerProvider(r.GetPolicyExecutionContext()!)); - Assert.Equal(request, requestProvider(r.GetPolicyExecutionContext()!)); - - return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); - }); - - var response = await invoker.SendAsync(request, default); - - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs deleted file mode 100644 index 15121709710..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/ValidationHelperTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Linq; -using Microsoft.Extensions.Http.Resilience.Internal.Validators; -using Microsoft.Extensions.Resilience.Options; -using Polly.Contrib.WaitAndRetry; -using Xunit; - -namespace Microsoft.Extensions.Resilience.Internal.Test; - -public class ValidationHelperTests -{ - [Fact] - public void GetExponentialWithJitterDeterministicDelay_ShouldReturnRetryPolicyUpperboundDelaySum() - { - var retryPolicyOptions = new RetryPolicyOptions - { - RetryCount = 3, - BaseDelay = TimeSpan.FromSeconds(2), - BackoffType = BackoffType.ExponentialWithJitter - }; - var upperbound = ValidationHelper.GetExponentialWithJitterDeterministicDelay(retryPolicyOptions); - var jitteredDelays = Backoff.DecorrelatedJitterBackoffV2(retryPolicyOptions.BaseDelay, retryPolicyOptions.RetryCount); - - var expected = TimeSpan.FromTicks(114_061_988); - Assert.True(upperbound >= jitteredDelays.Aggregate((accumulated, current) => accumulated + current)); - Assert.Equal(expected.TotalMilliseconds, upperbound.TotalMilliseconds); - } - - [Fact] - public void GetExponentialWithJitterDeterministicDelay_MaxDelayTest() - { - var options = new RetryPolicyOptions - { - RetryCount = 99, - BaseDelay = TimeSpan.FromDays(1), - BackoffType = BackoffType.ExponentialWithJitter - }; - - var upper = ValidationHelper.GetRetryPolicyDelaySum(options); - - Assert.Equal(TimeSpan.MaxValue.Ticks - 1000, upper.Ticks); - } - - [Fact] - public void GetRetryPolicyDelaySum_Ok() - { - var options = new RetryPolicyOptions - { - RetryCount = 2, - BaseDelay = TimeSpan.FromSeconds(2), - BackoffType = BackoffType.Linear - }; - - Assert.Equal(TimeSpan.FromSeconds(6), options.GetRetryPolicyDelaySum()); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs deleted file mode 100644 index 7dc799eb6a0..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/TestHandlerStub.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Http.Resilience.Test; - -public class TestHandlerStub : DelegatingHandler -{ - private readonly Func> _handlerFunc; - - public TestHandlerStub(HttpStatusCode responseStatus) -#pragma warning disable CA2000 // Dispose objects before losing scope - : this(new HttpResponseMessage(responseStatus)) -#pragma warning restore CA2000 // Dispose objects before losing scope - { - } - - public TestHandlerStub(HttpResponseMessage responseMessage) - : this((_, _) => Task.FromResult(responseMessage)) - { - } - - public TestHandlerStub(Func> handlerFunc) - { - _handlerFunc = handlerFunc; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return _handlerFunc(request, cancellationToken); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/ContextExtensionsTest.cs similarity index 82% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/ContextExtensionsTest.cs index 41149026dfe..16a1c73c9d5 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/PolicyContextExtensionsTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/ContextExtensionsTest.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Test; -public class PolicyContextExtensionsTest +public class ContextExtensionsTest { [Fact] public void WithCallingRequestMessage_ShouldSetRequestMessageInContext() @@ -49,4 +49,16 @@ public void GetCallingRequestMessage_RequestMessageNotSet_ShouldReturnNull() var result = context.GetCallingRequestMessage(); Assert.Null(result); } + + [Fact] + public void GetCallingRequestMessage_RequestMessageSetToIncompatibleObject_ShouldReturnNull() + { + var context = new Context + { + ["CallingRequestMessage"] = new object() + }; + + var result = context.GetCallingRequestMessage(); + Assert.Null(result); + } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs index 2c156015d16..14b63c430a1 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/FaultInjection/Internal/FaultInjectionTelemetryHandlerTests.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Resilience.FaultInjection; using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Logging; using Microsoft.Extensions.Telemetry.Testing.Metering; using Moq; using Xunit; @@ -17,7 +19,7 @@ public class FaultInjectionTelemetryHandlerTests [Fact] public void LogAndMeter_WithHttpContentKey() { - var logger = Mock.Of>(); + var logger = new FakeLogger(); using var meter = new Meter(); using var metricCollector = new MetricCollector(meter, MetricName); @@ -39,6 +41,14 @@ public void LogAndMeter_WithHttpContentKey() Assert.Equal(FaultType, latest.Tags[FaultInjectionEventMeterDimensions.FaultType]); Assert.Equal(InjectedValue, latest.Tags[FaultInjectionEventMeterDimensions.InjectedValue]); Assert.Equal(HttpContentKey, latest.Tags[FaultInjectionEventMeterDimensions.HttpContentKey]); + + var entries = logger.Collector.GetSnapshot(); + entries.Should().HaveCount(1); + entries[0].Message.Should().Be( + "Fault-injection group name: TestClient. " + + "Fault type: Type. " + + "Injected value: Value. " + + "Http content key: HttpContentKey."); } [Fact] diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs index c112ea01973..3a1c4f5a23c 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs @@ -9,11 +9,12 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using FluentAssertions; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Http.Resilience.Routing.Internal; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Telemetry.Metering; using Moq; using Polly; @@ -31,21 +32,21 @@ public abstract class HedgingTests : IDisposable public const int DefaultHedgingAttempts = 3; private readonly CancellationTokenSource _cancellationTokenSource; - private readonly Mock _requestCloneHandlerMock; + private readonly Mock _requestCloneHandlerMock; private readonly Mock _requestRoutingStrategyMock; - private readonly Mock _requestRoutingStrategyFactoryMock; + private readonly Mock _requestRoutingStrategyFactoryMock; private readonly IServiceCollection _services; private readonly List _requests = new(); private readonly Queue _responses = new(); private readonly Func _createDefaultBuilder; private bool _failure; - protected HedgingTests(Func createDefaultBuilder) + private protected HedgingTests(Func createDefaultBuilder) { _cancellationTokenSource = new CancellationTokenSource(); - _requestCloneHandlerMock = new Mock(MockBehavior.Strict); + _requestCloneHandlerMock = new Mock(MockBehavior.Strict); _requestRoutingStrategyMock = new Mock(MockBehavior.Strict); - _requestRoutingStrategyFactoryMock = new Mock(MockBehavior.Strict); + _requestRoutingStrategyFactoryMock = new Mock(MockBehavior.Strict); _services = new ServiceCollection().RegisterMetering().AddLogging(); _services.AddSingleton(_requestCloneHandlerMock.Object); @@ -76,28 +77,30 @@ public void AddHedging_EnsureRequestCloner() _createDefaultBuilder(services.AddHttpClient("dummy"), _requestRoutingStrategyFactoryMock.Object); - Assert.NotNull(services.BuildServiceProvider().GetRequiredService()); + Assert.NotNull(services.BuildServiceProvider().GetRequiredService()); } [Fact] public async Task SendAsync_EnsureContextFlows() { + var key = new ResiliencePropertyKey("custom-data"); using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - var context = new Context { ["custom-data"] = "my-data" }; - request.SetPolicyExecutionContext(context); + var context = ResilienceContext.Get(); + context.Properties.Set(key, "my-data"); + request.SetResilienceContext(context); var calls = 0; SetupRouting(); SetupRoutes(3, "https://enpoint-{0}:80"); - _services.RemoveAll(); - _services.AddRequestCloner(); + _services.RemoveAll(); + _services.TryAddSingleton(); ConfigureHedgingOptions(options => { - options.OnHedgingAsync = args => + options.OnHedging = args => { - Assert.Equal("my-data", (string)args.Context["custom-data"]); + args.Context.Properties.GetValue(key, "").Should().Be("my-data"); calls++; - return Task.CompletedTask; + return default; }; }); @@ -124,7 +127,7 @@ public async Task SendAsync_NoErrors_ShouldReturnSingleResponse() AddResponse(HttpStatusCode.OK); var response = await client.SendAsync(request, _cancellationTokenSource.Token); - Assert.Empty(_responses); + AssertNoResponse(); Assert.Single(_requests); Assert.Equal("https://enpoint-1:80/some-path?query", _requests[0]); @@ -175,9 +178,9 @@ public async Task SendAsync_NoRoutesLeftAndSomeResultPresent_ShouldReturn() SetupCloner(request, true); SetupRoutes(4); + AddResponse(HttpStatusCode.ServiceUnavailable); AddResponse(HttpStatusCode.InternalServerError); AddResponse(HttpStatusCode.InternalServerError); - AddResponse(HttpStatusCode.ServiceUnavailable); using var client = CreateClientWithHandler(); @@ -231,16 +234,23 @@ public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() Assert.Equal("https://enpoint-3:80/some-path?query", _requests[2]); } - protected void AddResponse(HttpStatusCode statusCode) + protected void AssertNoResponse() => Assert.Empty(_responses); + + protected void AddResponse(HttpStatusCode statusCode) => AddResponse(statusCode, 1); + + protected void AddResponse(HttpStatusCode statusCode, int count) { + for (int i = 0; i < count; i++) + { #pragma warning disable CA2000 // Dispose objects before losing scope - _responses.Enqueue(new HttpResponseMessage(statusCode)); + _responses.Enqueue(new HttpResponseMessage(statusCode)); #pragma warning restore CA2000 // Dispose objects before losing scope + } } - protected abstract void ConfigureHedgingOptions(Action configure); + protected abstract void ConfigureHedgingOptions(Action configure); - protected System.Net.Http.HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService().CreateClient(ClientId); + protected HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService().CreateClient(ClientId); protected void SetupCloner(HttpRequestMessage request, bool createCalled) { @@ -265,7 +275,7 @@ private Task InnerHandlerFunction(HttpRequestMessage reques return Task.FromResult(_responses.Dequeue()); } - private void SetupRoutes(int totalAttempts, string pattern = "https://dummy-{0}") + protected void SetupRoutes(int totalAttempts, string pattern = "https://dummy-{0}") { int attemptCount = 0; @@ -280,7 +290,7 @@ private void SetupRoutes(int totalAttempts, string pattern = "https://dummy-{0}" .Returns(() => attemptCount <= totalAttempts); } - private void SetupRouting(bool mustReturn = true) + protected void SetupRouting(bool mustReturn = true) { _requestRoutingStrategyFactoryMock.Setup(s => s.CreateRoutingStrategy()).Returns(() => _requestRoutingStrategyMock.Object); if (mustReturn) diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs deleted file mode 100644 index 26f9a50a609..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/ConfigurationStubFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; - -namespace Microsoft.Extensions.Http.Resilience.Test.Hedgings.Helpers; - -public sealed class ConfigurationStubFactory -{ - public static IConfiguration Create(Dictionary collection) - { - return new ConfigurationBuilder() - .AddInMemoryCollection(collection) - .Build(); - } - - public static IConfiguration CreateEmpty() - { - return new ConfigurationBuilder().Build(); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs deleted file mode 100644 index b48ec575013..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/OptionsUtilities.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; - -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; - -internal static class OptionsUtilities -{ - public static void ValidateOptions(object options) - { - var context = new ValidationContext(options); - Validator.ValidateObject(options, context, true); - } - - public static bool EqualOptions(T options1, T options2) - { - if (options1 is null && options2 is null) - { - return true; - } - - if (options1 is null || options2 is null) - { - return false; - } - - var propertiesValuesByName1 = options1.GetPropertiesValuesByName(); - var propertiesValuesByName2 = options2.GetPropertiesValuesByName(); - - foreach (var propertyDefinition1 in propertiesValuesByName1) - { - var propertyName = propertyDefinition1.Key; - var propertyValue1 = propertyDefinition1.Value; - - if (!propertiesValuesByName2.TryGetValue(propertyName, out var propertyValue2) || - !Equals(propertyValue1, propertyValue2)) - { - return false; - } - } - - return true; - } - - private static IDictionary GetPropertiesValuesByName(this T options) - { - return options! - .GetType() - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .GroupBy(property => property.Name) - .ToDictionary( - propertyGroup => propertyGroup.Key, - propertyGroup => propertyGroup.Last().GetValue(options)!); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs index 45035a4d934..4703db31cc1 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpClientHedgingResiliencePredicatesTests.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net; using System.Net.Http; +using FluentAssertions; +using Polly; using Polly.CircuitBreaker; using Polly.Timeout; using Xunit; @@ -19,4 +22,24 @@ public void IsTransientException_Ok() Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new HttpRequestException())); Assert.False(HttpClientHedgingResiliencePredicates.IsTransientHttpException(new InvalidOperationException())); } + + [Fact] + public void IsTransientOutcome_Ok() + { + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromException(new TimeoutRejectedException()))); + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromException(new BrokenCircuitException()))); + Assert.True(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromException(new HttpRequestException()))); + Assert.False(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromException(new InvalidOperationException()))); + Assert.False(HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromResult(null))); + } + + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.ServiceUnavailable, true)] + [InlineData(HttpStatusCode.OK, false)] + [Theory] + public void IsTransientOutcome_Response_Ok(HttpStatusCode code, bool expected) + { + using var response = new HttpResponseMessage(code); + HttpClientHedgingResiliencePredicates.IsTransientHttpOutcome(Outcome.FromResult(response)).Should().Be(expected); + } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs deleted file mode 100644 index 62b9dca196e..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpHedgingPolicyOptionsTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using Microsoft.Extensions.Resilience.Options; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; - -public class HttpHedgingPolicyOptionsTests -{ -#pragma warning disable S2330 - public static readonly IEnumerable HandledExceptionsClassified = new[] - { - new object[] { new InvalidCastException(), false }, - new object[] { new HttpRequestException(), true } - }; - - [Theory] - [InlineData(HttpStatusCode.OK, false)] - [InlineData(HttpStatusCode.BadRequest, false)] - [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] - [InlineData(HttpStatusCode.InternalServerError, true)] - [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] - [InlineData(HttpStatusCode.RequestTimeout, true)] - public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify( - HttpStatusCode statusCode, - bool expected) - { - using var httpReq = new HttpResponseMessage { StatusCode = statusCode }; - var actual = new HttpHedgingPolicyOptions().ShouldHandleResultAsError(httpReq); - - Assert.Equal(expected, actual); - } - - [Theory] - [MemberData(nameof(HandledExceptionsClassified))] - public void ShouldHandleException_DefaultInstance_ShouldClassify( - Exception exception, - bool expected) - { - var actual = new HttpHedgingPolicyOptions().ShouldHandleException(exception); - - Assert.Equal(expected, actual); - } - - [Fact] - public void OnHedging_CallsRecordMetric() - { - var options = new HttpHedgingPolicyOptions(); - var expectedError = "Something went wrong"; - var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); - - Assert.NotNull(options.OnHedgingAsync( - new HedgingTaskArguments( - delegateResult, - new Context(), - 0, - CancellationToken.None))); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs similarity index 76% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs index 22dbf3dc056..c575ed64a45 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/Validators/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs @@ -3,15 +3,15 @@ using System; using System.Collections.Generic; -#if NET8_0_OR_GREATER -using System.Linq; -#endif +using System.Threading.Tasks; +using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience.Internal.Validators; using Moq; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Internals.Validators; +namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; + public class HttpStandardHedgingResilienceOptionsCustomValidatorTests { [Fact] @@ -19,7 +19,7 @@ public void Validate_InvalidOptions_EnsureValidationErrors() { HttpStandardHedgingResilienceOptions options = new(); options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromSeconds(1); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(1); var validationResult = CreateValidator("dummy").Validate("dummy", options); @@ -28,7 +28,7 @@ public void Validate_InvalidOptions_EnsureValidationErrors() #if NET8_0_OR_GREATER // Whilst these API are marked as NET6_0_OR_GREATER we don't build .NET 6.0, // and as such the API is available in .NET 8 onwards. - Assert.Equal(3, validationResult.Failures.Count()); + validationResult.Failures.Should().HaveCount(3); #endif } @@ -39,7 +39,7 @@ public void Validate_ValidOptions_NoValidationErrors() var validationResult = CreateValidator("dummy").Validate("dummy", options); - Assert.True(validationResult.Succeeded); + validationResult.Succeeded.Should().BeTrue(); } [Fact] @@ -49,9 +49,8 @@ public void Validate_ValidOptionsWithoutRouting_ValidationErrors() var validationResult = CreateValidator("dummy").Validate("other", options); - Assert.True(validationResult.Failed); - Assert.Equal("The hedging routing is not configured for 'other' HTTP client.", validationResult.FailureMessage); - + validationResult.Failed.Should().BeTrue(); + validationResult.FailureMessage.Should().Be("The hedging routing is not configured for 'other' HTTP client."); } public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data @@ -59,24 +58,24 @@ public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data get { var options = new HttpStandardHedgingResilienceOptions(); - options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; - options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * 2); + options.EndpointOptions.TimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.Timeout.TotalMilliseconds * 2); yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); - options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.EndpointOptions.TimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = - TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); + TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.Timeout.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); options.HedgingOptions.MaxHedgedAttempts = 1; - options.HedgingOptions.HedgingDelay = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.HedgingOptions.HedgingDelay = options.TotalRequestTimeoutOptions.Timeout; yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); options.HedgingOptions.HedgingDelay = TimeSpan.FromDays(1); - options.HedgingOptions.HedgingDelayGenerator = _ => TimeSpan.FromDays(1); + options.HedgingOptions.HedgingDelayGenerator = _ => new ValueTask(TimeSpan.FromDays(1)); yield return new object[] { options }; } } @@ -95,12 +94,12 @@ public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data get { var options = new HttpStandardHedgingResilienceOptions(); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); - options.EndpointOptions.TimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(3); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(2); + options.EndpointOptions.TimeoutOptions.Timeout = TimeSpan.FromSeconds(3); yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(2); yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); @@ -108,8 +107,8 @@ public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data yield return new object[] { options }; options = new HttpStandardHedgingResilienceOptions(); - options.EndpointOptions.TimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; - options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.TimeoutInterval.TotalMilliseconds / 2); + options.EndpointOptions.TimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; + options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.EndpointOptions.TimeoutOptions.Timeout.TotalMilliseconds / 2); yield return new object[] { options }; } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsTests.cs new file mode 100644 index 00000000000..2fa18ba649a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; + +public class HttpStandardHedgingResilienceOptionsTests +{ + private readonly HttpStandardHedgingResilienceOptions _options; + + public HttpStandardHedgingResilienceOptionsTests() + { + _options = new HttpStandardHedgingResilienceOptions(); + } + + [Fact] + public void Ctor_EnsureDefaults() + { + _options.TotalRequestTimeoutOptions.Timeout.Should().Be(TimeSpan.FromSeconds(30)); + _options.EndpointOptions.TimeoutOptions.Timeout.Should().Be(TimeSpan.FromSeconds(10)); + + _options.TotalRequestTimeoutOptions.StrategyName.Should().Be("StandardHedging-TotalRequestTimeout"); + _options.HedgingOptions.StrategyName.Should().Be("StandardHedging-Hedging"); + _options.EndpointOptions.CircuitBreakerOptions.StrategyName.Should().Be("StandardHedging-CircuitBreaker"); + _options.EndpointOptions.TimeoutOptions.StrategyName.Should().Be("StandardHedging-AttemptTimeout"); + _options.EndpointOptions.RateLimiterOptions.StrategyName.Should().Be("StandardHedging-RateLimiter"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs deleted file mode 100644 index e2ae5f62385..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/HedgingContextExtensionsTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Http.Resilience.Internal; -using Moq; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; - -public class HedgingContextExtensionsTests -{ - [Fact] - public void GetSet_RoutingStrategy_Ok() - { - var setter = HedgingContextExtensions.CreateRoutingStrategySetter("my-pipeline"); - var getter = HedgingContextExtensions.CreateRoutingStrategyProvider("my-pipeline"); - var getterInvalid = HedgingContextExtensions.CreateRoutingStrategyProvider("my-other-pipeline"); - - var context = new Context(); - var strategy = Mock.Of(); - - setter(context, strategy); - - Assert.Equal(strategy, getter(context)); - Assert.Null(getterInvalid(context)); - } - - [Fact] - public void GetSet_HttpRequestMessageSnapshot_Ok() - { - var setter = HedgingContextExtensions.CreateRequestMessageSnapshotSetter("my-pipeline"); - var getter = HedgingContextExtensions.CreateRequestMessageSnapshotProvider("my-pipeline"); - var getterInvalid = HedgingContextExtensions.CreateRequestMessageSnapshotProvider("my-other-pipeline"); - - var context = new Context(); - var snapshot = Mock.Of(); - - setter(context, snapshot); - - Assert.Equal(snapshot, getter(context)); - Assert.Null(getterInvalid(context)); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs deleted file mode 100644 index aeefc7bbfaa..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RequestMessageSnapshotPolicyTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.Http.Resilience.Internal; -using Moq; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; - -public class RequestMessageSnapshotPolicyTests -{ - [Fact] - public async Task SendAsync_EnsureSnapshotAttached() - { - var snapshot = new Mock(MockBehavior.Strict); - snapshot.Setup(s => s.Dispose()); - var cloner = new Mock(MockBehavior.Strict); - cloner.Setup(c => c.CreateSnapshot(It.IsAny())).Returns(snapshot.Object); - var policy = new RequestMessageSnapshotPolicy("dummy", cloner.Object); - var context = new Context - { - ["Resilience.ContextExtensions.Request-dummy"] = new HttpRequestMessage() - }; - - await policy.ExecuteAsync(_ => Task.FromResult(new HttpResponseMessage()), context); - - HedgingContextExtensions.CreateRequestMessageSnapshotProvider("dummy")(context).Should().Be(snapshot.Object); - cloner.VerifyAll(); - snapshot.VerifyAll(); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs index 457dc77fe81..d1b678a1e61 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs @@ -12,13 +12,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Http.Resilience.Routing.Internal; -using Microsoft.Extensions.Http.Resilience.Test.Hedgings.Helpers; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Options; using Microsoft.Extensions.Resilience; -using Microsoft.Extensions.Resilience.Internal; using Moq; using Polly; +using Polly.Hedging; +using Polly.Registry; using Xunit; namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; @@ -52,7 +52,7 @@ public void EnsureValidated_BasicValidation() [Fact] public void EnsureValidated_AdvancedValidation() { - Builder.Configure(options => options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1)); + Builder.Configure(options => options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(1)); Assert.Throws(() => CreateClientWithHandler()); } @@ -72,7 +72,7 @@ public void Configure_CallbackWithServiceProvider_Ok() { Builder.Configure((o, serviceProvider) => { - serviceProvider.GetRequiredService().Should().NotBeNull(); + serviceProvider.GetRequiredService>().Should().NotBeNull(); o.HedgingOptions.MaxHedgedAttempts = 8; }); @@ -102,6 +102,22 @@ public void Configure_ValidConfigurationSection_ShouldInitialize() Assert.Equal(8, options.HedgingOptions.MaxHedgedAttempts); } + [Fact] + public void ActionGenerator_Ok() + { + var options = Builder.Services.BuildServiceProvider().GetRequiredService>().Get(Builder.Name); + var generator = options.HedgingOptions.HedgingActionGenerator; + var primary = ResilienceContext.Get(); + var secondary = ResilienceContext.Get(); + using var response = new HttpResponseMessage(HttpStatusCode.OK); + + var args = new HedgingActionGeneratorArguments(primary, secondary, 0, _ => Outcome.FromResultAsTask(response)); + generator.Invoking(g => g(args)).Should().Throw().WithMessage("Request message snapshot is not attached to the resilience context."); + + primary.Properties.Set(ResilienceKeys.RequestSnapshot, Mock.Of()); + generator.Invoking(g => g(args)).Should().Throw().WithMessage("Routing strategy is not attached to the resilience context."); + } + #if NET8_0_OR_GREATER // Whilst these API are marked as NET6_0_OR_GREATER we don't build .NET 6.0, // and as such the API is available in .NET 8 onwards. @@ -147,35 +163,40 @@ public void Configure_EmptyConfigurationSection_ShouldThrow() [Fact] public async Task VerifyPipeline() { - var noPolicy = Policy.NoOpAsync(); - var builder = new Mock>(MockBehavior.Strict); - Builder.Services.RemoveAll>(); - Builder.Services.AddSingleton(builder.Object); - - var serviceProvider = Builder.Services.BuildServiceProvider(); - var options = serviceProvider.GetRequiredService>().Get(Builder.Name); - - // primary handler - builder.SetupSequence(o => o.Initialize(It.Is(v => v.PipelineName == "clientId-standard-hedging"))); - builder.SetupSequence(o => o.AddTimeoutPolicy("StandardHedging-TotalRequestTimeout", options.TotalRequestTimeoutOptions)).Returns(builder.Object); - builder.SetupSequence(o => o.AddPolicy(It.Is>(p => p is RoutingPolicy))).Returns(builder.Object); - builder.SetupSequence(o => o.AddPolicy(It.Is>(p => p is RequestMessageSnapshotPolicy))).Returns(builder.Object); - builder.SetupSequence(o => o.AddHedgingPolicy("StandardHedging-Hedging", It.IsAny>(), options.HedgingOptions)).Returns(builder.Object); - builder.Setup(o => o.Build()).Returns(noPolicy); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + var strategies = new List>(); - // inner handler - builder.SetupSequence(o => o.Initialize(It.Is(v => v.PipelineName == "clientId-standard-hedging-endpoint"))); - builder.SetupSequence(o => o.AddBulkheadPolicy("StandardHedging-Bulkhead", options.EndpointOptions.BulkheadOptions)).Returns(builder.Object); - builder.SetupSequence(o => o.AddCircuitBreakerPolicy("StandardHedging-CircuitBreaker", options.EndpointOptions.CircuitBreakerOptions)).Returns(builder.Object); - builder.SetupSequence(o => o.AddTimeoutPolicy("StandardHedging-AttemptTimeout", options.EndpointOptions.TimeoutOptions)).Returns(builder.Object); - builder.Setup(o => o.Build()).Returns(noPolicy); + Builder.Services.RemoveAll(); + Builder.Services.AddTransient(_ => + { + return new ResilienceStrategyBuilder + { + OnCreatingStrategy = list => strategies.Add(list) + }; + }); + Builder.SelectStrategyByAuthority(SimpleClassifications.PublicData); - using var client = serviceProvider.GetRequiredService().CreateClient(ClientId); + SetupRouting(); + SetupRoutes(1); + SetupCloner(request, false); AddResponse(HttpStatusCode.OK); - using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + + using var client = CreateClientWithHandler(); await client.SendAsync(request, CancellationToken.None); - builder.VerifyAll(); + // primary handler + strategies.Should().HaveCount(2); + strategies[0].Should().HaveCount(4); + strategies[0][0].GetType().Name.Should().Contain("Routing"); + strategies[0][1].GetType().Name.Should().Contain("Snapshot"); + strategies[0][2].GetType().Name.Should().Contain("Timeout"); + strategies[0][3].GetType().Name.Should().Contain("Hedging"); + + // inner handler + strategies[1].Should().HaveCount(3); + strategies[1][0].GetType().Name.Should().Contain("RateLimiter"); + strategies[1][1].GetType().Name.Should().Contain("CircuitBreaker"); + strategies[1][2].GetType().Name.Should().Contain("Timeout"); } [InlineData(null)] @@ -183,22 +204,22 @@ public async Task VerifyPipeline() [Theory] public async Task VerifyPipelineSelection(string? customKey) { - var noPolicy = Policy.NoOpAsync(); - var provider = new Mock(MockBehavior.Strict); + var noPolicy = NullResilienceStrategy.Instance; + var provider = new Mock>(MockBehavior.Strict); Builder.Services.RemoveAll(); Builder.Services.AddSingleton(provider.Object); if (customKey == null) { - Builder.SelectPipelineByAuthority(SimpleClassifications.PublicData); + Builder.SelectStrategyByAuthority(SimpleClassifications.PublicData); } else { - Builder.SelectPipelineBy(_ => _ => customKey); + Builder.SelectStrategyBy(_ => _ => customKey); } customKey ??= "https://key:80"; - provider.Setup(v => v.GetPipeline("clientId-standard-hedging")).Returns(noPolicy); - provider.Setup(v => v.GetPipeline("clientId-standard-hedging-endpoint", customKey)).Returns(noPolicy); + provider.Setup(v => v.GetStrategy(new HttpKey("clientId-standard-hedging", string.Empty))).Returns(noPolicy); + provider.Setup(v => v.GetStrategy(new HttpKey("clientId-standard-hedging-endpoint", customKey))).Returns(noPolicy); using var client = CreateClientWithHandler(); using var request = new HttpRequestMessage(HttpMethod.Get, "https://key:80/discarded"); @@ -209,5 +230,39 @@ public async Task VerifyPipelineSelection(string? customKey) provider.VerifyAll(); } - protected override void ConfigureHedgingOptions(Action configure) => Builder.Configure(options => configure(options.HedgingOptions)); + [Fact] + public async Task DynamicReloads_Ok() + { + // arrange + var requests = new List(); + var config = ConfigurationStubFactory.Create( + new() + { + { "standard:HedgingOptions:MaxHedgedAttempts", "3" } + }, + out var reloadAction).GetSection("standard"); + + Builder.Configure(config).Configure(options => options.HedgingOptions.HedgingDelay = Timeout.InfiniteTimeSpan); + SetupRouting(); + SetupRoutes(10); + + var client = CreateClientWithHandler(); + + // act && assert + AddResponse(HttpStatusCode.InternalServerError, 3); + using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + SetupCloner(firstRequest, true); + await client.SendAsync(firstRequest); + AssertNoResponse(); + + reloadAction(new() { { "standard:HedgingOptions:MaxHedgedAttempts", "7" } }); + + AddResponse(HttpStatusCode.InternalServerError, 7); + using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); + SetupCloner(secondRequest, true); + await client.SendAsync(secondRequest); + AssertNoResponse(); + } + + protected override void ConfigureHedgingOptions(Action configure) => Builder.Configure(options => configure(options.HedgingOptions)); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/ConfigurationStubFactory.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/ConfigurationStubFactory.cs new file mode 100644 index 00000000000..69ac1235e92 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/ConfigurationStubFactory.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Extensions.Http.Resilience.Test.Helpers; + +public sealed class ConfigurationStubFactory +{ + public static IConfiguration Create(Dictionary collection) => Create(collection, out _); + + public static IConfiguration Create(Dictionary collection, out Action> reload) + { + var reloadable = new ReloadableConfiguration(); + reloadable.Reload(collection); + + reload = reloadable.Reload; + + return new ConfigurationBuilder() + .Add(reloadable) + .Build(); + } + + public static IConfiguration CreateEmpty() + { + return new ConfigurationBuilder().Build(); + } + + private class ReloadableConfiguration : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return this; + } + + public void Reload(Dictionary data) + { + Data = new Dictionary(data, StringComparer.OrdinalIgnoreCase); + OnReload(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/OptionsUtilities.cs similarity index 96% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/OptionsUtilities.cs index dc7f7df03b9..d7c41fed1e9 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Helpers/OptionsUtilities.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/OptionsUtilities.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Reflection; -namespace Microsoft.Extensions.Http.Resilience.Test; +namespace Microsoft.Extensions.Http.Resilience.Test.Helpers; internal static class OptionsUtilities { diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs similarity index 76% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs index 53ab6157c80..1f4a299285f 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Helpers/TestHandlerStub.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs @@ -2,16 +2,22 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; +namespace Microsoft.Extensions.Http.Resilience.Test.Helpers; public class TestHandlerStub : DelegatingHandler { private readonly Func> _handlerFunc; + public TestHandlerStub(HttpStatusCode statusCode) + : this((_, _) => Task.FromResult(new HttpResponseMessage(statusCode))) + { + } + public TestHandlerStub(Func> handlerFunc) { _handlerFunc = handlerFunc; diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RandomizerTest.cs similarity index 94% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RandomizerTest.cs index 25511c30b29..107c18f5904 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/RandomizerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RandomizerTest.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Http.Resilience.Internal; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; +namespace Microsoft.Extensions.Http.Resilience.Test.Internal; public class RandomizerTest { diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs new file mode 100644 index 00000000000..2ae0d60a69f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Moq; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internal; + +public class RequestMessageSnapshotStrategyTests +{ + [Fact] + public async Task SendAsync_EnsureSnapshotAttached() + { + var snapshot = new Mock(MockBehavior.Strict); + snapshot.Setup(s => s.Dispose()); + var cloner = new Mock(MockBehavior.Strict); + cloner.Setup(c => c.CreateSnapshot(It.IsAny())).Returns(snapshot.Object); + var strategy = new RequestMessageSnapshotStrategy(cloner.Object); + var context = ResilienceContext.Get(); + using var request = new HttpRequestMessage(); + context.Properties.Set(ResilienceKeys.RequestMessage, request); + + using var response = await strategy.ExecuteAsync( + context => + { + context.Properties.GetValue(ResilienceKeys.RequestSnapshot, null!).Should().Be(snapshot.Object); + return new ValueTask(new HttpResponseMessage()); + }, + context); + + cloner.VerifyAll(); + snapshot.VerifyAll(); + } + + [Fact] + public void ExecuteAsync_requestMessageNotFound_Throws() + { + var strategy = new RequestMessageSnapshotStrategy(Mock.Of()); + + strategy.Invoking(s => s.Execute(() => { })).Should().Throw(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RetryAfterHelperTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RetryAfterHelperTests.cs new file mode 100644 index 00000000000..8adbe9a1174 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RetryAfterHelperTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internal; + +public class RetryAfterHelperTests +{ + [Fact] + public void TryParse_Delta_Ok() + { + using var response = new HttpResponseMessage(); + response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10)); + + var parsed = RetryAfterHelper.TryParse(response, TimeProvider.System, out var retryAfter); + + parsed.Should().BeTrue(); + retryAfter.Should().Be(TimeSpan.FromSeconds(10)); + } + + [Fact] + public void TryParse_Null_Ok() + { + using var response = new HttpResponseMessage(); + + var parsed = RetryAfterHelper.TryParse(response, TimeProvider.System, out var retryAfter); + + parsed.Should().BeFalse(); + retryAfter.Should().Be(default); + } + + [Fact] + public void TryParse_Date_Ok() + { + var timeProvider = new FakeTimeProvider(); + + using var response = new HttpResponseMessage(); + response.Headers.RetryAfter = new RetryConditionHeaderValue(timeProvider.GetUtcNow() + TimeSpan.FromDays(1)); + + var parsed = RetryAfterHelper.TryParse(response, timeProvider, out var retryAfter); + + parsed.Should().BeTrue(); + retryAfter.Should().Be(TimeSpan.FromDays(1)); + } + + [Fact] + public void TryParse_DateInPast_Ok() + { + var timeProvider = new FakeTimeProvider(); + + using var response = new HttpResponseMessage(); + response.Headers.RetryAfter = new RetryConditionHeaderValue(timeProvider.GetUtcNow() + TimeSpan.FromDays(1)); + + timeProvider.Advance(TimeSpan.FromDays(2)); + var parsed = RetryAfterHelper.TryParse(response, timeProvider, out var retryAfter); + + parsed.Should().BeTrue(); + retryAfter.Should().Be(TimeSpan.Zero); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/ValidationHelperTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/ValidationHelperTests.cs new file mode 100644 index 00000000000..4f52e99461e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/ValidationHelperTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Polly.Retry; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internal; + +public class ValidationHelperTests +{ + [Fact] + public void GetAggregatedDelay_Constant_Ok() + { + ValidationHelper.GetAggregatedDelay( + new HttpRetryStrategyOptions + { + RetryCount = 10, + BackoffType = RetryBackoffType.Constant, + BaseDelay = TimeSpan.FromSeconds(1), + }) + .Should().Be(TimeSpan.FromSeconds(10)); + } + + [Fact] + public void GetAggregatedDelay_ExponentialWithJitter_ShouldNotBeRandomized() + { + var options = new HttpRetryStrategyOptions + { + RetryCount = 10, + BackoffType = RetryBackoffType.ExponentialWithJitter, + BaseDelay = TimeSpan.FromSeconds(1), + }; + + ValidationHelper + .GetAggregatedDelay(options) + .Should() + .Be(ValidationHelper.GetAggregatedDelay(options)); + } + + [Fact] + public void GetAggregatedDelay_Overflow_Handled() + { + var options = new HttpRetryStrategyOptions + { + RetryCount = 99, + BackoffType = RetryBackoffType.ExponentialWithJitter, + BaseDelay = TimeSpan.FromSeconds(1000), + }; + + ValidationHelper + .GetAggregatedDelay(options) + .Should() + .Be(TimeSpan.MaxValue); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs deleted file mode 100644 index 955c71d8dde..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerPolicyOptionsTests.cs +++ /dev/null @@ -1,147 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Resilience.Options; -using Polly; -using Polly.Timeout; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Polly; - -public class HttpCircuitBreakerPolicyOptionsTests -{ -#pragma warning disable S2330 - public static readonly IEnumerable HandledExceptionsClassified = new[] - { - new object[] { new InvalidCastException(), false }, - new object[] { new HttpRequestException(), true }, - new object[] { new TaskCanceledException(), false }, - new object[] { new TimeoutRejectedException(), true }, - }; - - private readonly HttpCircuitBreakerPolicyOptions _testObject; - - public HttpCircuitBreakerPolicyOptionsTests() - { - _testObject = new HttpCircuitBreakerPolicyOptions(); - } - - [Fact] - public void Constructor_ShouldInitialize() - { - var instance = new HttpCircuitBreakerPolicyOptions(); - Assert.NotNull(instance); - } - - [Fact] - public void ShouldHandleResultAsError_ShouldGetAndSet() - { - Predicate testValue = response => !response.IsSuccessStatusCode; - _testObject.ShouldHandleResultAsError = testValue; - - Assert.Equal(testValue, _testObject.ShouldHandleResultAsError); - } - - [Theory] - [InlineData(HttpStatusCode.OK, false)] - [InlineData(HttpStatusCode.BadRequest, false)] - [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] - [InlineData(HttpStatusCode.InternalServerError, true)] - [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] - [InlineData(HttpStatusCode.RequestTimeout, true)] - public void ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) - { - var response = new HttpResponseMessage { StatusCode = statusCode }; - var isTransientFailure = _testObject.ShouldHandleResultAsError(response); - Assert.Equal(expectedCondition, isTransientFailure); - response.Dispose(); - } - - [Fact] - public void ShouldHandleException_ShouldGetAndSet() - { - Predicate testValue = ex => ex is ArgumentNullException; - _testObject.ShouldHandleException = testValue; - - Assert.Equal(testValue, _testObject.ShouldHandleException); - } - - [Theory] - [MemberData(nameof(HandledExceptionsClassified))] - public void ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) - { - var shouldHandle = _testObject.ShouldHandleException(exception); - Assert.Equal(expectedToHandle, shouldHandle); - } - - [Fact] - public void OnCircuitBreak_ShouldGetAndSet() - { - Action> testValue = _ => { }; - _testObject.OnCircuitBreak = testValue; - - Assert.Equal(testValue, _testObject.OnCircuitBreak); - } - - [Fact] - public void OnCircuitReset_ShouldGetAndSet() - { - Action testValue = _ => { }; - _testObject.OnCircuitReset = testValue; - - Assert.Equal(testValue, _testObject.OnCircuitReset); - } - - [Theory] - [InlineData(HttpStatusCode.OK, false)] - [InlineData(HttpStatusCode.BadRequest, false)] - [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] - [InlineData(HttpStatusCode.InternalServerError, true)] - [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] - [InlineData(HttpStatusCode.RequestTimeout, true)] - public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) - { - var response = new HttpResponseMessage { StatusCode = statusCode }; - var isTransientFailure = new HttpCircuitBreakerPolicyOptions().ShouldHandleResultAsError(response); - Assert.Equal(expectedCondition, isTransientFailure); - response.Dispose(); - } - - [Theory] - [MemberData(nameof(HandledExceptionsClassified))] - public void ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle) - { - var shouldHandle = new HttpCircuitBreakerPolicyOptions().ShouldHandleException(exception); - Assert.Equal(expectedToHandle, shouldHandle); - } - - [Fact] - public void OnCircuitBreak_NoOp() - { - var options = new HttpCircuitBreakerPolicyOptions(); - var context = new Context(); - var expectedError = "Something went wrong"; - var delegateResult = new DelegateResult(new InvalidOperationException(expectedError)); - var args = new BreakActionArguments( - delegateResult, - context, - TimeSpan.FromSeconds(2), - CancellationToken.None); - Assert.Null(Record.Exception(() => options.OnCircuitBreak(args))); - } - - [Fact] - public void OnCircuitReset_NoOp() - { - var options = new HttpCircuitBreakerPolicyOptions { OnCircuitReset = (_) => { } }; - var context = new Context(); - - Assert.Null(Record.Exception(() => options.OnCircuitReset(new ResetActionArguments(context, CancellationToken.None)))); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerStrategyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerStrategyOptionsTests.cs new file mode 100644 index 00000000000..6db0ced4639 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpCircuitBreakerStrategyOptionsTests.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpCircuitBreakerStrategyOptionsTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true }, + new object[] { new TaskCanceledException(), false }, + new object[] { new TimeoutRejectedException(), true }, + }; + + private readonly HttpCircuitBreakerStrategyOptions _testObject; + private readonly ResilienceContext _context; + + public HttpCircuitBreakerStrategyOptionsTests() + { + _testObject = new HttpCircuitBreakerStrategyOptions(); + _context = ResilienceContext.Get(); + } + + [Fact] + public void Ctor_Defaults() + { + _testObject.BreakDuration.Should().Be(TimeSpan.FromSeconds(5)); + _testObject.FailureThreshold.Should().Be(0.1); + _testObject.SamplingDuration.Should().Be(TimeSpan.FromSeconds(30)); + _testObject.MinimumThroughput.Should().Be(100); + _testObject.ShouldHandle.Should().NotBeNull(); + _testObject.OnClosed.Should().BeNull(); + _testObject.OnOpened.Should().BeNull(); + _testObject.OnHalfOpened.Should().BeNull(); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public async Task ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + using var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = await _testObject.ShouldHandle(CreateArgs(response)); + Assert.Equal(expectedCondition, isTransientFailure); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = await _testObject.ShouldHandle(CreateArgs(exception)); + Assert.Equal(expectedToHandle, shouldHandle); + } + + private OutcomeArguments CreateArgs(Exception error) + => new(_context, Outcome.FromException(error), new CircuitBreakerPredicateArguments()); + + private OutcomeArguments CreateArgs(HttpResponseMessage response) + => new(_context, Outcome.FromResult(response), new CircuitBreakerPredicateArguments()); + +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs index 7e789ba2dfe..edf92a414d8 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpClientResiliencePredicatesTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using Polly; using Polly.Timeout; using Xunit; @@ -30,9 +31,12 @@ public void IsTransientHttpException_NullException_ShouldThrow() [Theory] [MemberData(nameof(HandledExceptionsClassified))] - public void IsTransientHttpException_Exception_ShouldClassify(Exception exceptions, bool expectedCondition) + public void IsTransientHttpException_Exception_ShouldClassify(Exception ex, bool expectedCondition) { - var isTransientHttpException = HttpClientResiliencePredicates.IsTransientHttpException(exceptions); + var isTransientHttpException = HttpClientResiliencePredicates.IsTransientHttpException(ex); + Assert.Equal(expectedCondition, isTransientHttpException); + + isTransientHttpException = HttpClientResiliencePredicates.IsTransientHttpOutcome(Outcome.FromException(ex)); Assert.Equal(expectedCondition, isTransientHttpException); } @@ -51,6 +55,10 @@ public void IsTransientFailure_ShouldClassify(HttpStatusCode statusCode, bool ex var response = new HttpResponseMessage { StatusCode = statusCode }; var isTransientFailure = HttpClientResiliencePredicates.IsTransientHttpFailure(response); Assert.Equal(expectedCondition, isTransientFailure); + + isTransientFailure = HttpClientResiliencePredicates.IsTransientHttpOutcome(Outcome.FromResult(response)); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRateLimiterStrategyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRateLimiterStrategyOptionsTests.cs new file mode 100644 index 00000000000..cdea65b3786 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRateLimiterStrategyOptionsTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.RateLimiting; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpRateLimiterStrategyOptionsTests +{ +#pragma warning disable S2330 + private readonly HttpRateLimiterStrategyOptions _testObject; + + public HttpRateLimiterStrategyOptionsTests() + { + _testObject = new HttpRateLimiterStrategyOptions(); + } + + [Fact] + public void Ctor_Defaults() + { + _testObject.DefaultRateLimiterOptions.Should().NotBeNull(); + _testObject.RateLimiter.Should().BeNull(); + _testObject.OnRejected.Should().BeNull(); + _testObject.DefaultRateLimiterOptions.QueueLimit.Should().Be(0); + _testObject.DefaultRateLimiterOptions.PermitLimit.Should().Be(1000); + _testObject.DefaultRateLimiterOptions.QueueProcessingOrder.Should().Be(QueueProcessingOrder.OldestFirst); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs deleted file mode 100644 index 96f92799cd8..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpResponseMessageExtensionsTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Globalization; -using System.Net.Http; -using Microsoft.Extensions.Http.Resilience.Internal; -using Microsoft.Extensions.Time.Testing; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Polly; - -public class HttpResponseMessageExtensionsTests -{ - private readonly TimeProvider _fakeClock = new FakeTimeProvider(); - - [Fact] - public void RetryAfter_WhenNoHeaderFound_ShouldReturnZero() - { - using var httpResponseMessage = new HttpResponseMessage(); - Assert.Equal(TimeSpan.Zero, RetryAfterHelper.ParseRetryAfterHeader(null!, _fakeClock)); - Assert.Equal(TimeSpan.Zero, RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock)); - } - - [Theory] - [InlineData(10)] - [InlineData(33)] - public void RetryAfter_WhenRelativeHeaderIsFound_ShouldReturnHeaderInterval(int seconds) - { - using var httpResponseMessage = new HttpResponseMessage(); - httpResponseMessage.Headers.Add("Retry-After", seconds.ToString(CultureInfo.InvariantCulture)); - var interval = RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock); - Assert.Equal(TimeSpan.FromSeconds(seconds), interval); - } - - [Theory] - [InlineData(10)] - [InlineData(33)] - public void RetryAfter_WhenAbsoluteHeaderIsFound_ShouldReturnHeaderInterval(int seconds) - { - using var httpResponseMessage = new HttpResponseMessage(); - httpResponseMessage.Headers.Add("Retry-After", (_fakeClock.GetUtcNow() + TimeSpan.FromSeconds(seconds)).ToString("r", CultureInfo.InvariantCulture)); - var interval = RetryAfterHelper.ParseRetryAfterHeader(httpResponseMessage, _fakeClock); - Assert.Equal(TimeSpan.FromSeconds(seconds), interval); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs deleted file mode 100644 index bb907889446..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryPolicyOptionTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using Microsoft.Extensions.Resilience.Options; -using Polly; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Polly; - -public class HttpRetryPolicyOptionTests -{ -#pragma warning disable S2330 - public static readonly IEnumerable HandledExceptionsClassified = new[] - { - new object[] { new InvalidCastException(), false }, - new object[] { new HttpRequestException(), true } - }; - - private readonly HttpRetryPolicyOptions _testClass; - - public HttpRetryPolicyOptionTests() - { - _testClass = new HttpRetryPolicyOptions(); - } - - [Fact] - public void ShouldHandleResultAsError_ShouldGetAndSet() - { - Predicate testValue = response => !response.IsSuccessStatusCode; - _testClass.ShouldHandleResultAsError = testValue; - - Assert.Equal(testValue, _testClass.ShouldHandleResultAsError); - } - - [Theory] - [InlineData(HttpStatusCode.OK, false)] - [InlineData(HttpStatusCode.BadRequest, false)] - [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] - [InlineData(HttpStatusCode.InternalServerError, true)] - [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] - [InlineData(HttpStatusCode.RequestTimeout, true)] - public void ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) - { - var response = new HttpResponseMessage { StatusCode = statusCode }; - var isTransientFailure = _testClass.ShouldHandleResultAsError(response); - Assert.Equal(expectedCondition, isTransientFailure); - response.Dispose(); - } - - [Fact] - public void ShouldHandleException_ShouldGetAndSet() - { - Predicate testValue = ex => ex is ArgumentNullException; - _testClass.ShouldHandleException = testValue; - - Assert.Equal(testValue, _testClass.ShouldHandleException); - } - - [Theory] - [MemberData(nameof(HandledExceptionsClassified))] - public void ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) - { - var shouldHandle = _testClass.ShouldHandleException(exception); - Assert.Equal(expectedToHandle, shouldHandle); - } - - [Theory] - [InlineData(HttpStatusCode.OK, false)] - [InlineData(HttpStatusCode.BadRequest, false)] - [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] - [InlineData(HttpStatusCode.InternalServerError, true)] - [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] - [InlineData(HttpStatusCode.RequestTimeout, true)] - public void ShouldHandleResultAsError_DefaultInstance_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) - { - var response = new HttpResponseMessage { StatusCode = statusCode }; - var isTransientFailure = new HttpRetryPolicyOptions().ShouldHandleResultAsError(response); - Assert.Equal(expectedCondition, isTransientFailure); - response.Dispose(); - } - - [Theory] - [MemberData(nameof(HandledExceptionsClassified))] - public void ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle) - { - var shouldHandle = new HttpRetryPolicyOptions().ShouldHandleException(exception); - Assert.Equal(expectedToHandle, shouldHandle); - } - - [Fact] - public void ShouldRetryAfterHeader_WhenNullHeader_ShouldReturnZero() - { - var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; - using var responseMessage = new HttpResponseMessage { }; - var delegateResult = new DelegateResult(responseMessage); - var result = options.RetryDelayGenerator != null - ? options.RetryDelayGenerator( - new RetryDelayArguments(delegateResult, new Context(), CancellationToken.None)) - : TimeSpan.Zero; - Assert.Equal(result, TimeSpan.Zero); - } - - [Fact] - public void ShouldRetryAfterHeader_WhenResponseContainsRetryAfterHeader_ShouldReturnTimeSpan() - { - var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; - using var responseMessage = new HttpResponseMessage - { - Headers = - { - RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10)) - } - }; - var args = new RetryDelayArguments( - new DelegateResult(responseMessage), - new Context(), - CancellationToken.None); - - var result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator(args) : TimeSpan.Zero; - Assert.Equal(result, TimeSpan.FromSeconds(10)); - } - - [Fact] - public void ShouldRetryAfterHeader_WhenResponseContainsNullHeader_ShouldReturnZero() - { - var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; - using var responseMessage = new HttpResponseMessage - { - }; - var delegateResult = new DelegateResult(responseMessage); - var result = options.RetryDelayGenerator != null - ? options.RetryDelayGenerator( - new RetryDelayArguments(delegateResult, new Context(), CancellationToken.None)) - : TimeSpan.Zero; - Assert.Equal(result, TimeSpan.Zero); - - result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator( - new RetryDelayArguments(null!, new Context(), CancellationToken.None)) - : TimeSpan.Zero; - Assert.Equal(result, TimeSpan.Zero); - } - - [Fact] - public void ShouldRetryAfterHeader_WhenDelegateHasException_ShouldReturnZero() - { - var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; - var args = new RetryDelayArguments( - new DelegateResult(new ArgumentNullException()), - new Context(), - CancellationToken.None); - - var result = options.RetryDelayGenerator!(args); - Assert.Equal(result, TimeSpan.Zero); - } - - [Fact] - public void ShouldRetryAfterHeader_WhenHeaderSetUsingAdd_ShouldReturnTimeSpan() - { - var options = new HttpRetryPolicyOptions { ShouldRetryAfterHeader = true }; - using var responseMessage = new HttpResponseMessage(); - responseMessage.Headers.Add("Retry-After", "10"); - var args = new RetryDelayArguments( - new DelegateResult(responseMessage), - new Context(), - CancellationToken.None); - - var result = options.RetryDelayGenerator != null ? options.RetryDelayGenerator(args) : TimeSpan.Zero; - Assert.Equal(result, TimeSpan.FromSeconds(10)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GetDelayGenerator_ShouldGetBasedOnShouldRetryAfterHeader(bool shouldRetryAfterHeader) - { - var options = new HttpRetryPolicyOptions - { - ShouldRetryAfterHeader = shouldRetryAfterHeader - }; - - Assert.Equal(shouldRetryAfterHeader, options.RetryDelayGenerator != null); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsTests.cs new file mode 100644 index 00000000000..470de412270 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpRetryStrategyOptionsTests.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using FluentAssertions; +using Polly; +using Polly.Retry; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpRetryStrategyOptionsTests +{ +#pragma warning disable S2330 + public static readonly IEnumerable HandledExceptionsClassified = new[] + { + new object[] { new InvalidCastException(), false }, + new object[] { new HttpRequestException(), true } + }; + + private readonly HttpRetryStrategyOptions _testClass; + + public HttpRetryStrategyOptionsTests() + { + _testClass = new HttpRetryStrategyOptions(); + } + + [Fact] + public void Ctor_Defaults() + { + var options = new HttpRetryStrategyOptions(); + + options.BackoffType.Should().Be(RetryBackoffType.ExponentialWithJitter); + options.RetryCount.Should().Be(3); + options.BaseDelay.Should().Be(TimeSpan.FromSeconds(2)); + options.ShouldHandle.Should().NotBeNull(); + options.OnRetry.Should().BeNull(); + options.ShouldRetryAfterHeader.Should().BeTrue(); + options.RetryDelayGenerator.Should().NotBeNull(); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public async Task ShouldHandleResultAsError_DefaultValue_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = await _testClass.ShouldHandle(CreateArgs(Outcome.FromResult(response))); + + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = await _testClass.ShouldHandle(CreateArgs(Outcome.FromException(exception))); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Theory] + [InlineData(HttpStatusCode.OK, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, false)] + [InlineData(HttpStatusCode.InternalServerError, true)] + [InlineData(HttpStatusCode.HttpVersionNotSupported, true)] + [InlineData(HttpStatusCode.RequestTimeout, true)] + public async Task ShouldHandleResultAsError_DefaultInstance_ShouldClassify(HttpStatusCode statusCode, bool expectedCondition) + { + var response = new HttpResponseMessage { StatusCode = statusCode }; + var isTransientFailure = await new HttpRetryStrategyOptions().ShouldHandle(CreateArgs(Outcome.FromResult(response))); + Assert.Equal(expectedCondition, isTransientFailure); + response.Dispose(); + } + + [Theory] + [MemberData(nameof(HandledExceptionsClassified))] + public async Task ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle) + { + var shouldHandle = await new HttpRetryStrategyOptions().ShouldHandle(CreateArgs(Outcome.FromException(exception))); + Assert.Equal(expectedToHandle, shouldHandle); + } + + [Fact] + public async Task ShouldRetryAfterHeader_InvalidOutcomes_ShouldReturnZero() + { + var options = new HttpRetryStrategyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage { }; + + Assert.NotNull(options.RetryDelayGenerator); + + var result = await options.RetryDelayGenerator( + new(ResilienceContext.Get(), + Outcome.FromResult(responseMessage), + new RetryDelayArguments(0, TimeSpan.Zero))); + Assert.Equal(result, TimeSpan.Zero); + + result = await options.RetryDelayGenerator( + new(ResilienceContext.Get(), + Outcome.FromResult(null), + new RetryDelayArguments(0, TimeSpan.Zero))); + Assert.Equal(result, TimeSpan.Zero); + + result = await options.RetryDelayGenerator( + new(ResilienceContext.Get(), + Outcome.FromException(new InvalidOperationException()), + new RetryDelayArguments(0, TimeSpan.Zero))); + Assert.Equal(result, TimeSpan.Zero); + } + + [Fact] + public async Task ShouldRetryAfterHeader_WhenResponseContainsRetryAfterHeader_ShouldReturnTimeSpan() + { + var options = new HttpRetryStrategyOptions { ShouldRetryAfterHeader = true }; + using var responseMessage = new HttpResponseMessage + { + Headers = + { + RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10)) + } + }; + + var result = await options.RetryDelayGenerator!( + new(ResilienceContext.Get(), + Outcome.FromResult(responseMessage), + new RetryDelayArguments(0, TimeSpan.Zero))); + + Assert.Equal(result, TimeSpan.FromSeconds(10)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetDelayGenerator_ShouldGetBasedOnShouldRetryAfterHeader(bool shouldRetryAfterHeader) + { + var options = new HttpRetryStrategyOptions + { + ShouldRetryAfterHeader = shouldRetryAfterHeader + }; + + Assert.Equal(shouldRetryAfterHeader, options.RetryDelayGenerator != null); + } + + private static OutcomeArguments CreateArgs(Outcome outcome) + => new(ResilienceContext.Get(), outcome, new RetryPredicateArguments(0)); + +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpTimeoutStrategyOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpTimeoutStrategyOptionsTests.cs new file mode 100644 index 00000000000..eaf40196f7c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Polly/HttpTimeoutStrategyOptionsTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Polly; + +public class HttpTimeoutStrategyOptionsTests +{ +#pragma warning disable S2330 + private readonly HttpTimeoutStrategyOptions _testObject; + + public HttpTimeoutStrategyOptionsTests() + { + _testObject = new HttpTimeoutStrategyOptions(); + } + + [Fact] + public void Ctor_Defaults() + { + _testObject.Timeout.Should().Be(TimeSpan.FromSeconds(30)); + _testObject.OnTimeout.Should().BeNull(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs similarity index 90% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs index c2dae0cf810..6cdfe0f88a0 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/DefaultRequestClonerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs @@ -9,24 +9,18 @@ using Microsoft.Extensions.Http.Resilience.Internal; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; #pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete public class DefaultRequestClonerTests { - private readonly DefaultRequestCloner _cloneHandler; + private readonly RequestCloner _cloneHandler; public DefaultRequestClonerTests() { - _cloneHandler = new DefaultRequestCloner(); - } - - [Fact] - public void CreateSnapshot_NullRequest_ShouldThrow() - { - Assert.Throws(() => _cloneHandler.CreateSnapshot(null!)); + _cloneHandler = new RequestCloner(); } [Fact] @@ -40,7 +34,7 @@ public void CreateSnapshot_StreamContent_ShouldThrow() }; var exception = Assert.Throws(() => _cloneHandler.CreateSnapshot(initialRequest)); - Assert.Equal("StreamContent content cannot by cloned using the DefaultRequestCloner.", exception.Message); + Assert.Equal("StreamContent content cannot by cloned using the RequestCloner.", exception.Message); initialRequest.Dispose(); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs new file mode 100644 index 00000000000..4d7bf3eb12f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; +using Moq; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + [InlineData(true, "https://dummy:21/path", "https://dummy:21")] + [InlineData(true, "https://dummy", "https://dummy")] + [InlineData(false, "https://dummy:21/path", "https://dummy:21")] + [InlineData(false, "https://dummy", "https://dummy")] + [Theory] + public void SelectStrategyByAuthority_Ok(bool standardResilience, string url, string expectedStrategyKey) + { + _builder.Services.AddFakeRedaction(); + + var pipelineName = standardResilience ? + _builder.AddStandardResilienceHandler().SelectStrategyByAuthority(DataClassification.Unknown).StrategyName : + _builder + .AddResilienceHandler("dummy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) + .SelectStrategyByAuthority(DataClassification.Unknown).StrategyName; + + var provider = _builder.Services.BuildServiceProvider().GetStrategyKeyProvider(pipelineName)!; + + using var request = new HttpRequestMessage(HttpMethod.Head, url); + + var key = provider.GetStrategyKey(request); + + Assert.Equal(expectedStrategyKey, key); + Assert.Same(provider.GetStrategyKey(request), provider.GetStrategyKey(request)); + } + + [Fact] + public void SelectStrategyByAuthority_Ok_NullURL_Throws() + { + _builder.Services.AddFakeRedaction(); + var builder = _builder.AddResilienceHandler("dummy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))).SelectStrategyByAuthority(DataClassification.Unknown); + var provider = StrategyKeyProviderHelper.GetStrategyKeyProvider(builder.Services.BuildServiceProvider(), builder.StrategyName)!; + + using var request = new HttpRequestMessage(); + + Assert.Throws(() => provider.GetStrategyKey(request)); + } + + [Fact] + public void SelectStrategyByAuthority_ErasingRedactor_InvalidOperationException() + { + _builder.Services.AddRedaction(); + var builder = _builder.AddResilienceHandler("dummy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))).SelectStrategyByAuthority(SimpleClassifications.PrivateData); + var provider = StrategyKeyProviderHelper.GetStrategyKeyProvider(builder.Services.BuildServiceProvider(), builder.StrategyName)!; + + using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); + + Assert.Throws(() => provider.GetStrategyKey(request)); + } + + [InlineData(true, "https://dummy:21/path", "https://")] + [InlineData(true, "https://dummy", "https://")] + [InlineData(false, "https://dummy:21/path", "https://")] + [InlineData(false, "https://dummy", "https://")] + [Theory] + public void SelectStrategyBy_Ok(bool standardResilience, string url, string expectedStrategyKey) + { + _builder.Services.AddFakeRedaction(); + + string? pipelineName = null; + + if (standardResilience) + { + pipelineName = _builder + .AddResilienceHandler("dummy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) + .SelectStrategyBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)).StrategyName; + } + else + { + pipelineName = _builder + .AddStandardResilienceHandler() + .SelectStrategyBy(_ => r => r.RequestUri!.GetLeftPart(UriPartial.Scheme)).StrategyName; + } + + var provider = _builder.Services.BuildServiceProvider().GetStrategyKeyProvider(pipelineName)!; + + using var request = new HttpRequestMessage(HttpMethod.Head, url); + + var key = provider.GetStrategyKey(request); + + Assert.Equal(expectedStrategyKey, key); + Assert.NotSame(provider.GetStrategyKey(request), provider.GetStrategyKey(request)); + } + + [InlineData(true, "https://dummy:21/path", "https://dummy:21")] + [InlineData(true, "https://dummy123", "https://dummy123")] + [InlineData(false, "https://dummy:21/path", "https://dummy:21")] + [InlineData(false, "https://dummy123", "https://dummy123")] + [Theory] + public async Task SelectStrategyBy_EnsureResilienceStrategyProviderCall(bool standardResilience, string url, string expectedStrategyKey) + { + var strategyProvider = new Mock>(MockBehavior.Strict); + + _builder.Services.AddFakeRedaction(); + _builder.Services.TryAddSingleton(strategyProvider.Object); + string? pipelineName = null; + if (standardResilience) + { + pipelineName = _builder + .AddResilienceHandler("dummy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) + .SelectStrategyByAuthority(DataClassification.None).StrategyName; + } + else + { + pipelineName = _builder + .AddStandardResilienceHandler() + .SelectStrategyByAuthority(DataClassification.None).StrategyName; + } + + _builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + + strategyProvider + .Setup(p => p.GetStrategy(new HttpKey(pipelineName, expectedStrategyKey))) + .Returns(NullResilienceStrategy.Instance); + + await CreateClient().GetAsync(url); + + strategyProvider.VerifyAll(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs new file mode 100644 index 00000000000..b7fd2d76839 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Compliance.Classification; +using Microsoft.Extensions.Compliance.Redaction; +using Microsoft.Extensions.Compliance.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Resilience; +using Microsoft.Extensions.Resilience.Internal; +using Microsoft.Extensions.Telemetry.Metering; +using Moq; +using Polly; +using Polly.Registry; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test; + +public sealed partial class HttpClientBuilderExtensionsTests +{ + [Fact] + public void AddResilienceHandler_ArgumentValidation() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + + Assert.Throws(() => builder.AddResilienceHandler(null!, _ => { })); + Assert.Throws(() => builder.AddResilienceHandler(string.Empty, _ => { })); + Assert.Throws(() => builder.AddResilienceHandler(null!, (_, _) => { })); + Assert.Throws(() => builder.AddResilienceHandler(string.Empty, (_, _) => { })); + Assert.Throws(() => builder.AddResilienceHandler("dummy", (Action>)null!)); + Assert.Throws(() => builder.AddResilienceHandler("dummy", (Action, ResilienceHandlerContext>)null!)); + + builder = null; + Assert.Throws(() => builder!.AddResilienceHandler("pipeline-name", _ => { })); + Assert.Throws(() => builder!.AddResilienceHandler("pipeline-name", (_, _) => { })); + } + + [Fact] + public void AddResilienceHandler_EnsureCorrectServicesRegistered() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + + builder.AddResilienceHandler("test", ConfigureBuilder); + + // add twice intentionally + builder.AddResilienceHandler("test", ConfigureBuilder); + + Assert.Contains(services, s => s.ServiceType == typeof(ResilienceStrategyProvider)); + } + + [Fact] + public void AddResilienceHandler_EnsureServicesNotAddedTwice() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + + builder.AddResilienceHandler("test", ConfigureBuilder); + var count = builder.Services.Count; + + // add twice intentionally + builder.AddResilienceHandler("test", ConfigureBuilder); + + builder.Services.Should().HaveCount(count + 2); + } + + [Fact] + public void AddResilienceHandler_EnsureFailureResultContext() + { + var serviceProvider = new ServiceCollection().AddHttpClient("client").AddResilienceHandler("test", ConfigureBuilder).Services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>>().Value; + + using var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + var context = options.GetContextFromResult(response); + + context.FailureReason.Should().Be("500"); + context.AdditionalInformation.Should().Be("InternalServerError"); + context.FailureSource.Should().Be(TelemetryConstants.Unknown); + + options.GetContextFromResult(null!).FailureReason.Should().Be(TelemetryConstants.Unknown); + } + + [Fact] + public async Task AddResilienceHandler_EnsureResilienceHandlerContext() + { + var verified = false; + _builder + .AddResilienceHandler("test", (_, context) => + { + context.ServiceProvider.Should().NotBeNull(); + context.BuilderName.Should().Be($"{BuilderName}-test"); + context.StrategyKey.Should().Be("dummy-key"); + verified = true; + }) + .SelectStrategyBy(_ => _ => "dummy-key"); + + _builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.InternalServerError)); + + await CreateClient(BuilderName).GetAsync("https://dummy"); + verified.Should().BeTrue(); + } + + [Fact] + public void AddResilienceHandler_EnsureCorrectRegistryOptions() + { + var services = new ServiceCollection(); + IHttpClientBuilder? builder = services.AddHttpClient("client"); + builder.AddResilienceHandler("test", ConfigureBuilder); + + var registryOptions = builder.Services.BuildServiceProvider().GetRequiredService>>().Value; + registryOptions.BuilderComparer.Equals(new HttpKey("A", "1"), new HttpKey("A", "2")).Should().BeTrue(); + registryOptions.BuilderComparer.Equals(new HttpKey("A", "1"), new HttpKey("B", "1")).Should().BeFalse(); + + registryOptions.StrategyComparer.Equals(new HttpKey("A", "1"), new HttpKey("A", "1")).Should().BeTrue(); + registryOptions.StrategyComparer.Equals(new HttpKey("A", "1"), new HttpKey("A", "2")).Should().BeFalse(); + + registryOptions.BuilderNameFormatter(new HttpKey("A", "1")).Should().Be("A"); + registryOptions.StrategyKeyFormatter(new HttpKey("A", "1")).Should().Be("1"); + } + + public enum PolicyType + { + Fallback, + Retry, + CircuitBreaker, + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task AddResilienceHandler_EnsureProperStrategyInstanceRetrieved(bool bySelector) + { + // arrange + var resilienceProvider = new Mock>(MockBehavior.Strict); + var services = new ServiceCollection().AddLogging().RegisterMetering().AddFakeRedaction(); + services.AddSingleton(resilienceProvider.Object); + var builder = services.AddHttpClient("client"); + var pipelineBuilder = builder.AddResilienceHandler("dummy", ConfigureBuilder); + var expectedStrategyName = "client-dummy"; + if (bySelector) + { + pipelineBuilder.SelectStrategyByAuthority(DataClassification.Unknown); + } + + builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + + var provider = services.BuildServiceProvider(); + if (bySelector) + { + resilienceProvider + .Setup(v => v.GetStrategy(new HttpKey(expectedStrategyName, "https://dummy1"))) + .Returns(NullResilienceStrategy.Instance); + } + else + { + resilienceProvider + .Setup(v => v.GetStrategy(new HttpKey(expectedStrategyName, string.Empty))) + .Returns(NullResilienceStrategy.Instance); + } + + var client = provider.GetRequiredService().CreateClient("client"); + + // act + await client.GetAsync("https://dummy1"); + + // assert + resilienceProvider.VerifyAll(); + } + + [Fact] + public async Task AddResilienceHandlerBySelector_EnsureResilienceStrategyProviderCalled() + { + // arrange + var services = new ServiceCollection().AddLogging().RegisterMetering(); + var providerMock = new Mock>(MockBehavior.Strict); + + services.AddSingleton(providerMock.Object); + var pipelineName = string.Empty; + + pipelineName = "client-my-pipeline"; + var clientBuilder = services.AddHttpClient("client"); + clientBuilder.AddResilienceHandler("my-pipeline", ConfigureBuilder); + clientBuilder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.OK)); + + providerMock + .Setup(v => v.GetStrategy(new HttpKey(pipelineName, string.Empty))) + .Returns(NullResilienceStrategy.Instance) + .Verifiable(); + + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("client"); + var pipelineProvider = provider.GetRequiredService>(); + + // act + await client.GetAsync("https://dummy1"); + + // assert + providerMock.VerifyAll(); + } + + [Fact] + public void AddResilienceHandler_AuthoritySelectorAndNotConfiguredRedaction_EnsureValidated() + { + // arrange + var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() + .AddHttpClient("my-client") + .AddResilienceHandler("my-pipeline", ConfigureBuilder) + .SelectStrategyByAuthority(SimpleClassifications.PrivateData); + + var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); + + var error = Assert.Throws(() => factory.CreateClient("my-client")); + Assert.Equal("The redacted strategy key is an empty string and cannot be used for the strategy selection. Is redaction correctly configured?", error.Message); + } + + [Fact] + public void AddResilienceHandler_AuthorityByCustomSelector_NotValidated() + { + // arrange + var clientBuilder = new ServiceCollection().AddLogging().RegisterMetering().AddRedaction() + .AddHttpClient("my-client") + .AddResilienceHandler("my-pipeline", ConfigureBuilder) + .SelectStrategyBy(_ => _ => string.Empty); + + var factory = clientBuilder.Services.BuildServiceProvider().GetRequiredService(); + + Assert.NotNull(factory.CreateClient("my-client")); + } + + private void ConfigureBuilder(ResilienceStrategyBuilder builder) => builder.AddTimeout(TimeSpan.FromSeconds(1)); +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs similarity index 65% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs index 847c3074660..19f6e088ecc 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/HttpClientBuilderExtensionsTests.Standard.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs @@ -3,14 +3,20 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Resilience; using Microsoft.Extensions.Telemetry.Metering; +using Polly; +using Polly.Registry; +using Polly.Retry; using Xunit; namespace Microsoft.Extensions.Http.Resilience.Test; @@ -27,13 +33,15 @@ public HttpClientBuilderExtensionsTests() _builder.Services.AddLogging(); } + private HttpClient CreateClient(string name = BuilderName) => _builder.Services.BuildServiceProvider().GetRequiredService().CreateClient(name); + private static readonly IConfigurationSection _validConfigurationSection = ConfigurationStubFactory.Create( new Dictionary { { "StandardResilienceOptions:CircuitBreakerOptions:FailureThreshold", "0.1"}, - { "StandardResilienceOptions:AttemptTimeoutOptions:TimeoutInterval", "00:00:05"}, - { "StandardResilienceOptions:TotalRequestTimeoutOptions:TimeoutInterval", "00:00:20"}, + { "StandardResilienceOptions:AttemptTimeoutOptions:Timeout", "00:00:05"}, + { "StandardResilienceOptions:TotalRequestTimeoutOptions:Timeout", "00:00:20"}, }) .GetSection("StandardResilienceOptions"); @@ -114,17 +122,44 @@ public void AddStandardResilienceHandler_ConfigurationPropertyWithTypo_Throws(Me AddStandardResilienceHandler(mode, builder, _invalidConfigurationSection, options => { }); - var provider = builder.Services.BuildServiceProvider().GetRequiredService(); #if NET8_0_OR_GREATER // Whilst these API are marked as NET6_0_OR_GREATER we don't build .NET 6.0, // and as such the API is available in .NET 8 onwards. - Assert.Throws(() => provider.GetPipeline($"test-standard")); + Assert.Throws(() => HttpClientBuilderExtensionsTests.GetStrategy(builder.Services, $"test-standard")); #else - var pipeline = provider.GetPipeline($"test-standard"); - Assert.NotNull(pipeline); + GetStrategy(builder.Services, $"test-standard").Should().NotBeNull(); #endif } + [Fact] + public void AddStandardResilienceHandler_EnsureCorrectStrategies() + { + var called = false; + var builder = new ServiceCollection().AddLogging().RegisterMetering().AddHttpClient("test"); + builder.Services.TryAddTransient(_ => + { + return new ResilienceStrategyBuilder + { + OnCreatingStrategy = strategies => + { + strategies.Should().HaveCount(5); + strategies[0].GetType().Name.Should().Contain("RateLimiter"); + strategies[1].GetType().Name.Should().Contain("Timeout"); + strategies[2].GetType().Name.Should().Contain("Retry"); + strategies[3].GetType().Name.Should().Contain("CircuitBreaker"); + strategies[4].GetType().Name.Should().Contain("Timeout"); + + called = true; + } + }; + }); + + builder.AddStandardResilienceHandler(); + + _ = builder.Services.BuildServiceProvider().GetRequiredService().CreateClient("test"); + called.Should().BeTrue(); + } + [InlineData(true)] [InlineData(false)] [Theory] @@ -136,18 +171,16 @@ public void AddStandardResilienceHandler_EnsureValidated(bool wholePipeline) { if (wholePipeline) { - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); - options.AttemptTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(2); + options.AttemptTimeoutOptions.Timeout = TimeSpan.FromSeconds(1); } else { - options.BulkheadOptions.MaxQueuedActions = -1; + options.RetryOptions.RetryCount = -3; } }); - var provider = builder.Services.BuildServiceProvider().GetRequiredService(); - - Assert.Throws(() => provider.GetPipeline($"test-standard")); + Assert.Throws(() => GetStrategy(builder.Services, $"test-standard")); } [InlineData(MethodArgs.None)] @@ -165,12 +198,46 @@ public void AddStandardResilienceHandler_EnsureConfigured(MethodArgs mode) AddStandardResilienceHandler(mode, builder, _validConfigurationSection, options => { }); - var provider = builder.Services.BuildServiceProvider().GetRequiredService(); - - var pipeline = provider.GetPipeline($"test-standard"); + var pipeline = GetStrategy(builder.Services, $"test-standard"); Assert.NotNull(pipeline); } + [Fact] + public async Task DynamicReloads_Ok() + { + // arrange + var requests = new List(); + var config = ConfigurationStubFactory.Create( + new() + { + { "standard:RetryOptions:RetryCount", "6" } + }, + out var reloadAction).GetSection("standard"); + + _builder.AddStandardResilienceHandler().Configure(config).Configure(options => + { + options.RetryOptions.BaseDelay = TimeSpan.Zero; + options.RetryOptions.BackoffType = RetryBackoffType.Constant; + }); + _builder.AddHttpMessageHandler(() => new TestHandlerStub((r, _) => + { + requests.Add(r); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)); + })); + + var client = CreateClient(); + + // act && assert + await client.GetAsync("https://dummy"); + requests.Should().HaveCount(7); + + requests.Clear(); + reloadAction(new() { { "standard:RetryOptions:RetryCount", "10" } }); + + await client.GetAsync("https://dummy"); + requests.Should().HaveCount(11); + } + private static void AddStandardResilienceHandler( MethodArgs mode, IHttpClientBuilder builder, @@ -194,4 +261,11 @@ private static void AddStandardResilienceHandler( _ => throw new NotSupportedException() }; } + + private static ResilienceStrategy GetStrategy(IServiceCollection services, string name) + { + var provider = services.BuildServiceProvider().GetRequiredService>(); + + return provider.GetStrategy(new HttpKey(name, string.Empty)); + } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpRequestMessageExtensionsTests.cs similarity index 96% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpRequestMessageExtensionsTests.cs index 2440b52bc14..ca1ea42a2a7 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/HttpRequestMessageExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpRequestMessageExtensionsTests.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Http.Resilience.Internal; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; #pragma warning disable CA2000 // Test class diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs similarity index 73% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs index 77d5d910a78..84b93e700a4 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/Validators/HttpStandardResilienceOptionsCustomValidatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsCustomValidatorTests.cs @@ -5,12 +5,16 @@ using System.Collections.Generic; #if NET8_0_OR_GREATER using System.Linq; +using Microsoft; +using Microsoft.Extensions; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Http.Resilience; #endif using Microsoft.Extensions.Http.Resilience.Internal.Validators; -using Microsoft.Extensions.Resilience.Options; +using Polly.Retry; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Internals.Validators; +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; public class HttpStandardResilienceOptionsCustomValidatorTests { [Fact] @@ -18,7 +22,7 @@ public void Validate_InvalidOptions_EnsureValidationErrors() { HttpStandardResilienceOptions options = new(); options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromSeconds(1); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(1); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(1); var validationResult = new HttpStandardResilienceOptionsCustomValidator().Validate(string.Empty, options); @@ -46,19 +50,19 @@ public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data get { var options = new HttpStandardResilienceOptions(); - options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; - options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * 2); + options.AttemptTimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.Timeout.TotalMilliseconds * 2); yield return new object[] { options }; options = new HttpStandardResilienceOptions(); - options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; - options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); + options.AttemptTimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.Timeout.TotalMilliseconds * 2) + TimeSpan.FromMilliseconds(10); yield return new object[] { options }; options = new HttpStandardResilienceOptions(); options.RetryOptions.RetryCount = 1; - options.RetryOptions.BackoffType = BackoffType.Linear; - options.RetryOptions.BaseDelay = options.TotalRequestTimeoutOptions.TimeoutInterval; + options.RetryOptions.BackoffType = RetryBackoffType.Linear; + options.RetryOptions.BaseDelay = options.TotalRequestTimeoutOptions.Timeout; yield return new object[] { options }; } } @@ -77,12 +81,12 @@ public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data get { var options = new HttpStandardResilienceOptions(); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); - options.AttemptTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(3); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(2); + options.AttemptTimeoutOptions.Timeout = TimeSpan.FromSeconds(3); yield return new object[] { options }; options = new HttpStandardResilienceOptions(); - options.TotalRequestTimeoutOptions.TimeoutInterval = TimeSpan.FromSeconds(2); + options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(2); yield return new object[] { options }; options = new HttpStandardResilienceOptions(); @@ -90,8 +94,8 @@ public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data yield return new object[] { options }; options = new HttpStandardResilienceOptions(); - options.AttemptTimeoutOptions.TimeoutInterval = options.TotalRequestTimeoutOptions.TimeoutInterval; - options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.TimeoutInterval.TotalMilliseconds / 2); + options.AttemptTimeoutOptions.Timeout = options.TotalRequestTimeoutOptions.Timeout; + options.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromMilliseconds(options.AttemptTimeoutOptions.Timeout.TotalMilliseconds / 2); yield return new object[] { options }; } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsTests.cs new file mode 100644 index 00000000000..b0ce1d0a389 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpStandardResilienceOptionsTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; + +public class HttpStandardResilienceOptionsTests +{ + private readonly HttpStandardResilienceOptions _options; + + public HttpStandardResilienceOptionsTests() + { + _options = new HttpStandardResilienceOptions(); + } + + [Fact] + public void Ctor_EnsureDefaults() + { + _options.AttemptTimeoutOptions.Timeout.Should().Be(TimeSpan.FromSeconds(10)); + _options.TotalRequestTimeoutOptions.Timeout.Should().Be(TimeSpan.FromSeconds(30)); + + _options.TotalRequestTimeoutOptions.StrategyName.Should().Be("Standard-TotalRequestTimeout"); + _options.RateLimiterOptions.StrategyName.Should().Be("Standard-RateLimiter"); + _options.RetryOptions.StrategyName.Should().Be("Standard-Retry"); + _options.CircuitBreakerOptions.StrategyName.Should().Be("Standard-CircuitBreaker"); + _options.AttemptTimeoutOptions.StrategyName.Should().Be("Standard-AttemptTimeout"); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs new file mode 100644 index 00000000000..bbf98fae0d3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Test.Helpers; +using Microsoft.Extensions.Http.Telemetry; +using Microsoft.Extensions.Telemetry; +using Polly; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Internals; + +public class ResilienceHandlerTest +{ + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureRequestMetadataFlows(bool resilienceContextSet) + { + using var handler = new ResilienceHandler(_ => NullResilienceStrategy.Instance); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + request.SetRequestMetadata(new RequestMetadata()); + + if (resilienceContextSet) + { + request.SetResilienceContext(ResilienceContext.Get()); + } + + handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); + + await invoker.SendAsync(request, default); + + if (resilienceContextSet) + { + request.GetResilienceContext()! + .Properties + .GetValue(new ResiliencePropertyKey(TelemetryConstants.RequestMetadataKey), null!) + .Should() + .NotBeNull(); + } + else + { + request.GetResilienceContext().Should().BeNull(); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) + { + using var handler = new ResilienceHandler(_ => NullResilienceStrategy.Instance); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + if (executionContextSet) + { + request.SetResilienceContext(ResilienceContext.Get()); + } + + handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); + + await invoker.SendAsync(request, default); + + if (executionContextSet) + { + Assert.NotNull(request.GetResilienceContext()); + } + else + { + Assert.Null(request.GetResilienceContext()); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task SendAsync_EnsureInvoker(bool executionContextSet) + { + using var handler = new ResilienceHandler(_ => NullResilienceStrategy.Instance); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + if (executionContextSet) + { + request.SetResilienceContext(ResilienceContext.Get()); + } + + handler.InnerHandler = new TestHandlerStub((r, _) => + { + r.GetResilienceContext().Should().NotBeNull(); + r.GetResilienceContext()!.Properties.GetValue(ResilienceKeys.RequestMessage, null!).Should().BeSameAs(r); + + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); + }); + + var response = await invoker.SendAsync(request, default); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + [Fact] + public async Task SendAsync_EnsureCancellationTokenFlowsToResilienceContext() + { + using var source = new CancellationTokenSource(); + using var handler = new ResilienceHandler(_ => NullResilienceStrategy.Instance); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + handler.InnerHandler = new TestHandlerStub((_, cancellationToken) => + { + cancellationToken.Should().Be(source.Token); + + return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); + }); + + var response = await invoker.SendAsync(request, source.Token); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + [Fact] + public async Task SendAsync_Exception_EnsureRethrown() + { + using var handler = new ResilienceHandler(_ => NullResilienceStrategy.Instance); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(); + + handler.InnerHandler = new TestHandlerStub((_, _) => throw new InvalidOperationException()); + + await invoker.Invoking(i => i.SendAsync(request, default)).Should().ThrowAsync(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/StrategyNameHelperTest.cs similarity index 57% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/StrategyNameHelperTest.cs index fb930606de4..5f84ff8704c 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Core/Internal/PipelineNameHelperTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/StrategyNameHelperTest.cs @@ -4,12 +4,12 @@ using Microsoft.Extensions.Http.Resilience.Internal; using Xunit; -namespace Microsoft.Extensions.Http.Resilience.Test.Internals; -public class PipelineNameHelperTest +namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; +public class StrategyNameHelperTest { [Fact] public void GetPipelineName_Ok() { - Assert.Equal("client-pipeline", PipelineNameHelper.GetPipelineName("client", "pipeline")); + Assert.Equal("client-pipeline", StrategyNameHelper.GetName("client", "pipeline")); } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/DefaultRoutingStrategyFactoryTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/DefaultRoutingStrategyFactoryTests.cs deleted file mode 100644 index 29a8572b91d..00000000000 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/DefaultRoutingStrategyFactoryTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience.Routing.Internal; -using Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; -using Moq; -using Xunit; - -namespace Microsoft.Extensions.Http.Resilience.Test.Routing; - -public sealed class DefaultRoutingStrategyFactoryTests : IDisposable -{ - private const string ClientName = "clientName"; - private readonly Uri _routeUri = new("https://bing.com"); - private readonly Mock _mockService = new(); - public void Dispose() - { - _mockService.VerifyAll(); - } - - [Fact] - public void CreateRoutingStrategy_ShouldCreateRoutingStrategyAndPassTheName() - { - var serviceCollection = new ServiceCollection(); - _mockService.Setup(s => s.Route).Returns(_routeUri); - - serviceCollection.AddSingleton(_mockService.Object); - using var provider = serviceCollection.BuildServiceProvider(); - var factory = new DefaultRoutingStrategyFactory(ClientName, provider); - - var routingStrategy = factory.CreateRoutingStrategy(); - Uri? resultRouteUri = null; - routingStrategy?.TryGetNextRoute(out resultRouteUri); - - var mockRoutingStrategy = routingStrategy as MockRoutingStrategy; - - Assert.NotNull(mockRoutingStrategy); - - Assert.Equal(ClientName, mockRoutingStrategy?.Name); - Assert.Equal(_routeUri, resultRouteUri!); - } - - [Fact] - public void CreateRoutingStrategy_WhenServiceIsNotInjectedShouldThrow() - { - var serviceCollection = new ServiceCollection(); - using var provider = serviceCollection.BuildServiceProvider(); - - var factory = new DefaultRoutingStrategyFactory(ClientName, provider); - - Assert.Throws(() => - { - factory.CreateRoutingStrategy(); - }); - } -} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/EndpointTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/EndpointTests.cs new file mode 100644 index 00000000000..d85bc9e7937 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/EndpointTests.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Routing; + +public class EndpointTests +{ + [Fact] + public void Uri_OK() + { + var endpoint = new Endpoint + { + Uri = new Uri("https://localhost:5001") + }; + + endpoint.Uri.Should().Be(new Uri("https://localhost:5001")); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/IStubRoutingService.cs similarity index 75% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/IStubRoutingService.cs index d6e7ab85efb..392848584f3 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/IStubRoutingService.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/IStubRoutingService.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; +namespace Microsoft.Extensions.Http.Resilience.Test.Routing; public interface IStubRoutingService { diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs similarity index 91% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs index ce934adbd36..40f207ea122 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/Internal/MockRoutingStrategy.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Extensions.Http.Resilience.Test.Hedging.Internals; +namespace Microsoft.Extensions.Http.Resilience.Test.Routing; // Can't use NotNullWhenAttribute since it's defined in two reference assemblies with InternalVisibleTo #pragma warning disable CS8767 diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs index 17eb890e56b..eefde7a8bdd 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs @@ -76,7 +76,7 @@ protected override IEnumerable ConfigureMinRoutes(IRoutingStrategyBuilde yield return "https://dummy-route/"; } - protected override IRequestRoutingStrategy CreateEmptyStrategy() => new OrderedGroupsRoutingStrategy(Mock.Of()); + internal override IRequestRoutingStrategy CreateEmptyStrategy() => new OrderedGroupsRoutingStrategy(Mock.Of()); protected override IEnumerable> ConfigureInvalidRoutes() { diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs new file mode 100644 index 00000000000..d5ccfe22ffb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using FluentAssertions; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; +using Moq; +using Xunit; + +namespace Microsoft.Extensions.Http.Resilience.Test.Routing; + +public class RoutingResilienceStrategyTests +{ + [Fact] + public void NoRequestMessage_Throws() + { + RoutingResilienceStrategy strategy = new RoutingResilienceStrategy(Mock.Of()); + + strategy.Invoking(s => s.Execute(() => { })).Should().Throw().WithMessage("The HTTP request message was not found in the resilience context."); + } +} diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs index ddb161279fb..168da3a211f 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs @@ -55,7 +55,7 @@ public void CreateStrategy_EnsurePooled() { var strategy = factory.CreateRoutingStrategy(); strategies.Add(strategy); - ((IPooledRequestRoutingStrategyFactory)factory).ReturnRoutingStrategy(strategy); + factory.ReturnRoutingStrategy(strategy); } // assert that some strategies were pooled @@ -154,9 +154,9 @@ internal void StrategyResultHelper(params string[] expectedUrls) CollectUrls(factory.CreateRoutingStrategy()).Should().Equal(expectedUrls); } - protected IRequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name).CreateRoutingStrategy(); + internal IRequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name).CreateRoutingStrategy(); - protected IRequestRoutingStrategyFactory CreateRoutingFactory(string? name = null) => Builder.Services.BuildServiceProvider().GetRoutingFactory(name ?? Builder.Name); + internal IRequestRoutingStrategyFactory CreateRoutingFactory(string? name = null) => Builder.Services.BuildServiceProvider().GetRoutingFactory(name ?? Builder.Name); private static IEnumerable CollectUrls(IRequestRoutingStrategy strategy) { @@ -177,7 +177,7 @@ protected static IConfigurationSection GetSection(IDictionary va protected abstract IEnumerable> ConfigureInvalidRoutes(); - protected abstract IRequestRoutingStrategy CreateEmptyStrategy(); + internal abstract IRequestRoutingStrategy CreateEmptyStrategy(); protected void SetupRandomizer(double result) => Randomizer.Setup(r => r.NextDouble(It.IsAny())).Returns(result); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs index b9a706425a4..4942f3e5977 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs @@ -160,7 +160,7 @@ protected override IEnumerable> ConfigureInvalid }); } - protected override IRequestRoutingStrategy CreateEmptyStrategy() => new WeightedGroupsRoutingStrategy(Mock.Of()); + internal override IRequestRoutingStrategy CreateEmptyStrategy() => new WeightedGroupsRoutingStrategy(Mock.Of()); private static WeightedEndpointGroup CreateGroup(params string[] endpoints) {