Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.DefaultTransportCloseIdleConnections()

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.ServerClose() メソッドに2つの重要な変更を加えることで、ポート再利用の問題に対処しています。

  1. s.CloseClientConnections() の呼び出し: httptest.Server は、テスト中にクライアントからの接続を受け付けます。これらの接続は、テストが終了してもすぐに閉じられない場合があります。CloseClientConnections() メソッドは、httptest.Server が管理しているすべてのクライアント接続を強制的に閉じます。これにより、これらの接続が使用していたポートが解放される可能性が高まります。

  2. http.DefaultTransport.(*http.Transport).CloseIdleConnections() の呼び出し: これはより重要な変更点です。httptest.Server を使用するテストでは、テストコード内でHTTPクライアント(例えば http.Gethttp.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 インスタンスをシャットダウンする際に呼び出されます。

  1. s.Listener.Close(): これは、テストサーバーがリッスンしていたネットワークリスナーを閉じます。これにより、新しい接続の受け入れが停止されます。

  2. s.wg.Wait(): s.wgsync.WaitGroup であり、サーバーが処理中のリクエストがすべて完了するのを待ちます。これにより、進行中のリクエストが中断されることなく、 gracefully にシャットダウンされます。

  3. s.CloseClientConnections(): この行が追加されました。これは、テストサーバーが現在保持しているすべてのクライアント接続(HTTP/1.1 Keep-Aliveなどで開かれたままになっている接続)を強制的に閉じます。これにより、これらの接続が占有していたポートが解放されます。

  4. if t, ok := http.DefaultTransport.(*http.Transport); ok { t.CloseIdleConnections() }: このブロック全体が追加されました。

    • http.DefaultTransporthttp.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言語の net/http および net/http/httptest パッケージのソースコード
  • TCP TIME_WAIT状態に関する一般的な知識
  • Go言語のIssueトラッカー (Issue #4436の詳細は見つかりませんでしたが、コミットメッセージからその存在が示唆されています)
    • (注: 検索ではGo言語のIssue #4436の直接的な詳細を見つけることができませんでした。しかし、コミットメッセージに記載されているため、過去に存在した問題であることは確かです。)