[インデックス 15344] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内の transport_test.go
ファイルに対する変更です。具体的には、テストの信頼性を向上させるための修正が含まれています。
コミット
commit a2ade45205e24b80a1242f5d8cd41f343e969bcd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 20 16:39:33 2013 -0800
net/http: improve test reliability
Fixes #4852
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7374045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a2ade45205e24b80a1242f5d8cd41f343e969bcd
元コミット内容
net/http: improve test reliability
このコミットは、net/http
パッケージのテストの信頼性を向上させることを目的としています。具体的には、テストが不安定になる原因となっていたタイムアウト処理を改善し、テストがより安定して成功するように修正しています。
変更の背景
このコミットは、GoのIssue #4852 を修正するために行われました。Issue #4852 は、「TestIssue4191_InfiniteGetTimeout
と TestIssue4191_InfiniteGetToPutTimeout
が時々失敗する」という報告です。これらのテストは、HTTPクライアントが無限にデータを送信するサーバーに対してタイムアウトが適切に機能するかどうかを検証するものです。
元のテストでは、コネクションのデッドライン(タイムアウト)を固定値(100ミリ秒)に設定していました。しかし、テスト環境の負荷やネットワークの状況によっては、この短いタイムアウトでは不十分であり、テストが不安定に失敗することがありました。特に、CI/CD環境のような共有リソースを使用する環境では、このような不安定なテストは開発効率を著しく低下させます。
この問題に対処するため、テストのタイムアウト処理をより堅牢にし、一時的なネットワークの遅延やシステム負荷の増加によってテストが失敗する可能性を低減する必要がありました。
前提知識の解説
Go言語の net/http
パッケージ
net/http
パッケージは、Go言語におけるHTTPクライアントおよびサーバーの実装を提供します。Webアプリケーションの開発において中心的な役割を果たすパッケージであり、HTTPリクエストの送信、レスポンスの受信、HTTPサーバーの構築など、多岐にわたる機能を提供します。
http.Client
と http.Transport
http.Client
: HTTPリクエストを送信するための高レベルなインターフェースを提供します。通常、Get
,Post
,Do
などのメソッドを通じてHTTP通信を行います。http.Transport
:http.Client
の内部で実際にネットワーク通信を行う低レベルなコンポーネントです。コネクションの確立、再利用、プロキシ設定、TLS設定、タイムアウト処理などを担当します。このコミットで変更されているのは、Transport
の内部で設定されるコネクションのデッドラインに関する部分です。
net.Conn.SetDeadline()
Go言語の net
パッケージには、ネットワークコネクション(net.Conn
インターフェース)に対して読み書きのデッドラインを設定するための SetDeadline(t time.Time)
メソッドがあります。このメソッドは、指定された時刻までに読み書き操作が完了しない場合にエラーを発生させ、操作を中断させることができます。これは、ネットワーク通信におけるハングアップを防ぎ、リソースの解放を確実にするために非常に重要です。
テストの信頼性(Test Reliability)
ソフトウェア開発において、テストの信頼性は非常に重要です。信頼性の低いテスト(Flaky Test、不安定なテスト)とは、コードの変更がないにもかかわらず、実行するたびに成功したり失敗したりするテストのことです。このようなテストは、開発者が実際のバグとテストの不安定さを区別することを困難にし、テストに対する信頼を損ない、結果として開発プロセスを遅らせる原因となります。不安定なテストの一般的な原因には、並行処理の競合状態、外部リソース(ネットワーク、データベース)の不安定性、不適切なタイムアウト設定などがあります。
技術的詳細
このコミットは、net/http/transport_test.go
内の TestIssue4191_InfiniteGetTimeout
と TestIssue4191_InfiniteGetToPutTimeout
という2つのテスト関数に焦点を当てています。これらのテストは、HTTPクライアントがサーバーからの応答を待つ際に、設定されたタイムアウトが正しく機能するかどうかを検証します。
元の実装では、クライアントコネクションのデッドラインを 100 * time.Millisecond
という固定値で設定していました。
conn.SetDeadline(time.Now().Add(100 * time.Millisecond))
この固定値が、テストが不安定になる原因でした。特に、テストが実行される環境の負荷やネットワークのレイテンシによっては、100ミリ秒では不十分な場合があり、タイムアウトエラーが発生してテストが失敗することがありました。
このコミットでは、この問題を解決するために以下の変更を導入しています。
- 可変タイムアウトの導入:
timeout
という変数を導入し、初期値を100 * time.Millisecond
とします。コネクションのデッドラインはこのtimeout
変数を使用して設定されます。timeout := 100 * time.Millisecond // ... conn.SetDeadline(time.Now().Add(timeout))
- リトライロジックの追加:
client.Get
の呼び出しがエラーになった場合、getFailed
というフラグをチェックします。- もし
getFailed
がfalse
であれば(つまり、最初の失敗であれば)、timeout
の値を10倍に増やし、i--
を実行してループカウンタをデクリメントし、continue
で次のイテレーションに進みます。これにより、テストはより長いタイムアウトで再試行されます。 getFailed
がtrue
であれば(つまり、タイムアウトを増やしても再度失敗した場合は)、エラーとして報告し、テストを終了します。
- もし
このリトライロジックにより、テストは一度のタイムアウト失敗で即座にエラーとなるのではなく、より長いタイムアウトで再試行する機会を得ます。これにより、一時的なネットワークの遅延やシステム負荷の増加によるテストの失敗を吸収し、テストの信頼性を大幅に向上させることができます。
nRuns
はテストの実行回数を制御しており、testing.Short()
が有効な場合は1回、それ以外の場合は5回実行されます。このリトライロジックは、これらの複数回の実行の中で、最初の失敗時にのみタイムアウトを延長するようになっています。
コアとなるコードの変更箇所
変更は src/pkg/net/http/transport_test.go
ファイルに集中しています。
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -970,6 +970,7 @@ func TestIssue4191_InfiniteGetTimeout(t *testing.T) {
\tio.Copy(w, neverEnding('a'))
})\n \tts := httptest.NewServer(mux)\n+\ttimeout := 100 * time.Millisecond
\n \tclient := &Client{\n \t\tTransport: &Transport{\n@@ -978,7 +979,7 @@ func TestIssue4191_InfiniteGetTimeout(t *testing.T) {\n \t\t\t\tif err != nil {\n \t\t\t\t\treturn nil, err\n \t\t\t\t}\n-\t\t\t\tconn.SetDeadline(time.Now().Add(100 * time.Millisecond))\n+\t\t\t\tconn.SetDeadline(time.Now().Add(timeout))\n \t\t\t\tif debug {\n \t\t\t\t\tconn = NewLoggingConn("client", conn)\n \t\t\t\t}\n@@ -988,6 +989,7 @@ func TestIssue4191_InfiniteGetTimeout(t *testing.T) {\n \t\t},\n \t}\n \n+\tgetFailed := false
\tnRuns := 5\n \tif testing.Short() {\n \t\tnRuns = 1\n@@ -998,6 +1000,14 @@ func TestIssue4191_InfiniteGetTimeout(t *testing.T) {\n \t\t}\n \t\tsres, err := client.Get(ts.URL + "/get")\n \t\tif err != nil {\n+\t\t\tif !getFailed {\n+\t\t\t\t// Make the timeout longer, once.\n+\t\t\t\tgetFailed = true\n+\t\t\t\tt.Logf("increasing timeout")\n+\t\t\t\ti--\n+\t\t\t\ttimeout *= 10\n+\t\t\t\tcontinue\n+\t\t\t}\n \t\t\tt.Errorf("Error issuing GET: %v", err)\n \t\t\tbreak\n \t\t}\n@@ -1024,6 +1034,7 @@ func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) {\n \t\tio.Copy(ioutil.Discard, r.Body)\n \t})\n \tts := httptest.NewServer(mux)\n+\ttimeout := 100 * time.Millisecond
\n \tclient := &Client{\n \t\tTransport: &Transport{\n@@ -1032,7 +1043,7 @@ func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) {\n \t\t\t\tif err != nil {\n \t\t\t\t\treturn nil, err\n \t\t\t\t}\n-\t\t\t\tconn.SetDeadline(time.Now().Add(100 * time.Millisecond))\n+\t\t\t\tconn.SetDeadline(time.Now().Add(timeout))\n \t\t\t\tif debug {\n \t\t\t\t\tconn = NewLoggingConn("client", conn)\n \t\t\t\t}\n@@ -1042,6 +1053,7 @@ func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) {\n \t\t},\n \t}\n \n+\tgetFailed := false
\tnRuns := 5\n \tif testing.Short() {\n \t\tnRuns = 1\n@@ -1052,6 +1064,14 @@ func TestIssue4191_InfiniteGetToPutTimeout(t *testing.T) {\n \t\t}\n \t\tsres, err := client.Get(ts.URL + "/get")\n \t\tif err != nil {\n+\t\t\tif !getFailed {\n+\t\t\t\t// Make the timeout longer, once.\n+\t\t\t\tgetFailed = true\n+\t\t\t\tt.Logf("increasing timeout")\n+\t\t\t\ti--\n+\t\t\t\ttimeout *= 10\n+\t\t\t\tcontinue\n+\t\t\t}\n \t\t\tt.Errorf("Error issuing GET: %v", err)\n \t\t\tbreak\n \t\t}\n```
## コアとなるコードの解説
### `TestIssue4191_InfiniteGetTimeout` および `TestIssue4191_InfiniteGetToPutTimeout`
これらのテスト関数は、HTTPクライアントがサーバーから無限にデータを読み取ろうとしたときに、設定されたタイムアウトが正しく機能するかどうかを検証します。
1. **`timeout` 変数の導入**:
```go
timeout := 100 * time.Millisecond
```
コネクションのデッドラインを設定するために使用される `timeout` 変数が導入されました。これにより、タイムアウト値を動的に変更できるようになります。
2. **`conn.SetDeadline(time.Now().Add(timeout))`**:
コネクションのデッドラインが、新しく導入された `timeout` 変数に基づいて設定されます。
3. **`getFailed` フラグの導入**:
```go
getFailed := false
```
これは、`client.Get` の呼び出しが一度でも失敗したかどうかを追跡するためのブーリアンフラグです。
4. **リトライロジック**:
```go
if err != nil {
if !getFailed {
// Make the timeout longer, once.
getFailed = true
t.Logf("increasing timeout")
i-- // Decrement loop counter to re-run with increased timeout
timeout *= 10
continue
}
t.Errorf("Error issuing GET: %v", err)
break
}
```
* `client.Get` がエラーを返した場合(通常はタイムアウトによるもの)、このブロックに入ります。
* `!getFailed` の条件は、これが最初の失敗であるかどうかをチェックします。
* 最初の失敗の場合:
* `getFailed` を `true` に設定し、タイムアウトが既に延長されたことを記録します。
* `t.Logf("increasing timeout")` で、タイムアウトが延長されたことをテストログに出力します。
* `i--` は、`for` ループのカウンタ `i` をデクリメントします。これにより、現在のイテレーションが実質的に再実行され、`nRuns` 回の試行回数を消費せずに、延長されたタイムアウトで再度 `client.Get` が試行されます。
* `timeout *= 10` は、タイムアウト値を10倍に増やします。これにより、次の試行ではより長い時間待機するようになります。
* `continue` は、現在のループの残りの部分をスキップし、次のイテレーションに進みます。これにより、延長されたタイムアウトで `client.Get` が再試行されます。
* `getFailed` が既に `true` の場合(つまり、タイムアウトを延長しても再度失敗した場合):
* `t.Errorf("Error issuing GET: %v", err)` でエラーを報告し、テストを失敗させます。
* `break` でループを終了します。
この変更により、テストは一時的なタイムアウトエラーに対してより寛容になり、テストの実行環境に起因する不安定な失敗を減らすことができます。これは、テストの信頼性を高め、開発者が実際のバグに集中できるようにするために非常に重要な改善です。
## 関連リンク
* Go Issue #4852: [https://github.com/golang/go/issues/4852](https://github.com/golang/go/issues/4852)
* Go CL 7374045: [https://golang.org/cl/7374045](https://golang.org/cl/7374045)
## 参考にした情報源リンク
* Go言語公式ドキュメント: `net/http` パッケージ
* Go言語公式ドキュメント: `net` パッケージ
* Go言語公式ドキュメント: `testing` パッケージ
* Go言語のテストに関する一般的なプラクティスとFlaky Testに関する記事 (一般的な知識として)