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

[インデックス 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 のカウントに直接反映されることはありませんでした。

修正後のコードでは、TransportDial フィールドに匿名関数が設定されています。このカスタム Dial 関数は以下の処理を行います。

  1. wg.Add(1) の追加: net.Dial を呼び出す直前に wg.Add(1) を呼び出しています。これにより、新しいネットワーク接続の確立(ダイヤル)が開始されるたびに、WaitGroup のカウンタが1増加します。これは、ダイヤル処理自体もテストが完了を待つべき「作業」の一部であることを WaitGroup に伝えます。
  2. defer wg.Done() の追加: net.Dial の呼び出し後に defer wg.Done() を追加しています。defer ステートメントにより、この Dial 関数が終了する際に必ず wg.Done() が呼び出され、WaitGroup のカウンタが1減少します。これにより、ダイヤル処理が成功しても失敗しても、そのゴルーチンが完了したことが WaitGroup に通知されます。
  3. 元の 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 の初期化方法が変更された点にあります。

  1. wg の初期化と Add(numReqs) の移動:

    • 変更前は、var wg sync.WaitGroupwg.Add(numReqs)Transport の初期化後にありました。
    • 変更後は、wg の宣言と wg.Add(numReqs)Transport の初期化の直前に移動しました。これは、Dial 関数内で wg を参照できるようにするためです。numReqs はテストで送信されるHTTPリクエストの総数です。
  2. カスタム 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とChange List 8127043の議論
  • Go言語の net/http パッケージのドキュメント
  • Go言語の sync パッケージのドキュメント
  • Go言語のテストに関する一般的な情報(特にflaky testとリークチェッカーについて)
  • Go言語の Transport の内部動作に関する一般的な知識