[インデックス 17576] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージ内のテストTestDialFailPDLeak
の改善に関するものです。具体的には、テストの実行時間を短縮し、特定の環境(Windows/386)でのテストをスキップすることで、開発ワークフローの効率化を図っています。同時に、ランタイムと統合されたネットワークポーラーにおけるメモリリークの検出能力を維持しています。
コミット
commit 81737a9a512cc0a52857d7c9d8137faa6ba7e5c1
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Thu Sep 12 11:10:25 2013 +0900
net: make TestDialFailPDLeak shorter
Reduces a number of trials but it still can detect memory leak
when we make blunders in runtime-integarted network poller work,
like just forgetting to call runtime_pollClose in code paths.
Also disables the test on windows/386.
R=alex.brainman, r
CC=golang-dev
https://golang.org/cl/13022046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/81737a9a512cc0a52857d7c9d8137faa6ba7e5c1
元コミット内容
net: make TestDialFailPDLeak shorter
このコミットは、TestDialFailPDLeak
テストを短縮することを目的としています。試行回数を減らすことでテスト時間を短縮しますが、ランタイムと統合されたネットワークポーラーの作業で、runtime_pollClose
の呼び出し忘れのようなミスがあった場合に、メモリリークを検出する能力は維持されます。また、Windows/386環境ではこのテストを無効にします。
変更の背景
TestDialFailPDLeak
は、Goのネットワークポーラーが適切にリソースを解放しているか、特に接続試行が失敗した場合にメモリリークが発生しないかを検証するためのテストです。元の実装では、count
変数が20000
という大きな値に設定されており、多数の失敗するダイヤル試行を繰り返していました。これにより、テストの実行時間が非常に長くなり、特にリソースが限られている環境やCI/CDパイプラインにおいて、開発のボトルネックとなる可能性がありました。
コミットメッセージにあるように、このテストは「ランタイムと統合されたネットワークポーラーの作業で、runtime_pollClose
の呼び出し忘れのようなミスがあった場合に、メモリリークを検出する」ことを目的としています。テストの目的を達成しつつ、実行時間を短縮することが求められました。
また、Windows/386環境ではこのテストが特に時間がかかるため、その環境でのテストをスキップする判断がなされました。これは、特定のプラットフォームでのテストの非効率性を解消し、全体的なテストスイートの実行時間を最適化するためです。
前提知識の解説
Goのネットワークポーラー (Network Poller)
Goのランタイムには、効率的なI/O処理を実現するための「ネットワークポーラー」が組み込まれています。これは、ノンブロッキングI/Oとイベント通知メカニズム(Linuxのepoll、macOS/BSDのkqueue、WindowsのIOCPなど)を利用して、多数のネットワーク接続を同時に処理できるようにするものです。
Goのgoroutineは、I/O操作がブロックされると自動的にスケジューリングされ、他のgoroutineが実行されます。ネットワークポーラーは、I/O操作が完了した際にgoroutineを再開させる役割を担います。これにより、Goは高い並行性と効率的なネットワーク処理を実現しています。
pollDesc
とruntime_pollClose
Goのネットワークポーラーは、各ネットワーク接続(ファイルディスクリプタやソケット)の状態を管理するためにpollDesc
という内部構造体を使用します。pollDesc
は、I/Oイベントの登録、待機、通知などの情報を含んでいます。
runtime_pollClose
は、Goランタイム内部で呼び出される関数で、pollDesc
構造体に関連付けられたリソースを解放する役割を担います。ネットワーク接続が閉じられたり、ダイヤル試行が失敗したりしてpollDesc
が不要になった場合、この関数が呼び出されてリソースが適切にクリーンアップされる必要があります。もしruntime_pollClose
の呼び出しが忘れられた場合、pollDesc
オブジェクトがメモリ上に残り続け、メモリリークを引き起こす可能性があります。
メモリリークの検出
メモリリークは、プログラムが確保したメモリを解放し忘れることで、利用可能なメモリが徐々に減少していく現象です。Goのようなガベージコレクション(GC)を持つ言語でも、GCが到達できない(参照されていない)が、実際には解放されるべきリソース(この場合はpollDesc
)が存在する場合にメモリリークが発生することがあります。
TestDialFailPDLeak
は、意図的に失敗するネットワークダイヤルを多数実行し、その前後でシステムのメモリ使用量(runtime.MemStats
のSys
フィールドなど)を比較することで、pollDesc
のリークがないかを検出します。もしpollDesc
が適切に解放されていなければ、メモリ使用量が増加し、テストが失敗するようになっています。
testing.Short()
Goのtesting
パッケージには、テストの実行時間を制御するためのtesting.Short()
関数が提供されています。これは、go test -short
コマンドでテストを実行した場合にtrue
を返します。開発者は、時間がかかるテストやリソースを多く消費するテストをtesting.Short()
で囲むことで、通常の開発サイクルではスキップし、CI/CD環境やリリース前の完全なテストスイート実行時のみに実行するといった使い分けが可能です。
sync.WaitGroup
sync.WaitGroup
は、Goのsync
パッケージが提供する同期プリミティブの一つで、複数のgoroutineの完了を待機するために使用されます。Add(delta int)
でカウンタを増やし、Done()
でカウンタを減らし、Wait()
でカウンタがゼロになるまでブロックします。このコミットでは、多数のダイヤル試行を並行して実行するために使用されています。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
Windows/386環境でのテストスキップ:
runtime.GOOS == "windows" && runtime.GOARCH == "386"
という条件が追加され、もし実行環境がWindowsの32ビットアーキテクチャである場合、テストがスキップされるようになりました。これは、この環境でのテスト実行が非常に時間がかかるためです。t.Skipf
を使用することで、テストがスキップされた理由が明確に報告されます。 -
count
変数の削減: 失敗するダイヤル試行の回数を制御するcount
変数の値が、20000
から500
に大幅に削減されました。コミットメッセージには「500はpollcache
のチャンクを使い切るのに十分」とあり、これはruntime/netpoll.goc
内のallocPollDesc
関数に関連しています。つまり、pollDesc
のキャッシュメカニズムを十分にテストし、リークを検出するためには500回の試行で十分であるという判断です。これにより、テストの実行時間が劇的に短縮されます。 -
並行ダイヤル試行の導入: 元のテストでは、ダイヤル試行が
for
ループ内で逐次的に実行されていました。この変更では、sync.WaitGroup
を導入し、各ダイヤル試行を新しいgoroutineで並行して実行するようにしました。var wg sync.WaitGroup
でWaitGroup
を宣言。- 各ダイヤル試行の前に
wg.Add(1)
でカウンタをインクリメント。 - ダイヤル試行を行う匿名関数を
go func() { ... }()
でgoroutineとして起動し、defer wg.Done()
で完了時にカウンタをデクリメント。 - 内側のループの最後に
wg.Wait()
を呼び出し、すべての並行ダイヤル試行が完了するまで待機します。
これにより、テストの実行時間が短縮されるだけでなく、並行処理下での
pollDesc
のリーク検出能力も向上する可能性があります。
コアとなるコードの変更箇所
src/pkg/net/dial_test.go
ファイルのTestDialFailPDLeak
関数が変更されています。
--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -431,9 +431,15 @@ func TestDialFailPDLeak(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
+ if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
+ // Just skip the test because it takes too long.
+ t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
+ }
const loops = 10
- const count = 20000
+ // 500 is enough to turn over the chunk of pollcache.
+ // See allocPollDesc in runtime/netpoll.goc.
+ const count = 500
var old runtime.MemStats // used by sysdelta
runtime.ReadMemStats(&old)
sysdelta := func() uint64 {
@@ -446,13 +452,20 @@ func TestDialFailPDLeak(t *testing.T) {
d := &Dialer{Timeout: time.Nanosecond} // don't bother TCP with handshaking
failcount := 0
for i := 0; i < loops; i++ {
+ var wg sync.WaitGroup
for i := 0; i < count; i++ {
- conn, err := d.Dial("tcp", "127.0.0.1:1")
- if err == nil {
- t.Error("dial should not succeed")
- conn.Close()
- t.FailNow()
- }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if c, err := d.Dial("tcp", "127.0.0.1:1"); err == nil {
+ t.Error("dial should not succeed")
+ c.Close()
+ }
+ }()
+ }
+ wg.Wait()
+ if t.Failed() {
+ t.FailNow()
}
if delta := sysdelta(); delta > 0 {
failcount++
コアとなるコードの解説
1. Windows/386でのスキップロジック
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
// Just skip the test because it takes too long.
t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
}
runtime.GOOS
とruntime.GOARCH
は、それぞれ現在のオペレーティングシステムとアーキテクチャを示すGoの定数です。このコードは、テストがWindowsの32ビット環境で実行されている場合に、t.Skipf
を呼び出してテストをスキップします。t.Skipf
は、テストをスキップし、指定されたフォーマット文字列と引数で理由を報告します。これにより、特定の環境でのテストの非効率性が解消されます。
2. count
変数の変更
const loops = 10
- const count = 20000
+ // 500 is enough to turn over the chunk of pollcache.
+ // See allocPollDesc in runtime/netpoll.goc.
+ const count = 500
count
変数の値が20000
から500
に減らされました。コメントにあるように、500
という値はpollcache
のチャンクを十分に「使い切る」(つまり、pollDesc
の割り当てと解放のサイクルを十分に発生させる)のに十分であると判断されています。これは、テストの目的であるメモリリークの検出能力を損なうことなく、テスト時間を大幅に短縮するための最適化です。
3. 並行ダイヤル試行の導入
for i := 0; i < loops; i++ {
+ var wg sync.WaitGroup
for i := 0; i < count; i++ {
- conn, err := d.Dial("tcp", "127.0.0.1:1")
- if err == nil {
- t.Error("dial should not succeed")
- conn.Close()
- t.FailNow()
- }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if c, err := d.Dial("tcp", "127.0.0.1:1"); err == nil {
+ t.Error("dial should not succeed")
+ c.Close()
+ }
+ }()
+ }
+ wg.Wait()
+ if t.Failed() {
+ t.FailNow()
}
この部分が最も大きな変更点です。
- 内側のループの開始時に
var wg sync.WaitGroup
が宣言され、各loops
イテレーションごとに新しいWaitGroup
が使用されます。 for i := 0; i < count; i++
ループ内で、各ダイヤル試行の前にwg.Add(1)
が呼び出されます。d.Dial
の呼び出しとエラーチェックは、go func() { ... }()
という匿名関数内で実行され、新しいgoroutineとして起動されます。- 匿名関数の冒頭には
defer wg.Done()
が追加されており、goroutineが終了する際にWaitGroup
のカウンタをデクリメントします。これにより、すべてのダイヤル試行goroutineが完了したことがWaitGroup
に通知されます。 - 内側のループの直後に
wg.Wait()
が呼び出され、count
個のすべてのダイヤル試行goroutineが完了するまでメインのテストgoroutineがブロックされます。 if t.Failed() { t.FailNow() }
は、並行実行されたgoroutine内でt.Error
が呼び出された場合に、メインのテストgoroutineも即座に失敗させるためのものです。t.Error
はテストを失敗としてマークしますが、即座にテストを終了させるわけではないため、t.FailNow
で明示的に終了させています。
この並行化により、count
が削減されたことと相まって、テストの実行時間が大幅に短縮されます。また、複数のgoroutineが同時にネットワークポーラーのリソースを操作することで、より現実的なシナリオでのメモリリークの検出が可能になります。
関連リンク
- Goのネットワークポーラーに関する一般的な情報:
- Goのネットワークプログラミングに関する公式ドキュメントやブログ記事
- Goの
net
パッケージのソースコード
sync.WaitGroup
に関するGoの公式ドキュメント:testing
パッケージに関するGoの公式ドキュメント:
参考にした情報源リンク
- https://github.com/golang/go/commit/81737a9a512cc0a52857d7c9d8137faa6ba7e5c1
- Go言語の公式ドキュメントおよびソースコード(特に
src/pkg/net/dial_test.go
、runtime/netpoll.goc
など) - Goのメモリ管理とガベージコレクションに関する一般的な情報源
- Goの並行処理と
goroutine
、sync
パッケージに関する一般的な情報源