TL;DR.
- HttpClientの再試行はPollyライブラリを使用するのが一般的
- HttpClient(Factory)とPollyの統合は nugetパッケージ Microsoft.Extensions.Http.Polly で提供される
- Pollyが適用されたHttpClientをFunctionの入力にバインドする、Azure Function Extentionを作成した
- 実装例はこちら→GitHub
- **[2019/04/20追記]**Azure Function v2 がDI(コンストラクタ インジェクション)に対応しました。
バインディングではないですが、もっとシンプルに実装できます。
はじめに
こんにちは。Azure Function v2使ってますか。
最近ようやく.NET Core化しつつあるのですが、依存関係で躓くことが多いです。
.Net Coreとか、ASP.NET Core 2.xとか、.NET Standardとか、追いきれないし、
SDKが対応してなかったり、依存関係地獄。
今度はCore 3系と、ASP.NET Core 2.2とか?
.NET 1.1と2.0しかない世界は平和だったと思う。
超図解 .NET Core エコシステムの全貌 的な本ないですかね。
今回はAzure Function上でのHttpClientのはなしです。
System.Net.Http.HttpClient
HttpClientは簡単なようで扱い方が難しいクラスです。
具体的になにが難しいかは、昔からいろんな方がたくさん書かれているので、そちらに。
- .NETのHttpClientの取り扱いには要注意という話
- YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE
- 開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント
System.Net.Http.IHttpClientFactory
そういったことを解消するため、
ASP.NET Core 2.1から、HttpClientFactoryが登場しました。
nugetパッケージ Microsoft.Extensions.Http で提供されています
(つかいかたは だいたい↓のBLOG Part1,2,3,5読めばわかる。)
Microsoft.Extensions.DependencyInjection.Abstractions依存なので、対応するDIコンテナが必要です。
Polly
ざっくり、一時的な障害の対処系ライブラリ。
3大雑記の一つであるところの、芝村先生のしばやん雑記が詳しい。
Polly+HttpClient(Factory)→Microsoft.Extensions.Http.Polly
Polly単体で使うのも悪くないですが、テストが…とかあるので、DIにしてくれるいいやつ。
HttpClient(Factory)とPollyの統合は nugetパッケージ Microsoft.Extensions.Http.Polly で提供される
(だいたい↓のBLOG Part4読めばわかる。)
Azure Function上のHttpClient
公式ドキュメントではstatic
で使いまわすことが推奨されています。
Azure Function Extention
staticだとテストが…とか、staticだとDNSの問題が…とかあるので、
HttpClientFactoryを使いたい。再試行もPolly使いたいし。
でも、Azure FunctionでDI使うのはとてもハードルが高い。
そこで、Functionの入力引数にHttpClientをバインドするExtentionを作ります。
Azure Function Extentionと聞いて敷居が高い気もしますが、
入力バインドであればとてもシンプルです。たった3クラス数行。
出来上がったものはこちら→GitHub
バインド用属性クラス
宣言だけ。
namespace PollyHttpClient.Azure.WebJobs.Extensions.Bindings
{
/// <summary>
/// Attribute used to bind to a <see cref="HttpClient"/> instance.
/// </summary>
[Binding]
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
public sealed class PollyHttpClientAttribute : Attribute
{ }
}
WebJobsStartupクラス
Azure Functionsランタイムが検出するExtentionのエントリポイントと、その処理
Extentionの追加と、HttpClientFactoryの登録をします。
[assembly: WebJobsStartup(typeof(PollyHttpClientWebJobsStartup))]
namespace PollyHttpClient.Azure.WebJobs.Extensions
{
public class PollyHttpClientWebJobsStartup : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
//Extentionの登録
builder.AddExtension<PollyHttpClientExtensionConfigProvider>();
//HttpClient+Pollyの登録
builder.Services.AddHttpClient<PollyHttpClientExtensionConfigProvider>(nameof(PollyHttpClientExtensionConfigProvider))
.SetHandlerLifetime(System.Threading.Timeout.InfiniteTimeSpan)
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound || (int)msg.StatusCode == 429)
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
))
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));
builder.Services.Configure<HttpClientFactoryOptions>(nameof(PollyHttpClientExtensionConfigProvider), options => options.SuppressHandlerScope = true);
}
}
}
ExtensionConfigProvider
バインディングルールの設定
HttpClientFactoryをDIで受け取り。
namespace PollyHttpClient.Azure.WebJobs.Extensions.Config
{
[Extension("PollyHttpClient")]
internal class PollyHttpClientExtensionConfigProvider : IExtensionConfigProvider
{
private readonly IHttpClientFactory httpClientFactory;
public PollyHttpClientExtensionConfigProvider(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}
public void Initialize(ExtensionConfigContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// HttpClientFactory Bindings
var bindingAttributeBindingRule = context.AddBindingRule<PollyHttpClientAttribute>();
bindingAttributeBindingRule.BindToInput<HttpClient>((httpClientFactoryAttribute) =>
{
return httpClientFactory.CreateClient(nameof(PollyHttpClientExtensionConfigProvider));
});
}
}
}
使い方
PollyHttpClient属性を付けた、HttpClient を引数に宣言。
public static class Function1
{
[FunctionName("Function1")]
public static async Task Run(
[TimerTrigger("0 */5 * * * *")]TimerInfo myTimer
, [PollyHttpClient]HttpClient httpClient
, ILogger log)
{
log.LogInformation("start func");
await SendRequest(httpClient, log, "https://httpstat.us/200");
await SendRequest(httpClient, log, "https://httpstat.us/500");
await SendRequest(httpClient, log, "https://httpstat.us/429");
//await SendRequest(httpClient, log, "https://httpstat.us/200?sleep=11000");
log.LogInformation("finish func");
}
private static async Task SendRequest(HttpClient httpClient, ILogger log, string url)
{
try
{
using (HttpResponseMessage response = await httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
}
}
catch (HttpRequestException ex)
{
log.LogError(ex, "HttpRequestException");
}
catch (Exception ex)
{
log.LogError(ex, "Exception");
}
}
}
ハマったポイント
ASP.NET Core 2.1のころ動作してて、このエントリを書こうとローカルで動かしてみたら、以下のエラーが
Microsoft.Azure.WebJobs.Host: Exception binding parameter 'httpClient'.
Microsoft.Extensions.DependencyInjection.Abstractions:
No service for type 'Microsoft.Extensions.Http.HttpMessageHandlerBuilder' has been registered.
Azure Functions Runtimeが2.0.12265からASP.NET Core 2.2になっていて、
HttpClientFactoryが子スコープでHttpMessageHandlerBuilderを生成するようになった?ため?
らしい…こうしたら治った。けど、子スコープってなに…?
Azure上では回し続けててエラーが出てなかった、Runtimeが1個前のまま動いてて、再起動したら無事死にました。
~2
とか設定した場合の、Runtimeの更新契機っていつでしょう…?
本番適用前だったので良かったですが、知らないうちにRuntime上がって死ぬとか悪夢
Runtime固定して、手動でアップデートするのがセオリーなんですかね…?皆さんどうしてますか?
おわりに
今回作ったものは、以下が解決した場合、不要となるハズ。
- azure-functions-host issue#3737 Dependency Injection support for Functions
- azure-functions-host issue#3736 Built in support for HttpClientFactory
ここまで書いて、今更ですが、コンストラクタインジェクションができるようになったそうで。
バインディングじゃなくて、型付HttpClientをコンテナに入れるだけで良い気がする。
ふーむ。
…確定申告しよっと。