[インデックス 15804] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるゴルーチンリークテストの不安定性(flakiness)を改善することを目的としています。具体的には、テストスイートの終了時に残存するゴルーチンをより正確に検出し、テストの信頼性を向上させています。
コミット
commit caf513a66c93edb89a4156e4a49f5ece867e21bd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Mar 15 15:09:17 2013 -0700
net/http: less flaky leaking goroutine test
Fixes #5005
R=golang-dev, adg, fullung
CC=golang-dev
https://golang.org/cl/7777043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/caf513a66c93edb89a4156e4a49f5ece867e21bd
元コミット内容
net/http: less flaky leaking goroutine test
Fixes #5005
変更の背景
Go言語の net/http
パッケージのテストスイートには、テスト実行後にゴルーチンがリークしていないか(つまり、不要なゴルーチンが終了せずに残り続けていないか)をチェックするメカニズムが含まれていました。しかし、このチェックが不安定(flaky)であり、実際のリークがない場合でもテストが失敗したり、逆にリークを見逃したりする問題がありました。
コミットメッセージにある Fixes #5005
は、このコミットがGoのIssue #5005を修正したことを示唆していますが、現在のGoのIssueトラッカーでは直接的なIssue #5005は見つかりませんでした。これは、Issue番号が変更されたか、内部的なトラッキング番号である可能性があります。しかし、コミットメッセージから、既存のゴルーチンリークテストが信頼性に欠け、その改善が求められていたことが明確に読み取れます。不安定なテストは開発プロセスを妨げ、真のバグを見逃す原因となるため、その修正は重要でした。
前提知識の解説
- Goroutine (ゴルーチン): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリングを管理します。
- Goroutine Leak (ゴルーチンリーク): ゴルーチンがその役割を終えた後も終了せずにメモリやCPUリソースを消費し続ける状態を指します。これは、チャネルの送受信がブロックされたままになったり、無限ループに陥ったりするなど、様々な原因で発生します。ゴルーチンリークは、アプリケーションのパフォーマンス低下やメモリ枯渇を引き起こす可能性があります。
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。WebアプリケーションやAPIサーバーを構築する上で不可欠なパッケージです。httptest
パッケージ:net/http
パッケージのテストを容易にするためのユーティリティを提供します。テスト用のHTTPサーバーを簡単に起動したり、HTTPリクエストをシミュレートしたりすることができます。defer
キーワード: Go言語のキーワードで、defer
文の後に続く関数呼び出しを、その関数がリターンする直前に実行するようにスケジュールします。リソースの解放(ファイルのクローズ、ロックの解除など)や、テスト後のクリーンアップ処理によく使用されます。- Flaky Test (不安定なテスト): 同じコードに対して、実行するたびに成功したり失敗したりするテストのことです。これは、並行処理のタイミングの問題、外部リソースへの依存、不適切なテスト環境のクリーンアップなど、様々な原因で発生します。不安定なテストは、開発者の信頼を損ない、CI/CDパイプラインの効率を低下させます。
runtime.Stack
: 現在のゴルーチンのスタックトレース、またはすべてのゴルーチンのスタックトレースを取得するためのGoの標準ライブラリ関数です。デバッグやプロファイリングに利用されます。
技術的詳細
このコミットの主要な目的は、net/http
パッケージのテストにおけるゴルーチンリーク検出の信頼性を高めることです。以前のテストでは、defer checkLeakedTransports(t)
という関数が各テストの終了時に呼び出され、ゴルーチンリークをチェックしていました。しかし、このアプローチは不安定でした。
不安定性の原因として考えられるのは、以下の点です。
- ゴルーチンの終了待ちの不十分さ: テストが終了しても、関連するゴルーチンが完全に終了するまでにわずかな時間差がある場合があります。
checkLeakedTransports
がゴルーチンの終了を十分に待たずにチェックを実行すると、まだ終了していないゴルーチンをリークと誤検知してしまう可能性があります。 - テストに関連しないゴルーチンの誤検知: Goランタイムやテストフレームワーク自身が生成するゴルーチン(例: ネットワークポーラー、テストランナーのゴルーチンなど)が、リークとは無関係に存在することがあります。これらのゴルーチンを適切にフィルタリングしないと、誤ってリークとして報告してしまうことがあります。
このコミットでは、これらの問題に対処するために、以下の変更が導入されました。
checkLeakedTransports
からafterTest
への置き換え: 多くのテストファイルで、defer checkLeakedTransports(t)
がdefer afterTest(t)
に変更されました。これは、ゴルーチンリークチェックのロジックがafterTest
関数に集約され、より堅牢になったことを示しています。z_last_test.go
におけるinterestingGoroutines
関数の導入と改善:interestingGoroutines()
関数が新しく導入され、runtime.Stack(buf, true)
を使用してすべてのゴルーチンのスタックトレースを取得します。- 取得したスタックトレースから、テストのリークとは無関係なゴルーチン(例:
created by net.newPollServer
,created by testing.RunTests
,closeWriteAndWait
,testing.Main(
など)をフィルタリングするロジックが追加されました。これにより、誤検知が大幅に削減されます。 - フィルタリングされた「興味深い」ゴルーチンのみが返され、ソートされることで、結果の一貫性が保たれます。
TestGoroutinesRunning
の改善:- このテストは、テストスイート全体の終了時に残存するゴルーチンをチェックする役割を担っています。
interestingGoroutines()
を呼び出して、リークの可能性のあるゴルーチンのみを対象とするようになりました。- 残存するゴルーチンの数が0より大きい場合にエラーを報告し、それぞれのスタックトレースと出現回数をログに出力することで、デバッグ情報が豊富になりました。
afterTest
関数のロジック強化:http.DefaultTransport.(*http.Transport).CloseIdleConnections()
を呼び出すことで、テスト終了時にアイドル状態のHTTPコネクションを積極的に閉じ、それに関連するゴルーチンを終了させます。これにより、テスト後のクリーンアップが強化されます。- ゴルーチンが完全に終了するまで、複数回のチェックと短い待機(
time.Sleep
)を繰り返すロジックが追加されました。これにより、タイミングの問題による不安定性が軽減されます。 - 最終的に残存するゴルーチンがある場合、
interestingGoroutines()
を使って取得したスタックトレースをエラーメッセージに含めることで、問題の特定を容易にしています。
これらの変更により、net/http
パッケージのテストは、ゴルーチンリークの検出においてより正確で信頼性の高いものとなりました。
コアとなるコードの変更箇所
このコミットの主要な変更は、以下のファイルに集中しています。
src/pkg/net/http/client_test.go
src/pkg/net/http/fs_test.go
src/pkg/net/http/serve_test.go
src/pkg/net/http/sniff_test.go
src/pkg/net/http/transport_test.go
- これらのファイルでは、各テスト関数の
defer checkLeakedTransports(t)
がdefer afterTest(t)
に一括で置き換えられています。
- これらのファイルでは、各テスト関数の
src/pkg/net/http/z_last_test.go
- このファイルは、テストスイートの最後に実行される特別なテストファイルであり、ゴルーチンリークチェックの主要なロジックが実装されています。
interestingGoroutines()
関数が新しく追加されました。TestGoroutinesRunning()
関数のロジックが大幅に修正され、interestingGoroutines()
を利用するようになりました。checkLeakedTransports()
関数がafterTest()
にリネームされ、その内部ロジックが強化されました。
コアとなるコードの解説
src/pkg/net/http/z_last_test.go
の変更点
このファイルは、テストスイートの最後に実行され、テスト中にゴルーチンがリークしていないことを確認するための重要な役割を担っています。
interestingGoroutines()
関数の追加
func interestingGoroutines() (gs []string) {
buf := make([]byte, 2<<20)
buf = buf[:runtime.Stack(buf, true)] // すべてのゴルーチンのスタックトレースを取得
for _, g := range strings.Split(string(buf), "\n\n") {
sl := strings.SplitN(g, "\n", 2)
if len(sl) != 2 {
continue
}
stack := strings.TrimSpace(sl[1])
if stack == "" ||
strings.Contains(stack, "created by net.newPollServer") || // ネットワークポーラーを除外
strings.Contains(stack, "created by testing.RunTests") || // テストランナーを除外
strings.Contains(stack, "closeWriteAndWait") ||
strings.Contains(stack, "testing.Main(") { // テストメイン関数を除外
continue
}
gs = append(gs, stack)
}
sort.Strings(gs) // 結果をソートして一貫性を保つ
return
}
この関数は、現在実行中のすべてのゴルーチンのスタックトレースを取得し、その中からテストのリークとは無関係なゴルーチン(例:Goランタイムの内部処理やテストフレームワーク自身のゴルーチン)を除外します。これにより、真のリークのみを検出対象とすることで、テストの誤検知を防ぎます。
TestGoroutinesRunning()
関数の修正
func TestGoroutinesRunning(t *testing.T) {
gs := interestingGoroutines() // 興味深いゴルーチンのみを取得
n := 0
stackCount := make(map[string]int)
for _, g := range gs {
stackCount[g]++
n++
}
t.Logf("num goroutines = %d", n)
if n > 0 { // 残存するゴルーチンがある場合
t.Error("Too many goroutines.")
for stack, count := range stackCount {
t.Logf("%d instances of:\n%s", count, stack) // 詳細なログ出力
}
}
}
このテストは、interestingGoroutines()
を利用して、テスト終了時に残存しているべきではないゴルーチンが存在しないことを確認します。もし残存するゴルーチンがあれば、そのスタックトレースと出現回数を詳細にログに出力し、デバッグを容易にします。以前は単にゴルーチンの総数をチェックしていましたが、より具体的な情報を提供するようになりました。
checkLeakedTransports()
から afterTest()
へのリネームとロジック強化
func afterTest(t *testing.T) {
http.DefaultTransport.(*http.Transport).CloseIdleConnections() // アイドルコネクションを閉じる
if testing.Short() {
return
}
var bad string
var stacks string
for i := 0; i < 4; i++ { // 複数回チェックと待機
bad = ""
stacks = ""
gs := interestingGoroutines() // 興味深いゴルーチンを取得
if len(gs) == 0 {
return // リークがない場合は終了
}
// ... (以前のロジックの一部が残っているが、主に interestingGoroutines に依存)
time.Sleep(250 * time.Millisecond) // ゴルーチンが終了するのを待つ
}
gs := interestingGoroutines() // 最終チェック
t.Errorf("Test appears to have leaked %s:\n%s", bad, strings.Join(gs, "\n\n")) // エラー報告
}
afterTest
関数は、各テストの終了時に呼び出されるクリーンアップおよびリークチェックの主要な関数です。
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
を呼び出すことで、HTTPトランスポートが保持しているアイドル状態のコネクションを閉じ、それに関連するゴルーチンを解放します。これは、HTTPクライアントのテストで特に重要です。interestingGoroutines()
を利用して、残存するゴルーチンを複数回チェックし、その間に短い遅延(time.Sleep
)を挟むことで、ゴルーチンが完全に終了するのを待ちます。これにより、タイミングの問題による誤検知を減らします。- 最終的にゴルーチンが残存している場合、
interestingGoroutines()
が返したスタックトレースをエラーメッセージに含めることで、どのゴルーチンがリークしているのかを明確に示します。
これらの変更により、Goの net/http
パッケージのテストは、ゴルーチンリークの検出において、より堅牢で信頼性の高いものとなりました。
関連リンク
- Go言語公式ドキュメント: https://go.dev/
net/http
パッケージドキュメント: https://pkg.go.dev/net/httphttptest
パッケージドキュメント: https://pkg.go.dev/net/http/httptest- Goにおける並行処理(ゴルーチンとチャネル)に関する一般的な情報源
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のIssueトラッカー: https://github.com/golang/go/issues (Issue #5005は直接見つからず)
- Go言語におけるゴルーチンリークに関する一般的な情報源 (例: ブログ記事、チュートリアルなど)
- Go言語のテストに関する一般的な情報源 (例:
testing
パッケージの利用方法、テストのベストプラクティスなど)