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

add http client handlers #26

Merged
merged 15 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
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-httpClientHandlers-rc11" />
<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 @@
/// Обертка, включающая в себя ConsulServiceAddress, MetricsHttpClientWrapper и MetricsTracingFactory
/// </summary>
[PublicAPI]
[Obsolete("Use HttpClientFactory instead")]
public class ConsulMetricsHttpClientWrapper : IDisposable
{
private readonly BaseServiceOptions _serviceOptions;
Expand Down Expand Up @@ -357,7 +358,7 @@
/// It must be used via DI, add it by .AddConsulMetricsHttpClientWrappers()
/// </summary>
[PublicAPI]
public class ConsulMetricsHttpClientWrapper<T> : ConsulMetricsHttpClientWrapper

Check warning on line 361 in ATI.Services.Consul/ConsulMetricsHttpClientWrapper.cs

View workflow job for this annotation

GitHub Actions / build

'ConsulMetricsHttpClientWrapper' is obsolete: 'Use HttpClientFactory instead'

Check warning on line 361 in ATI.Services.Consul/ConsulMetricsHttpClientWrapper.cs

View workflow job for this annotation

GitHub Actions / build

'ConsulMetricsHttpClientWrapper' is obsolete: 'Use HttpClientFactory instead'

Check warning on line 361 in ATI.Services.Consul/ConsulMetricsHttpClientWrapper.cs

View workflow job for this annotation

GitHub Actions / release-preview

'ConsulMetricsHttpClientWrapper' is obsolete: 'Use HttpClientFactory instead'
where T : BaseServiceOptions
{
public ConsulMetricsHttpClientWrapper(
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;
}
}
51 changes: 51 additions & 0 deletions ATI.Services.Consul/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ATI.Services.Consul
## Деплой


### Теги

Выкладка в nuget происходит на основе триггера на тег определённого формата: [как повышать версию](https://learn.microsoft.com/en-us/nuget/concepts/package-versioning)
##### ВАЖНО:
CptnSnail marked this conversation as resolved.
Show resolved Hide resolved
1. Все теги должны начинаться с `v`
2. Для тестовой версии тег должен быть с постфиксом, например `v1.0.1-rc`
3. Релизный тег должен состоять только из цифр версии, например `v1.0.0`

* Создание тега через git(нужно запушить его в origin) [создание тега и пуш в remote](https://git-scm.com/book/en/v2/Git-Basics-Tagging)
* Команды для тестового:
1. `git checkout <название ветки>`
2. `git tag -a <название тега> -m "<описание тега>" `
3. `git push --tags`
* Команды для релизного:
1. `git tag -a <название тега> <SHA коммита> -m "<описание тега>" `
2. `git push --tags`
* Через раздел [releases](https://github.com/atidev/ATI.Services.Consul/releases)(альфа версии нужно помечать соответсвующей галкой).
* При пуше, в некоторых IDE, необходимо отметить чекбокс об отправке тегов


#### Разработка теперь выглядит вот так:
1. Создаем ветку, пушим изменения, создаем pull request.
2. Добавляем на ветку тег с версией изменения
3. Срабатывает workflow билдит и пушит версию(берёт из названия тега) в nuget.
4. По готовности мерджим ветку в master.
5. Тегаем нужный коммит мастера.
Нужно обязательно описать изменения внесённые этим релизом в release notes
Здесь лучше воспользоваться интерфейсом гитхаба, там удобнее редактировать текст.
6. Срабатывает релизный workflow билдит и пушит в нугет релизную версию.
7. В разделе [Releases](https://github.com/atidev/ATI.Services.Consul/releases) появляется информация о нашем релиз и release notes.

---
## Документация

### Http

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

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

---



Loading