[インデックス 14471] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http/httptest
パッケージにおける、テストサーバーのポート再利用に関する問題を解決するためのものです。特にBSD系のOSで顕著な、ポートの早期再利用によるテストの不安定性を改善することを目的としています。
コミット
commit f97bb12bb02b1a5dd0e36032c8079e019fef9d54
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun Nov 25 15:23:20 2012 -0800
net/http/httptest: protect against port reuse
Should make BSDs more reliable. (they seem to reuse ports
quicker than Linux)
Tested by hand with local modifications to force reuse on
Linux. (net/http tests failed before, pass now) Details in the
issue.
Fixes #4436
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/6847101
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f97bb12bb02b1a5dd0e36032c8079e019fef9d54
元コミット内容
net/http/httptest
: ポート再利用からの保護
BSD系OSでの信頼性を向上させるはずです。(Linuxよりもポートを早く再利用するようです)
Linux上で再利用を強制するためのローカル変更を加えて手動でテストしました。(net/http
のテストは以前は失敗していましたが、現在は合格します)詳細はissueを参照してください。
Fixes #4436
R=golang-dev, minux.ma CC=golang-dev https://golang.org/cl/6847101
変更の背景
この変更の背景には、net/http/httptest
パッケージを使用してテストサーバーを起動・停止する際に発生する、ポートの再利用に関する問題があります。特にBSD系のオペレーティングシステム(macOSなど)では、TCPポートが閉じられた後、すぐに再利用可能になる特性があります。これにより、テストケースが連続して実行される場合、前のテストで使用したポートが完全に解放される前に次のテストが同じポートを使用しようとしてしまい、address already in use
のようなエラーが発生し、テストが不安定になることがありました。
コミットメッセージにもあるように、Linuxではこの問題は比較的発生しにくいものの、手動でポート再利用を強制するような状況を作り出すと、同様の問題が発生することが確認されています。このコミットは、このようなテストの不安定性を解消し、特にBSD系OSでのテストの信頼性を向上させることを目的としています。
前提知識の解説
TCPポートのライフサイクルとTIME_WAIT状態
TCP接続が閉じられる際、通常は「TIME_WAIT」状態という期間が存在します。これは、接続が完全に終了し、ネットワーク上に残っている可能性のある遅延パケットが到着するのを待つための状態です。このTIME_WAIT状態の期間中、そのポートは他の新しい接続のためにすぐに再利用することはできません。
オペレーティングシステムによって、このTIME_WAIT状態の期間や、ポートの再利用に関するポリシーが異なります。
- Linux: デフォルトでは、TIME_WAIT状態のポートは比較的長い期間(通常は数分)保持され、すぐに再利用されることは稀です。ただし、
SO_REUSEADDR
ソケットオプションを使用することで、TIME_WAIT状態のポートでも再利用を許可することができます。 - BSD系OS (macOSなど): BSD系のOSでは、Linuxに比べてTIME_WAIT状態のポートがより早く再利用される傾向があります。これは、テスト環境などで短期間に多数の接続が確立・切断される場合に、ポートの競合を引き起こしやすくなります。
net/http/httptest
パッケージ
net/http/httptest
は、Go言語の net/http
パッケージのテストを容易にするためのユーティリティパッケージです。主に以下の2つの主要な機能を提供します。
httptest.NewServer
: HTTPサーバーをテスト目的で起動し、そのサーバーのURLを返します。これにより、実際のネットワーク接続を介してHTTPハンドラをテストできます。httptest.NewRecorder
: HTTPレスポンスを記録するためのhttp.ResponseWriter
の実装を提供します。これにより、HTTPハンドラが生成するレスポンスの内容を簡単に検証できます。
テストサーバーは通常、テストのセットアップフェーズで起動され、テストの終了時に Close()
メソッドを呼び出してシャットダウンされます。この Close()
メソッドの挙動が、ポート再利用の問題に直結します。
http.DefaultTransport
と CloseIdleConnections()
Go言語の net/http
パッケージでは、HTTPクライアントがリクエストを送信する際に、内部的に http.Transport
という構造体を使用します。http.Transport
は、HTTP接続の確立、接続の再利用(Keep-Alive)、TLSハンドシェイクなどを管理します。
http.DefaultTransport
は、http.Client
がデフォルトで使用する http.Transport
のインスタンスです。多くのHTTPクライアントは、明示的に http.Transport
を設定しない限り、この http.DefaultTransport
を使用します。
http.Transport
には CloseIdleConnections()
というメソッドがあります。このメソッドは、現在アイドル状態(使用されていない)のHTTP接続をすべて閉じます。HTTP/1.1のKeep-Alive機能により、一度確立されたTCP接続は、次のリクエストのために再利用されることがあります。しかし、テストの終了時など、これらのアイドル接続が残っていると、関連するポートが解放されず、次のテストで同じポートを使用しようとした際に問題を引き起こす可能性があります。CloseIdleConnections()
を呼び出すことで、これらのアイドル接続を明示的に閉じ、関連するリソース(ポートなど)を解放することができます。
技術的詳細
このコミットは、httptest.Server
の Close()
メソッドに2つの重要な変更を加えることで、ポート再利用の問題に対処しています。
-
s.CloseClientConnections()
の呼び出し:httptest.Server
は、テスト中にクライアントからの接続を受け付けます。これらの接続は、テストが終了してもすぐに閉じられない場合があります。CloseClientConnections()
メソッドは、httptest.Server
が管理しているすべてのクライアント接続を強制的に閉じます。これにより、これらの接続が使用していたポートが解放される可能性が高まります。 -
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
の呼び出し: これはより重要な変更点です。httptest.Server
を使用するテストでは、テストコード内でHTTPクライアント(例えばhttp.Get
やhttp.Client
のインスタンス)を使用して、テストサーバーにリクエストを送信することが一般的です。これらのクライアントは、デフォルトでhttp.DefaultTransport
を使用し、Keep-Alive機能によって接続を再利用しようとします。 テストが終了し、httptest.Server
が閉じられても、http.DefaultTransport
が保持しているアイドル接続は自動的には閉じられません。これらのアイドル接続が閉じられない限り、関連するポートはTIME_WAIT状態に入ることができず、次のテストで同じポートが使用されることを妨げます。 この変更では、http.DefaultTransport
が*http.Transport
型である場合に限り、そのCloseIdleConnections()
メソッドを呼び出しています。これにより、テストサーバーとの間で確立されたアイドル状態のHTTP接続が明示的に閉じられ、ポートがより迅速に解放されるようになります。
これらの変更により、テストサーバーがシャットダウンされる際に、関連するすべてのネットワークリソース(特にポート)が確実に解放されるようになり、連続するテスト実行におけるポート競合の問題が大幅に軽減されます。
コアとなるコードの変更箇所
変更は src/pkg/net/http/httptest/server.go
ファイルの Server
型の Close()
メソッドにあります。
--- a/src/pkg/net/http/httptest/server.go
+++ b/src/pkg/net/http/httptest/server.go
@@ -155,6 +155,10 @@ func NewTLSServer(handler http.Handler) *Server {
func (s *Server) Close() {
s.Listener.Close()
s.wg.Wait()
+ s.CloseClientConnections()
+ if t, ok := http.DefaultTransport.(*http.Transport); ok {
+ t.CloseIdleConnections()
+ }
}
// CloseClientConnections closes any currently open HTTP connections
コアとなるコードの解説
func (s *Server) Close()
メソッドは、httptest.Server
インスタンスをシャットダウンする際に呼び出されます。
-
s.Listener.Close()
: これは、テストサーバーがリッスンしていたネットワークリスナーを閉じます。これにより、新しい接続の受け入れが停止されます。 -
s.wg.Wait()
:s.wg
はsync.WaitGroup
であり、サーバーが処理中のリクエストがすべて完了するのを待ちます。これにより、進行中のリクエストが中断されることなく、 gracefully にシャットダウンされます。 -
s.CloseClientConnections()
: この行が追加されました。これは、テストサーバーが現在保持しているすべてのクライアント接続(HTTP/1.1 Keep-Aliveなどで開かれたままになっている接続)を強制的に閉じます。これにより、これらの接続が占有していたポートが解放されます。 -
if t, ok := http.DefaultTransport.(*http.Transport); ok { t.CloseIdleConnections() }
: このブロック全体が追加されました。http.DefaultTransport
はhttp.RoundTripper
インターフェース型ですが、通常は*http.Transport
型のインスタンスです。型アサーション.(*http.Transport)
を使用して、それが実際に*http.Transport
型であるかどうかを確認しています。- もし
http.DefaultTransport
が*http.Transport
型であれば、そのCloseIdleConnections()
メソッドが呼び出されます。このメソッドは、http.DefaultTransport
が管理しているアイドル状態のHTTP接続プールをクリアし、すべてのアイドル接続を閉じます。これにより、テストコード内でhttp.DefaultClient
などを使用してテストサーバーにリクエストを送信した際に確立されたKeep-Alive接続が閉じられ、関連するポートが解放されます。
これらの追加された2行、特に t.CloseIdleConnections()
の呼び出しは、テストサーバーが使用していたポートが、次のテストが開始される前に確実に解放されるようにするために非常に重要です。これにより、特にポートの再利用が早いBSD系OSでのテストの信頼性が向上します。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go言語のコードレビューシステム (Gerrit): https://golang.org/cl/6847101
- Go言語の
net/http/httptest
パッケージのドキュメント: https://pkg.go.dev/net/http/httptest - Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http
参考にした情報源リンク
- コミットメッセージの内容
- Go言語の
net/http
およびnet/http/httptest
パッケージのソースコード - TCP TIME_WAIT状態に関する一般的な知識
- Go言語のIssueトラッカー (Issue #4436の詳細は見つかりませんでしたが、コミットメッセージからその存在が示唆されています)
- (注: 検索ではGo言語のIssue #4436の直接的な詳細を見つけることができませんでした。しかし、コミットメッセージに記載されているため、過去に存在した問題であることは確かです。)