Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
@@ -1,11 +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 System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Telemetry.Logging;

namespace Microsoft.Extensions.Http.Resilience.FaultInjection.Internal;

[ExcludeFromCodeCoverage]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't have this, the logging code should be tested too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mean adding exhaustive test coverage for everything the generated logging code does, which is effectively external and an implementation detail?

Seems wasteful to me to 100% cover someone else's code just because it's source-generated as long as it's being exercised by a test. I don't think you'd need it if the requirement wasn't 100% coverage as you could just tweak the threshold, but if it's all or nothing then this seems to be the sensible alternative to exhaustively re-testing the generated code to do logging.

By the same logic I exclude Request Delegate Generator code, otherwise you get fun behaviour like this: dotnet/aspnetcore#48376. I certainly wouldn't spend my time testing ASP.NET Code's error handling scenarios for my user-supplied delegate chasing a coverage metric.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What Martin says, we had this problem in Polly too. We had to artificially add test cases for code that was auto-generated.

https://github.com/App-vNext/Polly/blob/faaf2ee95b9b92386f8e091277604240f9ab05c0/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs#L173

Not a big fan of that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martincostello That's not what this implies.

This is about ensuring the telemetry produced by this component matches expectations. Within the tests, you use a FakeLogger that captures the telemetry and the ensure that the right thing is coming out of the logging system at the time you expect it to.

As a general rule, the only things that we consider acceptable to exclude from code coverage involve some legacy design patterns that preclude it and some racy code where its impossible to ensure all paths are visited in a given test run. Both are very rare things.

Copy link
Member

@martincostello martincostello Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But isn't that custom telemetry logging code called through the compiler generated code? There's no "real" code here in this class, it's just a partial method stub:

[LogMethod(0, LogLevel.Information,
"Fault-injection group name: {groupName}. " +
"Fault type: {faultType}. " +
"Injected value: {injectedValue}. " +
"Http content key: {httpContentKey}. ")]
public static partial void LogInjection(
ILogger logger,
string groupName,
string faultType,
string injectedValue,
string httpContentKey);

It not being covered doesn't preclude the assertions in the fake logger.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless of course [LogMethod] doesn't work the same as [LoggerMessage] which I initially thought this was.

Looking at some code of my own I have this:

[ExcludeFromCodeCoverage]
private static partial class Log
{
    [LoggerMessage(
       EventId = 1,
       Level = LogLevel.Information,
       Message = "Deployments have been frozen by {UserName}.")]
    public static partial void DeploymentsFrozen(ILogger logger, string? userName);
}

Which gets turned into this:

partial class Log
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.8.28008")]
    private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.String?, global::System.Exception?> __DeploymentsFrozenCallback =
        global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.String?>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(1, nameof(DeploymentsFrozen)), "Deployments have been frozen by {UserName}.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.8.28008")]
    public static partial void DeploymentsFrozen(global::Microsoft.Extensions.Logging.ILogger logger, global::System.String? userName)
    {
        if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
        {
            __DeploymentsFrozenCallback(logger, userName, null);
        }
    }
}

In that case it's not useful to add tests to test the coverage of the branching for IsEnabled() on all the logging we have, hence the coverage suppression in my case.

If it's a different use case here because that just looks similar to the above then disregard my comment 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Polly we are correctly checking that message is logged:

https://github.com/App-vNext/Polly/blob/faaf2ee95b9b92386f8e091277604240f9ab05c0/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs#L155

But for some strange reason, it want's us to cover the code-generated code too. For example, the auto-generated struct that backs the entry with all it's properties and methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a bug #4124. In the meantime, I had to manually decrease code-coverage to 98. Once that bug is fixed, we can put it back to 100.

internal static partial class Log
{
[LogMethod(0, LogLevel.Information,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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
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