[インデックス 15965] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおけるテストの安定性向上と、テスト失敗時のデバッグ情報の改善を目的としています。具体的には、ゴルーチンリークチェッカーの不安定性(flakiness)を解消し、リークが検出された際により詳細な出力が得られるように変更が加えられています。
コミット
commit 40cd845eea345d7ae84324c8d37cd0680b243773
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Mar 27 10:07:22 2013 -0700
net/http: improve test leak checker flakiness
And make it have more useful output on failure.
Update #5005
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8016046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/40cd845eea345d7ae84324c8d37cd0680b243773
元コミット内容
net/http: improve test leak checker flakiness
And make it have more useful output on failure.
Update #5005
変更の背景
Go言語のテストスイートでは、テスト実行後に予期せぬゴルーチン(goroutine)が残存していないかを確認する「ゴルーチンリークチェッカー」が組み込まれています。これは、テストがリソースを適切にクリーンアップしているか、あるいはデッドロックなどの問題でゴルーチンが終了しない状態になっていないかを検出するために重要です。
しかし、このリークチェッカーが不安定(flaky)であるという問題がありました。つまり、コード自体に問題がないにもかかわらず、テストの実行環境やタイミングによってリークが誤検出されたり、逆に実際のリークを見逃したりする可能性があったと考えられます。このような不安定なテストは、開発者がコードの変更に自信を持てなくさせ、CI/CDパイプラインの信頼性を低下させる原因となります。
また、リークが検出された際の出力が不十分であり、どのゴルーチンがリークしているのか、その原因は何なのかを特定しにくいという課題も存在しました。このコミットは、これらの問題を解決し、テストの信頼性とデバッグ効率を向上させることを目的としています。
コミットメッセージにある Update #5005
は、関連するIssue番号を示していますが、Go言語の公式リポジトリではこの番号のIssueは確認できませんでした。これは、内部的なトラッキング番号であるか、あるいは後に番号が変更された可能性が考えられます。
前提知識の解説
- ゴルーチン (Goroutine): Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。ゴルーチンはGoランタイムによってスケジューリングされます。
- スタックトレース (Stack Trace): プログラムの実行中にエラーや特定のイベントが発生した際に、その時点での関数呼び出しの履歴を記録したものです。これにより、どの関数がどの関数を呼び出し、最終的に問題が発生した場所に至ったのかを追跡できます。
- テストリークチェッカー: テストが終了した後に、テスト中に開始されたにもかかわらず終了していないゴルーチンや、開かれたままのリソース(ネットワーク接続、ファイルディスクリプタなど)がないかを検出するメカニズムです。これにより、テストがシステムの状態を汚染したり、後続のテストに影響を与えたりするのを防ぎます。
runtime.Stack()
: Go言語の標準ライブラリruntime
パッケージに含まれる関数で、現在のゴルーチンのスタックトレース、またはすべてのゴルーチンのスタックトレースを取得するために使用されます。strings.Contains()
: Go言語の標準ライブラリstrings
パッケージに含まれる関数で、ある文字列が別の文字列の部分文字列として含まれているかどうかを判定します。strings.Join()
: スライス内の文字列を結合して一つの文字列を生成する関数です。
技術的詳細
このコミットは、src/pkg/net/http/z_last_test.go
ファイル内の afterTest
関数に変更を加えています。この関数は、net/http
パッケージのテストスイートにおいて、各テストの実行後に呼び出されるクリーンアップおよびリークチェックのロジックを担っています。
変更の核心は、ゴルーチンのスタックトレースを取得する方法と、それを分析してリークを検出する方法の改善です。
-
スタックトレース取得方法の変更: 変更前は、
runtime.Stack(buf, true)
を使用して、事前に確保されたバッファbuf
にすべてのゴルーチンのスタックトレースを書き込んでいました。この方法では、バッファのサイズが不十分な場合、すべてのスタックトレースを取得できない可能性がありました。 変更後は、strings.Join(interestingGoroutines(), "\n\n")
を使用しています。これは、interestingGoroutines()
という関数が返す「興味深い」ゴルーチンのリストを結合してスタックトレース文字列を生成します。interestingGoroutines()
は、テストに関連する可能性のある特定のゴルーチンのみをフィルタリングして返す内部関数であると推測されます。これにより、不要なゴルーチンのスタックトレースを除外することで、出力のノイズを減らし、関連性の高い情報に焦点を当てることができます。また、バッファサイズの制約もなくなります。 -
リーク検出ロジックの改善: 変更前は、
badSubstring
マップに定義された部分文字列がスタックトレースに含まれているかをチェックし、リークの種類を特定していました。このロジック自体は変更されていませんが、スタックトレースの取得方法が変わったことで、より正確で関連性の高いスタックトレースに対してチェックが行われるようになります。 -
エラー出力の改善: 変更前は、
t.Errorf("Test appears to have leaked %s:\\n%s", bad, strings.Join(gs, "\n\n"))
のように、interestingGoroutines()
が返すゴルーチンリストgs
を直接エラーメッセージに含めていました。 変更後は、t.Errorf("Test appears to have leaked %s:\\n%s", bad, stacks)
のように、strings.Join(interestingGoroutines(), "\n\n")
で生成されたstacks
変数を直接エラーメッセージに含めるようになりました。これにより、エラーメッセージに表示されるスタックトレースが、よりフィルタリングされ、関連性の高いものになるため、デバッグが容易になります。 -
リトライロジックの維持:
for i := 0; i < 4; i++
ループとtime.Sleep(250 * time.Millisecond)
は、リークチェッカーが不安定な場合に備えて、複数回チェックを試みるためのリトライメカニズムです。これは、テスト終了直後にゴルーチンがまだ完全に終了していない可能性があるため、少し待ってから再チェックすることで誤検出を減らす目的があります。このロジックは変更されていません。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/z_last_test.go
+++ b/src/pkg/net/http/z_last_test.go
@@ -65,8 +65,6 @@ func afterTest(t *testing.T) {
if testing.Short() {
return
}
- buf := make([]byte, 1<<20)
- var stacks string
var bad string
badSubstring := map[string]string{
").readLoop(": "a Transport",
@@ -75,9 +73,10 @@ func afterTest(t *testing.T) {
"timeoutHandler": "a TimeoutHandler",
"net.(*netFD).connect(": "a timing out dial",
}
+ var stacks string
for i := 0; i < 4; i++ {
bad = ""
- stacks = string(buf[:runtime.Stack(buf, true)])
+ stacks = strings.Join(interestingGoroutines(), "\n\n")
for substr, what := range badSubstring {
if strings.Contains(stacks, substr) {
bad = what
@@ -90,6 +89,5 @@ func afterTest(t *testing.T) {
// shutting down, so give it some time.
time.Sleep(250 * time.Millisecond)
}
- gs := interestingGoroutines()
- t.Errorf("Test appears to have leaked %s:\\n%s", bad, strings.Join(gs, "\n\n"))
+ t.Errorf("Test appears to have leaked %s:\\n%s", bad, stacks)
}
コアとなるコードの解説
-
- buf := make([]byte, 1<<20)
と- var stacks string
の削除: 以前は、runtime.Stack
のために1MBのバッファbuf
を確保し、その結果をstacks
変数に格納していました。このバッファは、すべてのゴルーチンのスタックトレースを格納するために使われていましたが、interestingGoroutines()
を使う新しいアプローチでは不要になったため削除されました。 -
+ var stacks string
の移動:stacks
変数の宣言がループの直前に移動されました。これは、stacks
変数がループ内で再利用されるため、スコープを適切に設定し、初期化を明確にするためです。 -
- stacks = string(buf[:runtime.Stack(buf, true)])
の変更: この行は、runtime.Stack
を呼び出してすべてのゴルーチンのスタックトレースをbuf
に書き込み、それを文字列に変換していました。 -
+ stacks = strings.Join(interestingGoroutines(), "\n\n")
への変更: この新しい行は、interestingGoroutines()
関数を呼び出します。この関数は、テストに関連する可能性のある特定のゴルーチン(例えば、HTTPサーバーの処理に関連するゴルーチンなど)のスタックトレースを文字列のスライスとして返すと推測されます。その結果をstrings.Join
を使って\n\n
で結合し、一つの大きな文字列stacks
として格納します。これにより、不要なゴルーチンの情報が排除され、より関連性の高い情報のみがリークチェックの対象となります。 -
- gs := interestingGoroutines()
の削除: 以前は、エラーメッセージの生成のために再度interestingGoroutines()
を呼び出し、その結果をgs
に格納していました。 -
- t.Errorf("Test appears to have leaked %s:\\n%s", bad, strings.Join(gs, "\n\n"))
の変更: 以前は、gs
を再度strings.Join
で結合してエラーメッセージに含めていました。 -
+ t.Errorf("Test appears to have leaked %s:\\n%s", bad, stacks)
への変更: 新しいコードでは、既にstrings.Join(interestingGoroutines(), "\n\n")
で生成されたstacks
変数を直接エラーメッセージに渡しています。これにより、冗長な処理が削減され、エラー出力の効率が向上します。また、stacks
には既にフィルタリングされた「興味深い」ゴルーチンの情報が含まれているため、エラーメッセージがより簡潔で有用になります。
これらの変更により、ゴルーチンリークチェッカーは、より的を絞ったゴルーチンを監視し、リークが検出された場合には、より関連性の高いスタックトレース情報を提供できるようになり、テストの不安定性が軽減され、デバッグが容易になります。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
net/http
パッケージのドキュメント: https://pkg.go.dev/net/httpruntime
パッケージのドキュメント: https://pkg.go.dev/runtimestrings
パッケージのドキュメント: https://pkg.go.dev/strings
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/40cd845eea345d7ae84324c8d37cd0680b243773
- Go言語のIssueトラッカー (一般的な情報源として): https://github.com/golang/go/issues
- Go言語のコードレビューシステム (Gerrit): https://go.googlesource.com/go/+/refs/heads/master (コミットメッセージの
https://golang.org/cl/8016046
はGerritの変更リストへのリンクです) - Go言語のテストに関する一般的な情報 (例:
testing
パッケージ): https://pkg.go.dev/testing - Go言語の並行処理に関する情報 (ゴルーチン): https://go.dev/doc/effective_go#concurrency