[インデックス 15997] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内の TestTransportConcurrency
というテストの不安定性(flakiness)を解消することを目的としています。具体的には、テストが時折失敗する原因となっていた競合状態を修正し、テストの信頼性を向上させています。
コミット
commit 68130a07afff6b0f514e574e5517f8f5d3da9765
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Mar 28 18:36:06 2013 -0700
net/http: unflake TestTransportConcurrency
Fixes #5005
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8127043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68130a07afff6b0f514e574e5517f8f5d3da9765
元コミット内容
net/http: unflake TestTransportConcurrency
Fixes #5005
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8127043
変更の背景
このコミットの背景には、Go言語の net/http
パッケージにおける TestTransportConcurrency
というテストが、実行環境やタイミングによって成功したり失敗したりする「flaky test(不安定なテスト)」であったという問題があります。コミットメッセージに Fixes #5005
とあるように、これはGoのIssue 5005で報告された問題に対応するものです。
不安定なテストは、開発プロセスにおいて大きな障害となります。テストがランダムに失敗すると、開発者はその失敗が実際のバグによるものなのか、それともテスト自体の問題なのかを判断するのに時間を費やすことになります。これにより、CI/CDパイプラインの信頼性が低下し、開発者の生産性が損なわれます。
TestTransportConcurrency
は、net/http.Transport
が複数の並行リクエストをどのように処理するかを検証するテストです。特に、コネクションの再利用や並行ダイヤル(接続確立)の挙動が関係しています。このテストが不安定であった原因は、Transport
の内部的な「ソケット遅延バインディング (socket late binding)」の挙動と、テストにおける sync.WaitGroup
の同期メカニズムの不整合にありました。
具体的には、Transport
がアイドルコネクションプールにコネクションを保持する際、Dial
(接続確立)が完了する前にHTTPリクエスト自体が完了してしまうケースがありました。これにより、テストが wg.Done()
を呼び出して WaitGroup
のカウントを減らす一方で、バックグラウンドで進行中の Dial
処理が WaitGroup
に追加されないため、WaitGroup
がゼロになり、テストが終了してしまうことがありました。その結果、まだ完了していない Dial
処理が残存し、Goのテストフレームワークが提供する「リークチェッカー(leak checker)」がゴルーチンリークを検出してテストを失敗させていたと考えられます。
このコミットは、このような競合状態を解消し、Dial
処理も WaitGroup
の同期対象に含めることで、テストがすべての関連するゴルーチンが完了するまで待機するように修正しています。
前提知識の解説
Flaky Test (不安定なテスト)
Flaky testとは、同じコードベースに対して同じテストを複数回実行した際に、成功したり失敗したりするテストのことです。これは、テストが外部要因(ネットワークの遅延、データベースの状態、システム時刻など)に依存していたり、並行処理における競合状態やタイミングの問題を抱えていたりする場合によく発生します。Flaky testは、テスト結果の信頼性を損ない、開発者の生産性を低下させるため、可能な限り修正されるべきです。
Go言語の net/http
パッケージ
Go言語の net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。
http.Client
: HTTPリクエストを送信するためのクライアントです。http.Transport
:http.Client
の内部で使用され、実際のネットワーク接続の確立、コネクションプーリング、プロキシ設定、TLSハンドシェイクなどを担当します。Transport
は、複数のHTTPリクエスト間でTCPコネクションを再利用することで、パフォーマンスを向上させます。Transport.Dial
フィールド:http.Transport
構造体にはDial
というフィールドがあり、これはカスタムのダイヤル関数(ネットワーク接続を確立する関数)を設定するために使用されます。デフォルトではnet.Dial
が使用されますが、このフィールドを上書きすることで、接続確立の挙動をカスタマイズできます。例えば、タイムアウトの設定、プロキシ経由の接続、またはテスト目的でのモック接続などに利用されます。
sync.WaitGroup
Go言語の sync
パッケージが提供する WaitGroup
は、複数のゴルーチンが完了するまでメインのゴルーチンが待機するための同期プリミティブです。
Add(delta int)
:WaitGroup
のカウンタにdelta
を加算します。通常、新しいゴルーチンを開始する前にwg.Add(1)
を呼び出します。Done()
:WaitGroup
のカウンタを1減らします。ゴルーチンがその処理を完了したときに呼び出されます。通常、defer wg.Done()
の形で使用され、ゴルーチンがどのように終了しても確実にカウンタが減らされるようにします。Wait()
:WaitGroup
のカウンタがゼロになるまでブロックします。
Goのテストにおけるリークチェッカー (Leak Checker)
Goのテストフレームワークには、テスト終了時に実行中のゴルーチンが残っていないかをチェックする「リークチェッカー」の機能があります。テストが終了したにもかかわらず、テスト内で開始されたゴルーチンがまだ実行中である場合、それは「ゴルーチンリーク」と見なされ、テストは失敗します。これは、テストがクリーンアップを適切に行っているか、または予期せぬバックグラウンド処理が残っていないかを検証するのに役立ちます。
ソケット遅延バインディング (Socket Late Binding)
net/http.Transport
の文脈における「ソケット遅延バインディング」とは、HTTPリクエストが発行された際に、すぐにTCPコネクションを確立するのではなく、必要になるまで接続の確立を遅延させる挙動を指します。特に、アイドルコネクションプールに既存のコネクションがある場合、新しいリクエストはそのコネクションを再利用しようとします。しかし、プールに適切なコネクションがない場合や、新しいコネクションが必要な場合、Dial
処理が開始されます。この Dial
処理は非同期的に行われることがあり、HTTPリクエストの処理自体が Dial
の完了を待たずに進行し、レスポンスが返されることがあります。この「遅延」が、テストの同期問題を引き起こす原因となることがあります。
技術的詳細
このコミットは、TestTransportConcurrency
テストの http.Transport
の設定にカスタムの Dial
関数を導入することで、テストの不安定性を解消しています。
元のコードでは、http.Transport
はデフォルトの Dial
関数(net.Dial
)を使用していました。この場合、Dial
処理が開始されても、それが sync.WaitGroup
のカウントに直接反映されることはありませんでした。
修正後のコードでは、Transport
の Dial
フィールドに匿名関数が設定されています。このカスタム Dial
関数は以下の処理を行います。
wg.Add(1)
の追加:net.Dial
を呼び出す直前にwg.Add(1)
を呼び出しています。これにより、新しいネットワーク接続の確立(ダイヤル)が開始されるたびに、WaitGroup
のカウンタが1増加します。これは、ダイヤル処理自体もテストが完了を待つべき「作業」の一部であることをWaitGroup
に伝えます。defer wg.Done()
の追加:net.Dial
の呼び出し後にdefer wg.Done()
を追加しています。defer
ステートメントにより、このDial
関数が終了する際に必ずwg.Done()
が呼び出され、WaitGroup
のカウンタが1減少します。これにより、ダイヤル処理が成功しても失敗しても、そのゴルーチンが完了したことがWaitGroup
に通知されます。- 元の
net.Dial
の呼び出し: 最終的に、実際のネットワーク接続はnet.Dial(netw, addr)
を呼び出すことで行われます。
この変更により、TestTransportConcurrency
テストは、HTTPリクエストの完了だけでなく、そのリクエストに関連して開始されたすべてのネットワークダイヤル処理が完了するまで WaitGroup
を通じて待機するようになります。これにより、Transport
の「ソケット遅延バインディング」によって Dial
処理がHTTPリクエストの完了後にバックグラウンドで継続しても、テストのリークチェッカーが誤ってゴルーチンリークを報告することがなくなります。結果として、テストはより堅牢で信頼性の高いものになります。
コアとなるコードの変更箇所
src/pkg/net/http/transport_test.go
ファイルの以下の部分が変更されました。
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -950,14 +950,28 @@ func TestTransportConcurrency(t *testing.T) {
fmt.Fprintf(w, "%v", r.FormValue("echo"))
}))
defer ts.Close()
- tr := &Transport{}
+
+ var wg sync.WaitGroup
+ wg.Add(numReqs)
+
+ tr := &Transport{
+ Dial: func(netw, addr string) (c net.Conn, err error) {
+ // Due to the Transport's "socket late
+ // binding" (see idleConnCh in transport.go),
+ // the numReqs HTTP requests below can finish
+ // with a dial still outstanding. So count
+ // our dials as work too so the leak checker
+ // doesn't complain at us.
+ wg.Add(1)
+ defer wg.Done()
+ return net.Dial(netw, addr)
+ },
+ }
defer tr.CloseIdleConnections()
c := &Client{Transport: tr}
reqs := make(chan string)
defer close(reqs)
- var wg sync.WaitGroup
- wg.Add(numReqs)
for i := 0; i < maxProcs*2; i++ {
go func() {
for req := range reqs {
コアとなるコードの解説
変更の核心は、TestTransportConcurrency
関数内で http.Transport
の初期化方法が変更された点にあります。
-
wg
の初期化とAdd(numReqs)
の移動:- 変更前は、
var wg sync.WaitGroup
とwg.Add(numReqs)
がTransport
の初期化後にありました。 - 変更後は、
wg
の宣言とwg.Add(numReqs)
がTransport
の初期化の直前に移動しました。これは、Dial
関数内でwg
を参照できるようにするためです。numReqs
はテストで送信されるHTTPリクエストの総数です。
- 変更前は、
-
カスタム
Dial
関数の設定:- 変更前は、
tr := &Transport{}
と、Dial
フィールドはデフォルトのままでした。 - 変更後は、
tr := &Transport{ Dial: func(...) { ... } }
のように、匿名関数をDial
フィールドに明示的に設定しています。 - この匿名関数の中では、以下の重要な処理が行われます。
wg.Add(1)
:net.Dial
が呼び出される(つまり、新しいネットワーク接続が試みられる)直前にWaitGroup
のカウンタを1増やします。これにより、ダイヤル処理もWaitGroup
の監視対象となります。defer wg.Done()
:Dial
関数が終了する際に、WaitGroup
のカウンタを1減らします。これにより、ダイヤル処理が完了したことがWaitGroup
に通知され、リークチェッカーが誤ってゴルーチンリークを検出するのを防ぎます。return net.Dial(netw, addr)
: 実際のネットワーク接続は、Go標準のnet.Dial
関数に委譲されます。
- 変更前は、
この修正により、テストはHTTPリクエストの完了だけでなく、それらのリクエストを処理するために行われたすべてのネットワークダイヤル操作の完了も待機するようになります。これにより、Transport
の内部的な非同期動作(特に「ソケット遅延バインディング」)によって発生していた競合状態が解消され、テストの不安定性が取り除かれました。
関連リンク
- Go Issue 5005: https://github.com/golang/go/issues/5005
- Go Change List 8127043: https://golang.org/cl/8127043
参考にした情報源リンク
- 上記のGo Issue 5005とChange List 8127043の議論
- Go言語の
net/http
パッケージのドキュメント - Go言語の
sync
パッケージのドキュメント - Go言語のテストに関する一般的な情報(特にflaky testとリークチェッカーについて)
- Go言語の
Transport
の内部動作に関する一般的な知識