Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt Polly V8 in Microsoft.Extensions.Http.Resilience #4108

Merged
merged 16 commits into from
Jun 27, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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<NoRemoteCallHandler>();

int routes = clientType.HasFlag(HedgingClientType.ManyRoutes) ? 50 : 2;
Expand Down
8 changes: 4 additions & 4 deletions eng/Packages/General.props
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
<PackageVersion Include="OpenTelemetry" Version="1.4.0" />
<PackageVersion Include="Polly.Contrib.Simmy" Version="0.3.0" />
<PackageVersion Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageVersion Include="Polly" Version="8.0.0-alpha.1" />
<PackageVersion Include="Polly.Core" Version="8.0.0-alpha.1" />
<PackageVersion Include="Polly.Extensions" Version="8.0.0-alpha.1" />
<PackageVersion Include="Polly.RateLimiting" Version="8.0.0-alpha.1" />
<PackageVersion Include="Polly" Version="8.0.0-alpha.4" />
<PackageVersion Include="Polly.Core" Version="8.0.0-alpha.4" />
<PackageVersion Include="Polly.Extensions" Version="8.0.0-alpha.4" />
<PackageVersion Include="Polly.RateLimiting" Version="8.0.0-alpha.4" />
<PackageVersion Include="protobuf-net" Version="3.0.101" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
namespace Microsoft.Extensions.Http.Resilience.FaultInjection;

