Skip to content

Commit

Permalink
Add httpclient extensions (#80)
Browse files Browse the repository at this point in the history
* added http client handlers

* added obsolete

* .net 8

* Update ci.yml

* Update release-preview.yml

* Update release.yml

* fix httpclient bug

* transient httpclienthandlers

* add SendAsync retryPolicy + update readme

* fix

* update readme

* add handlers constructor logging

* review

* add named and typed http clients

* fix readme

* fix 5xx logging

---------

Co-authored-by: Станислав Терещенков <tereschenkov.s@ati.su>
  • Loading branch information
CptnSnail and Станислав Терещенков committed May 28, 2024
1 parent 0e9f94b commit e341d8b
Show file tree
Hide file tree
Showing 26 changed files with 813 additions and 224 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
dotnet-version: '8.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
- name: Build
run: dotnet build --configuration Release
run: dotnet build --configuration Release
4 changes: 2 additions & 2 deletions .github/workflows/release-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
dotnet-version: '8.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
- name: Pack
run: dotnet pack --configuration Release /p:Version=${VERSION} --output .
- name: Push
run: dotnet nuget push atisu.services.common.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${ATISERVICES_NUGET_APIKEY}
env:
ATISERVICES_NUGET_APIKEY: ${{ secrets.ATISERVICES_NUGET_APIKEY }}
ATISERVICES_NUGET_APIKEY: ${{ secrets.ATISERVICES_NUGET_APIKEY }}
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ jobs:
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
dotnet-version: '8.0.x' # SDK Version to use; x will use the latest version of the 6.0 channel
- name: Build
run: dotnet build --configuration Release /p:Version=${VERSION}
- name: Pack
run: dotnet pack --configuration Release /p:Version=${VERSION} --no-build --output .
- name: Push
run: dotnet nuget push atisu.services.common.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${ATISERVICES_NUGET_APIKEY}
env:
ATISERVICES_NUGET_APIKEY: ${{ secrets.ATISERVICES_NUGET_APIKEY }}
ATISERVICES_NUGET_APIKEY: ${{ secrets.ATISERVICES_NUGET_APIKEY }}
2 changes: 1 addition & 1 deletion ATI.Services.Common/ATI.Services.Common.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Main">
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifiers>linux-x64</RuntimeIdentifiers>
<Authors>Team Services</Authors>
<Company>ATI</Company>
Expand Down
37 changes: 37 additions & 0 deletions ATI.Services.Common/Http/Extensions/HttpClientBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using ATI.Services.Common.Http.HttpHandlers;
using ATI.Services.Common.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;

namespace ATI.Services.Common.Http.Extensions;

[PublicAPI]
public static class HttpClientBuilderExtensions
{
public static IHttpClientBuilder WithProxyFields<TServiceOptions>(this IHttpClientBuilder httpClientBuilder)
where TServiceOptions : BaseServiceOptions
{
httpClientBuilder.Services.AddTransient<HttpProxyFieldsHandler<TServiceOptions>>();

return httpClientBuilder
.AddHttpMessageHandler<HttpProxyFieldsHandler<TServiceOptions>>();
}

public static IHttpClientBuilder WithLogging<TServiceOptions>(this IHttpClientBuilder httpClientBuilder)
where TServiceOptions : BaseServiceOptions
{
httpClientBuilder.Services.AddTransient<HttpLoggingHandler<TServiceOptions>>();

return httpClientBuilder
.AddHttpMessageHandler<HttpLoggingHandler<TServiceOptions>>();
}

public static IHttpClientBuilder WithMetrics<TServiceOptions>(this IHttpClientBuilder httpClientBuilder)
where TServiceOptions : BaseServiceOptions
{
httpClientBuilder.Services.AddTransient<HttpMetricsHandler<TServiceOptions>>();

return httpClientBuilder
.AddHttpMessageHandler<HttpMetricsHandler<TServiceOptions>>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
using Polly.Registry;
using Polly.Timeout;

namespace ATI.Services.Common.Http;
namespace ATI.Services.Common.Http.Extensions;

/// <summary>
/// https://habr.com/ru/companies/dododev/articles/503376/
Expand All @@ -34,11 +34,28 @@ public static class HttpClientBuilderPolicyExtensions
ILogger logger)
{
var methodsToRetry = serviceOptions.HttpMethodsToRetry ?? new List<string> { HttpMethod.Get.Method };

return clientBuilder
.AddPolicyHandler((_, message) =>
{
if (!methodsToRetry.Contains(message.Method.Method, StringComparer.OrdinalIgnoreCase))
var medianFirstRetryDelay = serviceOptions.MedianFirstRetryDelay;
var retryCount = serviceOptions.RetryCount;
var checkHttpMethod = true;
var retryPolicySettings = message.Options.GetRetryPolicy();
if (retryPolicySettings != null)
{
if (retryPolicySettings.MedianFirstRetryDelay != null)
medianFirstRetryDelay = retryPolicySettings.MedianFirstRetryDelay.Value;
if (retryPolicySettings.RetryCount != null)
retryCount = retryPolicySettings.RetryCount.Value;
// If retrySettings set via HttpClient.SendAsync method - ignore http method check
checkHttpMethod = false;
}
if (retryCount == 0 || checkHttpMethod && !methodsToRetry.Contains(message.Method.Method, StringComparer.OrdinalIgnoreCase))
{
return Policy.NoOpAsync<HttpResponseMessage>();
}
Expand All @@ -47,12 +64,12 @@ public static class HttpClientBuilderPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.OrResult(r => r.StatusCode == (HttpStatusCode) 429) // Too Many Requests
.WaitAndRetryAsync(RetryPolicyDelay(serviceOptions.MedianFirstRetryDelay, serviceOptions.RetryCount),
.WaitAndRetryAsync(RetryPolicyDelay(medianFirstRetryDelay, retryCount),
(response, sleepDuration, retryCount, _) =>
{
logger.ErrorWithObject(response?.Exception, "Error while WaitAndRetry", new
{
serviceOptions.ConsulName,
serviceOptions.ServiceName,
message.RequestUri,
message.Method,
response?.Result?.StatusCode,
Expand All @@ -71,8 +88,23 @@ public static class HttpClientBuilderPolicyExtensions
var registry = new PolicyRegistry();
return clientBuilder.AddPolicyHandler(message =>
{
var policyKey = message.RequestUri.Host;
var policy = registry.GetOrAdd(policyKey, BuildCircuitBreakerPolicy(message, serviceOptions, logger));
var circuitBreakerExceptionsCount = serviceOptions.CircuitBreakerExceptionsCount;
var circuitBreakerDuration = serviceOptions.CircuitBreakerDuration;
var retryPolicySettings = message.Options.GetRetryPolicy();
if (retryPolicySettings != null)
{
if (retryPolicySettings.CircuitBreakerExceptionsCount != null)
circuitBreakerExceptionsCount = retryPolicySettings.CircuitBreakerExceptionsCount.Value;
if (retryPolicySettings.CircuitBreakerDuration != null)
circuitBreakerDuration = retryPolicySettings.CircuitBreakerDuration.Value;
}
if (circuitBreakerExceptionsCount == 0)
return Policy.NoOpAsync<HttpResponseMessage>();
var policyKey = $"{message.RequestUri.Host}:{message.RequestUri.Port}";
var policy = registry.GetOrAdd(policyKey, BuildCircuitBreakerPolicy(message, serviceOptions, circuitBreakerExceptionsCount, circuitBreakerDuration, logger));
return policy;
});
}
Expand All @@ -81,27 +113,34 @@ public static class HttpClientBuilderPolicyExtensions
this IHttpClientBuilder httpClientBuilder,
TimeSpan timeout)
{
return httpClientBuilder.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(timeout));
return httpClientBuilder.AddPolicyHandler((_, message) =>
{
var policyTimeout = message.Options.GetRetryPolicy()?.TimeOut ?? timeout;
return Policy.TimeoutAsync<HttpResponseMessage>(policyTimeout);
});
}


private static AsyncCircuitBreakerPolicy<HttpResponseMessage> BuildCircuitBreakerPolicy(
HttpRequestMessage message,
BaseServiceOptions serviceOptions,
int circuitBreakerExceptionsCount,
TimeSpan circuitBreakerDuration,
ILogger logger)
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.OrResult(r => r.StatusCode == (HttpStatusCode) 429) // Too Many Requests
.CircuitBreakerAsync(
serviceOptions.CircuitBreakerExceptionsCount,
serviceOptions.CircuitBreakerDuration,
circuitBreakerExceptionsCount,
circuitBreakerDuration,
(response, circuitState, timeSpan, _) =>
{
logger.ErrorWithObject(null, "CB onBreak", new
{
serviceOptions.ConsulName,
serviceOptions.ServiceName,
message.RequestUri,
message.Method,
response?.Result?.StatusCode,
Expand All @@ -113,7 +152,7 @@ public static class HttpClientBuilderPolicyExtensions
{
logger.ErrorWithObject(null, "CB onReset", new
{
serviceOptions.ConsulName,
serviceOptions.ServiceName,
message.RequestUri,
message.Method,
context
Expand All @@ -123,7 +162,7 @@ public static class HttpClientBuilderPolicyExtensions
{
logger.ErrorWithObject(null, "CB onHalfOpen", new
{
serviceOptions.ConsulName,
serviceOptions.ServiceName,
message.RequestUri,
message.Method,
});
Expand Down
Loading

0 comments on commit e341d8b

Please sign in to comment.