зеркало из https://github.com/dotnet/extensions.git
[release/8.5] Detect connection timeouts in Hedging (#5149)
This commit is contained in:
Коммит
de1f3236c7
|
@ -2,7 +2,10 @@
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using Polly;
|
||||
using Polly.CircuitBreaker;
|
||||
|
@ -17,13 +20,26 @@ public static class HttpClientHedgingResiliencePredicates
|
|||
/// <summary>
|
||||
/// Determines whether an outcome should be treated by hedging as a transient failure.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The outcome of the user-specified callback.</param>
|
||||
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
|
||||
public static bool IsTransient(Outcome<HttpResponseMessage> outcome) => outcome switch
|
||||
{
|
||||
{ Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true,
|
||||
{ Exception: { } exception } when IsTransientHttpException(exception) => true,
|
||||
_ => false,
|
||||
};
|
||||
public static bool IsTransient(Outcome<HttpResponseMessage> outcome)
|
||||
=> outcome switch
|
||||
{
|
||||
{ Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true,
|
||||
{ Exception: { } exception } when IsTransientHttpException(exception) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an <see cref="HttpResponseMessage"/> should be treated by hedging as a transient failure.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The outcome of the user-specified callback.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> associated with the execution.</param>
|
||||
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
|
||||
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
|
||||
public static bool IsTransient(Outcome<HttpResponseMessage> outcome, CancellationToken cancellationToken)
|
||||
=> HttpClientResiliencePredicates.IsHttpConnectionTimeout(outcome, cancellationToken)
|
||||
|| IsTransient(outcome);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an exception should be treated by hedging as a transient failure.
|
||||
|
@ -35,8 +51,7 @@ public static class HttpClientHedgingResiliencePredicates
|
|||
return exception switch
|
||||
{
|
||||
BrokenCircuitException => true,
|
||||
_ when HttpClientResiliencePredicates.IsTransientHttpException(exception) => true,
|
||||
_ => false,
|
||||
_ => HttpClientResiliencePredicates.IsTransientHttpException(exception),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ public class HttpHedgingStrategyOptions : HedgingStrategyOptions<HttpResponseMes
|
|||
/// Initializes a new instance of the <see cref="HttpHedgingStrategyOptions"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default the options is set to handle only transient failures,
|
||||
/// By default, the options is set to handle only transient failures,
|
||||
/// i.e. timeouts, 5xx responses and <see cref="HttpRequestException"/> exceptions.
|
||||
/// </remarks>
|
||||
public HttpHedgingStrategyOptions()
|
||||
{
|
||||
ShouldHandle = args => new ValueTask<bool>(HttpClientHedgingResiliencePredicates.IsTransient(args.Outcome));
|
||||
ShouldHandle = args => new ValueTask<bool>(HttpClientHedgingResiliencePredicates.IsTransient(args.Outcome, args.Context.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using Polly;
|
||||
using Polly.Timeout;
|
||||
|
@ -26,6 +29,17 @@ public static class HttpClientResiliencePredicates
|
|||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an <see cref="HttpResponseMessage"/> should be treated by resilience strategies as a transient failure.
|
||||
/// </summary>
|
||||
/// <param name="outcome">The outcome of the user-specified callback.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> associated with the execution.</param>
|
||||
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
|
||||
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
|
||||
public static bool IsTransient(Outcome<HttpResponseMessage> outcome, CancellationToken cancellationToken)
|
||||
=> IsHttpConnectionTimeout(outcome, cancellationToken)
|
||||
|| IsTransient(outcome);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an exception should be treated by resilience strategies as a transient failure.
|
||||
/// </summary>
|
||||
|
@ -33,10 +47,14 @@ public static class HttpClientResiliencePredicates
|
|||
{
|
||||
_ = Throw.IfNull(exception);
|
||||
|
||||
return exception is HttpRequestException ||
|
||||
exception is TimeoutRejectedException;
|
||||
return exception is HttpRequestException or TimeoutRejectedException;
|
||||
}
|
||||
|
||||
internal static bool IsHttpConnectionTimeout(in Outcome<HttpResponseMessage> outcome, in CancellationToken cancellationToken)
|
||||
=> !cancellationToken.IsCancellationRequested
|
||||
&& outcome.Exception is OperationCanceledException { Source: "System.Private.CoreLib" }
|
||||
&& outcome.Exception.InnerException is TimeoutException;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a response contains a transient failure.
|
||||
/// </summary>
|
||||
|
@ -52,7 +70,6 @@ public static class HttpClientResiliencePredicates
|
|||
return statusCode >= InternalServerErrorCode ||
|
||||
response.StatusCode == HttpStatusCode.RequestTimeout ||
|
||||
statusCode == TooManyRequests;
|
||||
|
||||
}
|
||||
|
||||
private const int InternalServerErrorCode = (int)HttpStatusCode.InternalServerError;
|
||||
|
|
|
@ -26,7 +26,7 @@ public class HttpRetryStrategyOptions : RetryStrategyOptions<HttpResponseMessage
|
|||
/// </remarks>
|
||||
public HttpRetryStrategyOptions()
|
||||
{
|
||||
ShouldHandle = args => new ValueTask<bool>(HttpClientResiliencePredicates.IsTransient(args.Outcome));
|
||||
ShouldHandle = args => new ValueTask<bool>(HttpClientResiliencePredicates.IsTransient(args.Outcome, args.Context.CancellationToken));
|
||||
BackoffType = DelayBackoffType.Exponential;
|
||||
ShouldRetryAfterHeader = true;
|
||||
UseJitter = true;
|
||||
|
|
|
@ -35,6 +35,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
private readonly Func<RequestRoutingStrategy> _requestRoutingStrategyFactory;
|
||||
private readonly IServiceCollection _services;
|
||||
private readonly Queue<HttpResponseMessage> _responses = new();
|
||||
private ServiceProvider? _serviceProvider;
|
||||
private bool _failure;
|
||||
|
||||
private protected HedgingTests(Func<IHttpClientBuilder, Func<RequestRoutingStrategy>, TBuilder> createDefaultBuilder)
|
||||
|
@ -63,6 +64,11 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
_requestRoutingStrategyMock.VerifyAll();
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_serviceProvider?.Dispose();
|
||||
foreach (var response in _responses)
|
||||
{
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -93,7 +99,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
|
@ -108,7 +114,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
AddResponse(HttpStatusCode.OK);
|
||||
|
||||
var response = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
AssertNoResponse();
|
||||
|
||||
Assert.Single(Requests);
|
||||
|
@ -164,7 +170,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var result = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
Assert.Equal(DefaultHedgingAttempts + 1, Requests.Count);
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, result.StatusCode);
|
||||
}
|
||||
|
@ -183,7 +189,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
|
||||
RequestContexts.Distinct().OfType<ResilienceContext>().Should().HaveCount(3);
|
||||
}
|
||||
|
@ -204,7 +210,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
|
||||
RequestContexts.Distinct().OfType<ResilienceContext>().Should().HaveCount(3);
|
||||
|
||||
|
@ -226,7 +232,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
Assert.Equal(2, Requests.Count);
|
||||
}
|
||||
|
||||
|
@ -244,7 +250,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
using var client = CreateClientWithHandler();
|
||||
|
||||
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
using var result = await client.SendAsync(request, _cancellationTokenSource.Token);
|
||||
Assert.Equal(3, Requests.Count);
|
||||
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
||||
Assert.Equal("https://enpoint-1:80/some-path?query", Requests[0]);
|
||||
|
@ -268,7 +274,12 @@ public abstract class HedgingTests<TBuilder> : IDisposable
|
|||
|
||||
protected abstract void ConfigureHedgingOptions(Action<HttpHedgingStrategyOptions> configure);
|
||||
|
||||
protected HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>().CreateClient(ClientId);
|
||||
protected HttpClient CreateClientWithHandler()
|
||||
{
|
||||
_serviceProvider?.Dispose();
|
||||
_serviceProvider = _services.BuildServiceProvider();
|
||||
return _serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(ClientId);
|
||||
}
|
||||
|
||||
private Task<HttpResponseMessage> InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.Http.Resilience.Test.Hedging;
|
||||
|
||||
internal sealed class OperationCanceledExceptionMock : OperationCanceledException
|
||||
{
|
||||
public OperationCanceledExceptionMock(Exception innerException)
|
||||
: base(null, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public override string? Source { get => "System.Private.CoreLib"; set => base.Source = value; }
|
||||
}
|
|
@ -46,7 +46,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
{
|
||||
Builder.Configure(options => options.Hedging.MaxHedgedAttempts = -1);
|
||||
|
||||
Assert.Throws<OptionsValidationException>(() => CreateClientWithHandler());
|
||||
Assert.Throws<OptionsValidationException>(CreateClientWithHandler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -54,7 +54,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
{
|
||||
Builder.Configure(options => options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(1));
|
||||
|
||||
Assert.Throws<OptionsValidationException>(() => CreateClientWithHandler());
|
||||
Assert.Throws<OptionsValidationException>(CreateClientWithHandler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -62,7 +62,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
{
|
||||
Builder.Configure(o => o.Hedging.MaxHedgedAttempts = 8);
|
||||
|
||||
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
|
||||
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
|
||||
}
|
||||
|
@ -76,7 +77,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
o.Hedging.MaxHedgedAttempts = 8;
|
||||
});
|
||||
|
||||
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
|
||||
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
|
||||
}
|
||||
|
@ -97,7 +99,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
|
||||
Builder.Configure(section);
|
||||
|
||||
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
|
||||
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
|
||||
}
|
||||
|
@ -105,7 +108,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
[Fact]
|
||||
public void ActionGenerator_Ok()
|
||||
{
|
||||
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
|
||||
var generator = options.Hedging.ActionGenerator;
|
||||
var primary = ResilienceContextPool.Shared.Get();
|
||||
var secondary = ResilienceContextPool.Shared.Get();
|
||||
|
@ -133,9 +137,12 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
Builder.Configure(section);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
Builder.Services.BuildServiceProvider()
|
||||
.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>()
|
||||
.Get(Builder.Name));
|
||||
{
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
return serviceProvider
|
||||
.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>()
|
||||
.Get(Builder.Name);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -163,7 +170,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
[Fact]
|
||||
public void VerifyPipeline()
|
||||
{
|
||||
var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
using var serviceProvider = Builder.Services.BuildServiceProvider();
|
||||
var pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<HttpKey>>();
|
||||
|
||||
// primary handler
|
||||
|
@ -209,7 +216,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
using var request = new HttpRequestMessage(HttpMethod.Get, "https://key:80/discarded");
|
||||
AddResponse(HttpStatusCode.OK);
|
||||
|
||||
var response = await client.SendAsync(request, CancellationToken.None);
|
||||
using var response = await client.SendAsync(request, CancellationToken.None);
|
||||
|
||||
provider.VerifyAll();
|
||||
}
|
||||
|
@ -235,14 +242,14 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
// act && assert
|
||||
AddResponse(HttpStatusCode.InternalServerError, 3);
|
||||
using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query");
|
||||
await client.SendAsync(firstRequest);
|
||||
using var _ = await client.SendAsync(firstRequest);
|
||||
AssertNoResponse();
|
||||
|
||||
reloadAction(new() { { "standard:Hedging:MaxHedgedAttempts", "6" } });
|
||||
|
||||
AddResponse(HttpStatusCode.InternalServerError, 7);
|
||||
using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query");
|
||||
await client.SendAsync(secondRequest);
|
||||
using var __ = await client.SendAsync(secondRequest);
|
||||
AssertNoResponse();
|
||||
}
|
||||
|
||||
|
@ -257,11 +264,70 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
|
|||
// act && assert
|
||||
AddResponse(HttpStatusCode.InternalServerError, 3);
|
||||
using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://some-endpoint:1234/some-path?query");
|
||||
await client.SendAsync(firstRequest);
|
||||
using var _ = await client.SendAsync(firstRequest);
|
||||
AssertNoResponse();
|
||||
|
||||
Requests.Should().AllSatisfy(r => r.Should().Be("https://some-endpoint:1234/some-path?query"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_FailedConnect_ShouldReturnResponseFromHedging()
|
||||
{
|
||||
const string FailingEndpoint = "www.failing-host.com";
|
||||
|
||||
var services = new ServiceCollection();
|
||||
_ = services
|
||||
.AddHttpClient(ClientId)
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new MockHttpMessageHandler(FailingEndpoint))
|
||||
.AddStandardHedgingHandler(routing =>
|
||||
routing.ConfigureOrderedGroups(g =>
|
||||
{
|
||||
g.Groups.Add(new UriEndpointGroup
|
||||
{
|
||||
Endpoints = [new WeightedUriEndpoint { Uri = new Uri($"https://{FailingEndpoint}:3000") }]
|
||||
});
|
||||
|
||||
g.Groups.Add(new UriEndpointGroup
|
||||
{
|
||||
Endpoints = [new WeightedUriEndpoint { Uri = new Uri("https://microsoft.com") }]
|
||||
});
|
||||
}))
|
||||
.Configure(opt =>
|
||||
{
|
||||
opt.Hedging.MaxHedgedAttempts = 10;
|
||||
opt.Hedging.Delay = TimeSpan.FromSeconds(11);
|
||||
opt.Endpoint.CircuitBreaker.FailureRatio = 0.99;
|
||||
opt.Endpoint.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(900);
|
||||
opt.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(200);
|
||||
opt.Endpoint.Timeout.Timeout = TimeSpan.FromSeconds(200);
|
||||
});
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var clientFactory = provider.GetRequiredService<IHttpClientFactory>();
|
||||
using var client = clientFactory.CreateClient(ClientId);
|
||||
|
||||
var ex = await Record.ExceptionAsync(async () =>
|
||||
{
|
||||
using var _ = await client.GetAsync($"https://{FailingEndpoint}:3000");
|
||||
});
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
protected override void ConfigureHedgingOptions(Action<HttpHedgingStrategyOptions> configure) => Builder.Configure(options => configure(options.Hedging));
|
||||
|
||||
private class MockHttpMessageHandler(string failingEndpoint) : HttpMessageHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.RequestUri?.Host == failingEndpoint)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
throw new OperationCanceledExceptionMock(new TimeoutException());
|
||||
}
|
||||
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,6 @@
|
|||
<Description>Unit tests for Microsoft.Extensions.Http.Resilience.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net462' ">
|
||||
<!-- See https://github.com/dotnet/extensions/issues/4531 -->
|
||||
<SkipTests>true</SkipTests>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="configs\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -6,8 +6,10 @@ using System.Collections.Generic;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Http.Resilience.Test.Hedging;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using Xunit;
|
||||
|
@ -19,16 +21,15 @@ public class HttpRetryStrategyOptionsTests
|
|||
#pragma warning disable S2330
|
||||
public static readonly IEnumerable<object[]> HandledExceptionsClassified = new[]
|
||||
{
|
||||
new object[] { new InvalidCastException(), false },
|
||||
new object[] { new HttpRequestException(), true }
|
||||
new object[] { new InvalidCastException(), null!, false },
|
||||
[new HttpRequestException(), null!, true],
|
||||
[new OperationCanceledExceptionMock(new TimeoutException()), null!, true],
|
||||
[new OperationCanceledExceptionMock(new TimeoutException()), default(CancellationToken), true],
|
||||
[new OperationCanceledExceptionMock(new InvalidOperationException()), default(CancellationToken), false],
|
||||
[new OperationCanceledExceptionMock(new TimeoutException()), new CancellationToken(canceled: true), false],
|
||||
};
|
||||
|
||||
private readonly HttpRetryStrategyOptions _testClass;
|
||||
|
||||
public HttpRetryStrategyOptionsTests()
|
||||
{
|
||||
_testClass = new HttpRetryStrategyOptions();
|
||||
}
|
||||
private readonly HttpRetryStrategyOptions _testClass = new();
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Defaults()
|
||||
|
@ -63,9 +64,10 @@ public class HttpRetryStrategyOptionsTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HandledExceptionsClassified))]
|
||||
public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle)
|
||||
public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, CancellationToken? token, bool expectedToHandle)
|
||||
{
|
||||
var shouldHandle = await _testClass.ShouldHandle(CreateArgs(Outcome.FromException<HttpResponseMessage>(exception)));
|
||||
var args = CreateArgs(Outcome.FromException<HttpResponseMessage>(exception), token ?? default);
|
||||
var shouldHandle = await _testClass.ShouldHandle(args);
|
||||
Assert.Equal(expectedToHandle, shouldHandle);
|
||||
}
|
||||
|
||||
|
@ -86,9 +88,10 @@ public class HttpRetryStrategyOptionsTests
|
|||
|
||||
[Theory]
|
||||
[MemberData(nameof(HandledExceptionsClassified))]
|
||||
public async Task ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle)
|
||||
public async Task ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, CancellationToken? token, bool expectedToHandle)
|
||||
{
|
||||
var shouldHandle = await new HttpRetryStrategyOptions().ShouldHandle(CreateArgs(Outcome.FromException<HttpResponseMessage>(exception)));
|
||||
var args = CreateArgs(Outcome.FromException<HttpResponseMessage>(exception), token ?? default);
|
||||
var shouldHandle = await new HttpRetryStrategyOptions().ShouldHandle(args);
|
||||
Assert.Equal(expectedToHandle, shouldHandle);
|
||||
}
|
||||
|
||||
|
@ -96,7 +99,7 @@ public class HttpRetryStrategyOptionsTests
|
|||
public async Task ShouldRetryAfterHeader_InvalidOutcomes_ShouldReturnNull()
|
||||
{
|
||||
var options = new HttpRetryStrategyOptions { ShouldRetryAfterHeader = true };
|
||||
using var responseMessage = new HttpResponseMessage { };
|
||||
using var responseMessage = new HttpResponseMessage();
|
||||
|
||||
Assert.NotNull(options.DelayGenerator);
|
||||
|
||||
|
@ -153,7 +156,8 @@ public class HttpRetryStrategyOptionsTests
|
|||
Assert.Equal(shouldRetryAfterHeader, options.DelayGenerator != null);
|
||||
}
|
||||
|
||||
private static RetryPredicateArguments<HttpResponseMessage> CreateArgs(Outcome<HttpResponseMessage> outcome)
|
||||
=> new(ResilienceContextPool.Shared.Get(), outcome, 0);
|
||||
|
||||
private static RetryPredicateArguments<HttpResponseMessage> CreateArgs(
|
||||
Outcome<HttpResponseMessage> outcome,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> new(ResilienceContextPool.Shared.Get(cancellationToken), outcome, 0);
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
|
|||
|
||||
AddStandardResilienceHandler(mode, builder, _invalidConfigurationSection, options => { });
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => HttpClientBuilderExtensionsTests.GetPipeline(builder.Services, $"test-standard"));
|
||||
Assert.Throws<InvalidOperationException>(() => HttpClientBuilderExtensionsTests.GetPipeline(builder.Services, "test-standard"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -175,7 +175,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
|
|||
}
|
||||
});
|
||||
|
||||
Assert.Throws<OptionsValidationException>(() => GetPipeline(builder.Services, $"test-standard"));
|
||||
Assert.Throws<OptionsValidationException>(() => GetPipeline(builder.Services, "test-standard"));
|
||||
}
|
||||
|
||||
[InlineData(MethodArgs.None)]
|
||||
|
@ -193,7 +193,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
|
|||
|
||||
AddStandardResilienceHandler(mode, builder, _validConfigurationSection, options => { });
|
||||
|
||||
var pipeline = GetPipeline(builder.Services, $"test-standard");
|
||||
var pipeline = GetPipeline(builder.Services, "test-standard");
|
||||
Assert.NotNull(pipeline);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,12 +9,7 @@ namespace Microsoft.Extensions.Http.Resilience.Test.Resilience;
|
|||
|
||||
public class HttpStandardResilienceOptionsTests
|
||||
{
|
||||
private readonly HttpStandardResilienceOptions _options;
|
||||
|
||||
public HttpStandardResilienceOptionsTests()
|
||||
{
|
||||
_options = new HttpStandardResilienceOptions();
|
||||
}
|
||||
private readonly HttpStandardResilienceOptions _options = new();
|
||||
|
||||
[Fact]
|
||||
public void Ctor_EnsureDefaults()
|
||||
|
|
Загрузка…
Ссылка в новой задаче