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


# TL;DR.
* HttpClientの再試行は[Pollyライブラリ](https://github.com/App-vNext/Polly)を使用するのが一般的
* HttpClient(Factory)とPollyの統合は nugetパッケージ [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) で提供される
* Pollyが適用されたHttpClientをFunctionの入力にバインドする、Azure Function Extentionを作成した
    * 実装例はこちら→[GitHub](https://github.com/HiroyukiSakoh/PollyHttpClient.Azure.Functions)
* **[2019/04/20追記]**Azure Function v2 がDI(コンストラクタ インジェクション)に対応しました。<br>バインディングではないですが、もっとシンプルに実装できます。
    * こちら⇒[Azure Function v2のDIサンプル](https://qiita.com/HiroyukiSakoh/items/e9e472a92a53da4d7568)

# はじめに
こんにちは。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の取り扱いには要注意という話](https://qiita.com/nskhara/items/b7c31d60531ffbe29537)
* [YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE](https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/)
* [開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント](https://www.infoq.com/jp/news/2016/09/HttpClient)

# System.Net.Http.IHttpClientFactory
そういったことを解消するため、
ASP.NET Core 2.1から、HttpClientFactoryが登場しました。
nugetパッケージ [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http/) で提供されています

（つかいかたは だいたい↓のBLOG Part1,2,3,5読めばわかる。）

* [HttpClientFactory in ASP.NET Core 2.1 (Part 1)
An Introduction to HttpClientFactory](https://www.stevejgordon.co.uk/introduction-to-httpclientfactory-aspnetcore)

[Microsoft.Extensions.DependencyInjection.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/)依存なので、[対応するDIコンテナ](https://github.com/aspnet/DependencyInjection#using-other-containers-with-microsoftextensionsdependencyinjection)が必要です。

# Polly
* [Pollyライブラリ](https://github.com/App-vNext/Polly)

ざっくり、一時的な障害の対処系ライブラリ。
３大雑記の一つであるところの、芝村先生のしばやん雑記が詳しい。

* [しばやん雑記 C# と Polly を使って回復力の高いアプリケーションを書く](https://blog.shibayan.jp/entry/20190102/1546425897)

# Polly+HttpClient(Factory)→Microsoft.Extensions.Http.Polly
Polly単体で使うのも悪くないですが、テストが…とかあるので、DIにしてくれるいいやつ。

HttpClient(Factory)とPollyの統合は nugetパッケージ [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) で提供される

（だいたい↓のBLOG Part4読めばわかる。）

* [HttpClientFactory in ASP.NET Core 2.1 (Part 4)
Integrating with Polly for transient fault handling](https://www.stevejgordon.co.uk/httpclientfactory-using-polly-for-transient-fault-handling)

# Azure Function上のHttpClient
公式ドキュメントでは`static`で使いまわすことが推奨されています。

* Docs [Azure Functions で接続を管理する方法/静的クライアントを使用する](https://docs.microsoft.com/ja-jp/azure/azure-functions/manage-connections#use-static-clients) 


# Azure Function Extention
staticだとテストが…とか、staticだとDNSの問題が…とかあるので、
HttpClientFactoryを使いたい。再試行もPolly使いたいし。

でも、Azure FunctionでDI使うのはとてもハードルが高い。
そこで、Functionの入力引数にHttpClientをバインドするExtentionを作ります。

Azure Function Extentionと聞いて敷居が高い気もしますが、
入力バインドであればとてもシンプルです。たった3クラス数行。

出来上がったものはこちら→[GitHub](https://github.com/HiroyukiSakoh/PollyHttpClient.Azure.Functions)

## バインド用属性クラス
宣言だけ。

```csharp: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の登録をします。

```csharp: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で受け取り。

```csharp: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 を引数に宣言。

```csharp: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を生成するようになった？ため？
らしい…[こうしたら](https://github.com/HiroyukiSakoh/PollyHttpClient.Azure.Functions/blob/833b01ee8543f517ffd31f87f86d7948118ef4c9/PollyHttpClient.Azure.WebJobs.Extensions/PollyHttpClientWebJobsStartup.cs#L52)治った。けど、子スコープってなに…？

Azure上では回し続けててエラーが出てなかった、Runtimeが1個前のまま動いてて、再起動したら無事死にました。
`~2`とか設定した場合の、Runtimeの更新契機っていつでしょう…？
本番適用前だったので良かったですが、知らないうちにRuntime上がって死ぬとか悪夢
Runtime固定して、手動でアップデートするのがセオリーなんですかね…？皆さんどうしてますか？

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

* [azure-functions-host issue#3737 Dependency Injection support for Functions](https://github.com/Azure/azure-functions-host/issues/3737)
* [azure-functions-host issue#3736 Built in support for HttpClientFactory](https://github.com/Azure/azure-functions-host/issues/3736)

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

* [しばやん雑記 Azure Functions v2 でインスタンスメソッドも Function として利用可能に](https://blog.shibayan.jp/entry/20190120/1547967300)

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

…確定申告しよっと。

# 参考
* [Alternative approach to HttpClient in Azure Functions 2.0 - bringing in HttpClientFactory](https://www.tpeczek.com/2018/12/alternative-approach-to-httpclient-in.html)
* [HttpClientFactory を使用して回復力の高い HTTP 要求を実装する](https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests)
* [HttpClientFactory ポリシーと Polly ポリシーで指数バックオフを含む HTTP 呼び出しの再試行を実装する](https://docs.microsoft.com/ja-jp/dotnet/standard/microservices-architecture/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly)