HttpClientをusingしたらちゃんと動かなかった話
1. はじめに
皆さんこんにちは。
新卒エンジニアの井上です。
本日はちょっとした業務中の技術面の失敗談をお話したいと思います。
2. 失敗談
背景
とある案件で、「実際にHTTP経由でアクセスを行い、200-299のステータスコードが返ってきたら成功した旨、それ以外であれば失敗した旨をDBに記録する」といったバッチを作成していました。
処理もそこまで複雑ではないため、設計して、コードを書いて、テスト完了するまで大体1週間もかからないくらいです。
工数にして30から40。大したことないですね。
実装
public class Hoge
{
private static bool UrlExists(string url)
{
using (var client = new HttpClient())
{
var response = client.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(url))).Result;
return response.IsSuccessStatusCode;
}
}
}
実際のコードではないですが、私が実装したのはこんな感じです。
Hogeクラスにはバッチ処理の大体の流れが記述されていました。
HttpClientはIDisposableを実装しているため、usingステートメントが使えます。
リソースの開放漏れを防ぐため、基本的に使えるなら使ったほうが良いですし、HttpClientについてGoogleで検索しても、こんな感じのサンプルコードが出てきたりします。
何が起こったか?
私はこれを作った後、他の案件にアサインされていたため、テスト環境へのデプロイと動作確認は他のメンバーに行ってもらいました。
しばらくすると、こんなチャットが飛んできました…
完全になんにも動かないならまだわかりますが、「途中で失敗」となると何が起こったのかわかりません。
チャットに貼られていたスタックトレースを見ると、どうやらHttpClient#SendAsync()で落ちています。
そして色々調べていると、「HttpClientのアンチパターン」という名目で、私が書いたようなものと同じような書き方が出てきました。
アンチパターンを実装してしまったみたいです。
原因
さて、usingステートメントは、これを抜けたとき(あるいは例外が発生したとき)に自動的にDispose()メソッドの呼び出しをします。
途中で例外が発生してしまうような状況でも、確実にDisposeすることができます。
ここでHttpClientの話に移りますが、HttpClientはインスタンスが生成されたときにその時点で利用可能なソケットをオープンします。
そして、Dispose()が呼ばれると、インスタンス生成時にオープンしたソケットをクローズします。
しかし、ソケットはクローズした時点では「TIME_WAIT」状態となります。
これが「CLOSED」になるにはしばらく時間がかかります。
…もうおわかりでしょうか?
ループ処理などでHttpClientのインスタンス生成/Disposeを繰り返してしまうと、TIME_WAIT状態のソケットが大量に生まれます。
直にバッチ処理は利用可能なソケットを全て使い果たし、例外で落ちるのでした…
対策
Microsoft Docを見ると、「HttpClientはアプリケーションのライフサイクルを通して再利用されることを目的としています」とあります。
つまり、アプリが起動した時点でインスタンス生成を行い、アプリの終了に伴ってDisposeしてください、ということです。
なるほど…わかりました。
public class Hoge2 : IDisposable
{
private m_httpClient;
public Hoge2()
{
m_httpClient = new HttpClient();
}
private static bool UrlExists(string url)
{
var response = m_httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, new Uri(url))).Result;
return response.IsSuccessStatusCode;
}
public void Dispose()
{
m_httpClient.Dispose();
}
}
(Hoge2はバッチ処理の中で一度しかインスタンス生成されません。)
これで、HttpClientはできるだけ使い回されるようになりました。
短いバッチ処理のため、処理クラスにIDisposableを実装し、利用側(Mainメソッド)でusingするようにしました。
今のところ本番までデプロイされて動いてるようですが、不具合報告はまだ来ていません。(知らないだけかも)
3. まとめ
この記事を書く際、HttpClientについて調べ直してみたんですが、同じような実装をしてしまってハマっている人が結構見つかりました…
Microsoftが詳しいドキュメントをちゃんと用意してくれているので、なにか使うときは一通り目を通しておいたほうがこのようなことは防げたのかなと思います。
また、私が大量データを想定したテストまでは実施しなかったのもミスの一つです。実施要否の判断って難しいところではあるんですが…
いつの間にか入社してから8ヶ月が経ち、わかることも多くなってきたところですが、それ以上にわからないことがあるのだと気づきました。
これからもたくさん失敗をして乗り越えて成長していきたいですね。?