/// <summary>
/// Provides extension methods for <see cref="Polly.Context"/>.
/// Provides extension methods for <see cref="Context"/>.
/// </summary>
[Experimental]
public static class PolicyContextExtensions
public static class ContextExtensions
{
private const string CallingRequestMessage = "CallingRequestMessage";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<IChaosPolicyFactory>();
var httpClientChaosPolicyFactory = services.GetRequiredService<IHttpClientChaosPolicyFactory>();
_ = pipelineBuilder
.AddPolicy(httpClientChaosPolicyFactory.CreateHttpResponsePolicy())
.AddPolicy(chaosPolicyFactory.CreateExceptionPolicy())
.AddPolicy(chaosPolicyFactory.CreateLatencyPolicy<HttpResponseMessage>());
});
return httpClientBuilder.AddHttpMessageHandler(serviceProvider =>
{
var chaosPolicyFactory = serviceProvider.GetRequiredService<IChaosPolicyFactory>();
var httpClientChaosPolicyFactory = serviceProvider.GetRequiredService<IHttpClientChaosPolicyFactory>();

return httpClientBuilder;
var policy = Policy.WrapAsync(
chaosPolicyFactory.CreateLatencyPolicy<HttpResponseMessage>(),
chaosPolicyFactory.CreateExceptionPolicy().AsAsyncPolicy<HttpResponseMessage>(),
httpClientChaosPolicyFactory.CreateHttpResponsePolicy());

return new PolicyHttpMessageHandler(policy);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// It is using three chained layers in this order (from the outermost to the innermost): Bulkhead -> Circuit Breaker -> Attempt Timeout.
/// </remarks>
public class HedgingEndpointOptions
{
private static readonly TimeSpan _timeoutInterval = TimeSpan.FromSeconds(10);

/// <summary>
/// Gets or sets the bulkhead options for the endpoint.
/// </summary>
/// <remarks>
/// By default it is initialized with a unique instance of <see cref="HttpBulkheadPolicyOptions"/> using default properties values.
/// By default it is initialized with a unique instance of <see cref="HttpRateLimiterStrategyOptions"/> using default properties values.
/// </remarks>
[Required]
[ValidateObjectMembers]
public HttpBulkheadPolicyOptions BulkheadOptions { get; set; } = new();
public HttpRateLimiterStrategyOptions RateLimiterOptions { get; set; } = new HttpRateLimiterStrategyOptions
{
StrategyName = StandardHedgingStrategyNames.RateLimiter
};

/// <summary>
/// Gets or sets the circuit breaker options for the endpoint.
/// </summary>
/// <remarks>
/// By default it is initialized with a unique instance of <see cref="HttpCircuitBreakerPolicyOptions"/> using default properties values.
/// By default it is initialized with a unique instance of <see cref="HttpCircuitBreakerStrategyOptions"/> using default properties values.
/// </remarks>
[Required]
[ValidateObjectMembers]
public HttpCircuitBreakerPolicyOptions CircuitBreakerOptions { get; set; } = new();
public HttpCircuitBreakerStrategyOptions CircuitBreakerOptions { get; set; } = new HttpCircuitBreakerStrategyOptions
{
StrategyName = StandardHedgingStrategyNames.CircuitBreaker
};

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// By default it is initialized with a unique instance of <see cref="HttpTimeoutPolicyOptions"/>
/// using a custom <see cref="TimeoutPolicyOptions.TimeoutInterval"/> of 10 seconds.
/// By default it is initialized with a unique instance of <see cref="HttpTimeoutStrategyOptions"/>
/// using a custom <see cref="TimeoutStrategyOptions.Timeout"/> of 10 seconds.
/// </remarks>
[Required]
[ValidateObjectMembers]
public HttpTimeoutPolicyOptions TimeoutOptions { get; set; } = new()
public HttpTimeoutStrategyOptions TimeoutOptions { get; set; } = new()
{
TimeoutInterval = _timeoutInterval,
Timeout = TimeSpan.FromSeconds(10),
StrategyName = StandardHedgingStrategyNames.AttemptTimeout
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>
/// Adds a standard hedging handler which wraps the execution of the request with a standard hedging mechanism.
Expand All @@ -28,12 +29,14 @@ public static partial class HttpClientBuilderExtensions
/// A <see cref="IStandardHedgingHandlerBuilder"/> builder that can be used to configure the standard hedging behavior.
/// </returns>
/// <remarks>
/// 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 cref="IStandardHedgingHandlerBuilder"/>.
///
/// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the policies inside the pipeline.
/// It is recommended that you configure the way the strategies are selected by calling
/// <see cref="StandardHedgingHandlerBuilderExtensions.SelectStrategyByAuthority(IStandardHedgingHandlerBuilder, DataClassification)"/>
/// extensions.
/// <para>
/// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the used resilience strategies.
/// </para>
/// </remarks>
public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder, Action<IRoutingStrategyBuilder> configure)
{
Expand All @@ -55,45 +58,86 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt
/// A <see cref="IStandardHedgingHandlerBuilder"/> builder that can be used to configure the standard hedging behavior.
/// </returns>
/// <remarks>
/// 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 cref="IStandardHedgingHandlerBuilder"/>.
///
/// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the policies inside the pipeline.
/// It is recommended that you configure the way the strategies are selected by calling
/// <see cref="StandardHedgingHandlerBuilderExtensions.SelectStrategyByAuthority(IStandardHedgingHandlerBuilder, DataClassification)"/>
/// extensions.
/// <para>
/// See <see cref="HttpStandardHedgingResilienceOptions"/> for more details about the used resilience strategies.
/// </para>
/// </remarks>
public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHttpClientBuilder builder)
{
_ = Throw.IfNull(builder);

var optionsName = builder.Name;
var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services);
_ = builder.Services.AddRequestCloner();
builder.Services.TryAddSingleton<IRequestCloner, RequestCloner>();
_ = builder.Services.AddValidatedOptions<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsValidator>(optionsName);
_ = builder.Services.AddValidatedOptions<HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsCustomValidator>(optionsName);
_ = builder.Services.PostConfigure<HttpStandardHedgingResilienceOptions>(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<HttpResponseMessage, HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsCustomValidator>(
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<HttpStandardHedgingResilienceOptions>(optionsName);
context.EnableReloads<HttpStandardHedgingResilienceOptions>(optionsName);

_ = builder
.AddStrategy(new RoutingResilienceStrategy(context.ServiceProvider.GetRoutingFactory(routingBuilder.Name)))
.AddStrategy(new RequestMessageSnapshotStrategy(context.ServiceProvider.GetRequiredService<IRequestCloner>()))
.AddTimeout(options.TotalRequestTimeoutOptions)
.AddHedging(options.HedgingOptions);
});

// configure inner handler
var innerBuilder = builder.AddResilienceHandler(StandardInnerHandlerPostfix);
_ = innerBuilder
.SelectPipelineByAuthority(new DataClassification("FIXME", 1))
.AddPolicy<HttpResponseMessage, HttpStandardHedgingResilienceOptions, HttpStandardHedgingResilienceOptionsValidator>(
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<HttpStandardHedgingResilienceOptions>(optionsName);
context.EnableReloads<HttpStandardHedgingResilienceOptions>(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;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,4 +28,14 @@ _ when HttpClientResiliencePredicates.IsTransientHttpException(exception) => tru
_ => false,
};
};

/// <summary>
/// Determines whether an outcome should be treated by hedging as a transient failure.
/// </summary>
public static readonly Predicate<Outcome<HttpResponseMessage>> IsTransientHttpOutcome = outcome => outcome switch
{
{ Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true,
{ Exception: { } exception } when IsTransientHttpException(exception) => true,
_ => false,
};
}
Loading