Help us understand the problem. What is going on with this article?

Azure Function v2にHttpClientFactory+Pollyを組み込み、HttpClientの再試行を実装する

More than 1 year has passed since last update.

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は簡単なようで扱い方が難しいクラスです。
具体的になにが難しいかは、昔からいろんな方がたくさん書かれているので、そちらに。

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

バインド用属性クラス

宣言だけ。

PollyHttpClientAttribute.cs
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の登録をします。

PollyHttpClientWebJobsStartup.cs
[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で受け取り。

PollyHttpClientExtensionConfigProvider.cs
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 を引数に宣言。

Function1.cs
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固定して、手動でアップデートするのがセオリーなんですかね…?皆さんどうしてますか?

おわりに

今回作ったものは、以下が解決した場合、不要となるハズ。

ここまで書いて、今更ですが、コンストラクタインジェクションができるようになったそうで。

バインディングじゃなくて、型付HttpClientをコンテナに入れるだけで良い気がする。
ふーむ。

…確定申告しよっと。

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away