Skip to content

Commit

Permalink
Merge pull request #26 from atidev/add-httpclient-extensions
Browse files Browse the repository at this point in the history
add http client handlers
  • Loading branch information
CptnSnail authored May 28, 2024
2 parents 52db3f1 + 5cfb5c2 commit 3fc8db1
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 14 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.consul.${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.consul.${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 ATI.Services.Consul/ATI.Services.Consul.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 All @@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="atisu.services.common" Version="15.0.0" />
<PackageReference Include="atisu.services.common" Version="16.0.0" />
<PackageReference Include="Consul" Version="1.6.10.9" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions ATI.Services.Consul/ConsulMetricsHttpClientWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace ATI.Services.Consul
/// Обертка, включающая в себя ConsulServiceAddress, MetricsHttpClientWrapper и MetricsTracingFactory
/// </summary>
[PublicAPI]
[Obsolete("Use HttpClientFactory instead")]
public class ConsulMetricsHttpClientWrapper : IDisposable
{
private readonly BaseServiceOptions _serviceOptions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -6,6 +7,7 @@ namespace ATI.Services.Consul;
[PublicAPI]
public static class ConsulMetricsHttpClientWrapperServiceCollectionExtensions
{
[Obsolete("Use HttpClientFactory and ServiceCollection.AddConsulHttpClients instead")]
public static IServiceCollection AddConsulMetricsHttpClientWrappers(this IServiceCollection services)
{
// Add IHttpClientFactory for ConsulMetricsHttpClientWrapper<>
Expand Down
24 changes: 24 additions & 0 deletions ATI.Services.Consul/Http/HttpClientBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Threading;
using ATI.Services.Common.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;

namespace ATI.Services.Consul.Http;

[PublicAPI]
public static class HttpClientBuilderExtensions
{
public static IHttpClientBuilder WithConsul<TServiceOptions>(this IHttpClientBuilder httpClientBuilder)
where TServiceOptions : BaseServiceOptions
{
httpClientBuilder.Services.AddSingleton<HttpConsulHandler<TServiceOptions>>();

return httpClientBuilder
.AddHttpMessageHandler<HttpConsulHandler<TServiceOptions>>()
// By default, handlers are alive for 2 minutes
// If we don't set InfiniteTimeSpan, every 2 minutes HttpConsulHandler will be recreated
// And it will lead to new ConsulServiceAddress instances, which constructor is pretty expensive and will stop http requests for some time
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
}

}
52 changes: 52 additions & 0 deletions ATI.Services.Consul/Http/HttpConsulHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ATI.Services.Common.Logging;
using ATI.Services.Common.Metrics;
using ATI.Services.Common.Options;
using Microsoft.Extensions.Options;
using NLog;

namespace ATI.Services.Consul.Http;

public class HttpConsulHandler<T> : HttpConsulHandler where T : BaseServiceOptions
{
public HttpConsulHandler(MetricsFactory metricsFactory, IOptions<T> serviceOptions)
: base(metricsFactory, serviceOptions.Value)
{
}
}

public class HttpConsulHandler : DelegatingHandler
{
private readonly ConsulServiceAddress _serviceAddress;
private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();

protected HttpConsulHandler(MetricsFactory metricsFactory, BaseServiceOptions serviceOptions)
{
_serviceAddress =
new ConsulServiceAddress(metricsFactory, serviceOptions.ConsulName, serviceOptions.Environment);

Logger.WarnWithObject("HttpConsulHandler constructor", new { serviceOptions.ServiceName });
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
var url = await _serviceAddress.ToHttpAsync();
var relativeUrl = request.RequestUri?.PathAndQuery;
request.RequestUri = new Uri(new Uri(url), relativeUrl);

return await base.SendAsync(request, ct);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_serviceAddress?.Dispose();
}

base.Dispose(disposing);
}
}
65 changes: 65 additions & 0 deletions ATI.Services.Consul/Http/ServiceCollectionHttpClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using ATI.Services.Common.Http.Extensions;
using ATI.Services.Common.Options;
using ATI.Services.Common.Variables;
using JetBrains.Annotations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using ConfigurationManager = ATI.Services.Common.Behaviors.ConfigurationManager;

namespace ATI.Services.Consul.Http;

[PublicAPI]
public static class ServiceCollectionHttpClientExtensions
{
/// <summary>
/// Add HttpClient to HttpClientFactory with retry/cb/timeout policy
/// Will add only if UseHttpClientFactory == true
/// </summary>
/// <param name="services"></param>
/// <typeparam name="TAdapter">Type of the http adapter for typed HttpClient</typeparam>
/// <typeparam name="TServiceOptions"></typeparam>
/// <returns></returns>s
public static IServiceCollection AddConsulHttpClient<TAdapter, TServiceOptions>(this IServiceCollection services)
where TAdapter : class
where TServiceOptions : BaseServiceOptions
{
var className = typeof(TServiceOptions).Name;
var settings = ConfigurationManager.GetSection(className).Get<TServiceOptions>();
if (settings == null)
{
throw new Exception($"Cannot find section for {className}");
}

if (string.IsNullOrEmpty(settings.ConsulName))
{
throw new Exception($"Class {className} has ConsulName == null while AddConsulHttpClient");
}

var serviceName = settings.ServiceName;
var logger = LogManager.GetLogger(serviceName);

var serviceVariablesOptions = ConfigurationManager.GetSection(nameof(ServiceVariablesOptions)).Get<ServiceVariablesOptions>();

services.AddHttpClient<TAdapter>(httpClient =>
{
// We will override this url by consul, but we need to set it, otherwise we will get exception because HttpRequestMessage doesn't have baseUrl (only relative)
httpClient.BaseAddress = new Uri("http://localhost");
httpClient.SetBaseFields(serviceVariablesOptions.GetServiceAsClientName(), serviceVariablesOptions.GetServiceAsClientHeaderName(), settings.AdditionalHeaders);
})
.WithLogging<TServiceOptions>()
.WithProxyFields<TServiceOptions>()
.AddRetryPolicy(settings, logger)
// Get new instance url for each retry (because 1 instance can be down)
.WithConsul<TServiceOptions>()
.AddHostSpecificCircuitBreakerPolicy(settings, logger)
.AddTimeoutPolicy(settings.TimeOut)
.WithMetrics<TServiceOptions>();
// we don't override PooledConnectionLifetime even we use HttpClient in static TAdapter
// because we are getting new host from consul for each request
// https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines

return services;
}
}
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# ATI.Services.Consul
## Деплой
Выкладка в nuget происходит на основе триггера на тег определённого формата
- `v1.0.0` - формат релизная версия на ветке master
- `v1.0.0-rc1` - формат тестовой/альфа/бета версии на любой ветке
Выкладка в nuget происходит на основе триггера на тег определённого формата
- `v1.0.0` - формат релизная версия на ветке master
- `v1.0.0-rc1` - формат тестовой/альфа/бета версии на любой ветке

Тег можно создать через git(нужно запушить его в origin) [создание тега и пуш в remote](https://git-scm.com/book/en/v2/Git-Basics-Tagging)
Тег можно создать через git(нужно запушить его в origin) [создание тега и пуш в remote](https://git-scm.com/book/en/v2/Git-Basics-Tagging)
или через раздел [releses](https://github.com/atidev/ATI.Services.Consul/releases)(альфа версии нужно помечать соответсвующей галкой).

#### Разработка теперь выглядит вот так:
Expand All @@ -22,7 +22,7 @@

### Consul
Чтобы зарегистрировать сервис в консуле нужно:
`в appsettings.json` поместить блок
`в appsettings.json` поместить блок
```json
"ConsulRegistratorOptions": {
"ProvideEnvironment": "env",
Expand All @@ -40,8 +40,21 @@
]
}
```
> Да, там массив. Да, можно зарегать один сервак под разными тегами, именами, всем.
> Да, там массив. Да, можно зарегать один сервак под разными тегами, именами, всем.
> Это необходимо в сервиса [нотификаций](http://stash.ri.domain:7990/projects/AS/repos/ati.notifications.core/browse/ATI.Notifications.Core.API/appsettings.json), для версионирования старых мобильных приложений, которые ходят без прокси и не выживают после смены контрактов.
Далее осталось только добавить в `Startup.cs` `services.AddConsul()`

---

### Http

За основу взята работа с `HttClientFactory` из `atisu.services.common`, но добавлены следующие extensions:
1. `services.AddConsulHttpClient<TAdapter, TServiceOptions>`

Методы делают почти все то же самое, что и `services.AddCustomHttpClient<>`, но дополнительно:
1. Добавляется `ServiceAsClientName` хедер во все запросы
2. Добавляется `HttpConsulHandler`, который на каждый запрос (retry) получает ip+port инстанса из `ConsulServiceAddress`

---

0 comments on commit 3fc8db1

Please sign in to comment.