[インデックス 14728] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtesting
パッケージ内のexample
テストにおけるファイルディスクリプタ(fd)リークを修正するものです。具体的には、パイプの読み取り側が適切に閉じられていなかった問題に対処しています。
コミット
testing: fix example test fd leak
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4b7c2dfcea6568f4f9c5d9b0aadd205821e6f7d3
元コミット内容
commit 4b7c2dfcea6568f4f9c5d9b0aadd205821e6f7d3
Author: Emil Hessman <c.emil.hessman@gmail.com>
Date: Sat Dec 22 13:41:01 2012 -0500
testing: fix example test fd leak
Close the read side of the pipe.
Fixes #4551.
R=rsc
CC=golang-dev
https://golang.org/cl/6962049
--─
src/pkg/testing/example.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pkg/testing/example.go b/src/pkg/testing/example.go
index dc97255965..34d4b2bda9 100644
--- a/src/pkg/testing/example.go
+++ b/src/pkg/testing/example.go
@@ -50,6 +50,7 @@ func RunExamples(matchString func(pat, str string) (bool, error), examples []Int
go func() {
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r)
+ r.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
os.Exit(1)
変更の背景
このコミットは、Go言語のIssue #4551「go test
leaks file descriptors for examples」を修正するために行われました。この問題は、go test
コマンドでExample
関数を実行する際に、テストが終了しても関連するファイルディスクリプタが閉じられずにリークするというものでした。
Example
関数は、Goのドキュメンテーションとテストを統合するユニークな機能です。これらの関数は、コードの動作例を示すために使用され、go test
コマンドによって実行され、その出力が期待される出力と一致するかどうかが検証されます。この検証プロセスの一環として、Example
関数の標準出力はパイプにリダイレクトされ、その内容がキャプチャされます。
しかし、このパイプの読み取り側が適切に閉じられていなかったため、Example
関数が実行されるたびにファイルディスクリプタが消費され、解放されない状態になっていました。多数のExample
関数を持つ大規模なプロジェクトや、テストを繰り返し実行するCI/CD環境などでは、このリークが原因でシステムがファイルディスクリプタの最大数に達し、新たなファイル操作やネットワーク接続ができなくなるという問題を引き起こす可能性がありました。
このコミットは、このリソースリークを解消し、testing
パッケージの堅牢性と信頼性を向上させることを目的としています。
前提知識の解説
1. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系オペレーティングシステムにおいて、ファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる整数値です。プロセスがファイルを開いたり、ネットワーク接続を確立したり、パイプを作成したりするたびに、新しいファイルディスクリプタが割り当てられます。これらのリソースは使用後に明示的に閉じる(解放する)必要があります。閉じ忘れると、ファイルディスクリプタがリークし、システム全体のリソースが枯渇する可能性があります。
2. パイプ (Pipe)
パイプは、プロセス間通信(IPC)の一種で、一方のプロセスが書き込んだデータをもう一方のプロセスが読み取ることができる単方向のデータストリームです。Go言語では、os.Pipe()
関数を使用してパイプを作成できます。この関数は、読み取り側(*os.File
)と書き込み側(*os.File
)の2つのファイルオブジェクトを返します。パイプもファイルディスクリプタを使用するため、使用後は両方の側を適切に閉じる必要があります。
3. io.Copy
関数
io.Copy(dst, src)
関数は、src
(io.Reader
インターフェースを実装するオブジェクト)からデータを読み込み、dst
(io.Writer
インターフェースを実装するオブジェクト)に書き込むためのGo言語のユーティリティ関数です。この関数は、src
のEOF(End Of File)に達するか、エラーが発生するまでデータをコピーし続けます。io.Copy
は内部でsrc
を閉じないため、src
がファイルやネットワーク接続などのリソースである場合は、io.Copy
の呼び出し後に明示的に閉じる必要があります。
4. Goのtesting
パッケージとExample
関数
Goのtesting
パッケージは、ユニットテスト、ベンチマークテスト、そしてExample
関数をサポートします。Example
関数は、Example<Name>
という命名規則に従う関数で、コードの実行例とその出力をドキュメントとして提供します。go test
コマンドは、これらのExample
関数を実行し、その標準出力(os.Stdout
)をキャプチャして、関数内のコメントで指定された期待される出力と比較します。このキャプチャメカニズムのために、内部的にパイプが使用されます。
技術的詳細
このファイルディスクリプタリークは、testing
パッケージがExample
関数の出力をキャプチャするためにパイプを使用する際に発生していました。具体的には、RunExamples
関数内で、Example
関数の標準出力をリダイレクトするためにos.Pipe()
が呼び出され、その書き込み側がos.Stdout
として設定されます。そして、別のゴルーチンでパイプの読み取り側からデータを読み取り、bytes.Buffer
にコピーしていました。
問題は、この読み取り側のファイルディスクリプタ(r
)が、io.Copy
によるデータコピーが完了した後も閉じられていなかった点にありました。io.Copy
は、ソース(この場合はパイプの読み取り側r
)からデータを読み取るだけで、そのソースを自動的に閉じる機能はありません。したがって、Example
関数が実行されるたびに、新しいパイプが作成され、その読み取り側のファイルディスクリプタがシステムに残されたままになっていました。
このコミットによる修正は、io.Copy
の呼び出し直後にr.Close()
を追加することで、このリークを解消しています。これにより、パイプの読み取り側が不要になった時点で明示的に閉じられ、関連するファイルディスクリプタがオペレーティングシステムに返却されるようになります。
この修正は、Example
テストの実行が完了した後に、使用されたリソースが適切に解放されることを保証し、特に多数のExample
テストを持つプロジェクトや、テストを頻繁に実行する環境での安定性を向上させます。
コアとなるコードの変更箇所
変更はsrc/pkg/testing/example.go
ファイル内のRunExamples
関数にあります。
--- a/src/pkg/testing/example.go
+++ b/src/pkg/testing/example.go
@@ -50,6 +50,7 @@ func RunExamples(matchString func(pat, str string) (bool, error), examples []Int
go func() {
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r)
+ r.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
os.Exit(1)
コアとなるコードの解説
変更が加えられたのは、RunExamples
関数内のゴルーチンです。このゴルーチンは、Example
関数の標準出力がリダイレクトされたパイプの読み取り側(r
)からデータを読み取る役割を担っています。
元のコードでは、以下のようになっていました。
go func() {
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r) // ここでパイプからデータを読み込む
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
os.Exit(1)
}
// ... 後続の処理 ...
}()
このコードでは、io.Copy(buf, r)
が完了した後、r
(パイプの読み取り側)が閉じられていませんでした。r
は*os.File
型であり、ファイルディスクリプタを保持しています。このため、ゴルーチンが終了しても、r
が保持するファイルディスクリプタは解放されず、リークが発生していました。
修正後のコードは以下のようになります。
go func() {
buf := new(bytes.Buffer)
_, err := io.Copy(buf, r)
r.Close() // ここでパイプの読み取り側を明示的に閉じる
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\\n", err)
os.Exit(1)
}
// ... 後続の処理 ...
}()
追加されたr.Close()
は、io.Copy
がパイプからのデータ読み込みを終えた直後に呼び出されます。これにより、パイプの読み取り側に関連付けられたファイルディスクリプタが適切に閉じられ、オペレーティングシステムに返却されます。これにより、ファイルディスクリプタのリークが防止され、システムリソースの枯渇を防ぐことができます。
この修正は、Goの標準ライブラリにおけるリソース管理のベストプラクティスに従ったものであり、不要になったリソースは速やかに解放するという原則を反映しています。
関連リンク
- Go Issue #4551:
go test
leaks file descriptors for examples - Go CL 6962049: testing: fix example test fd leak
参考にした情報源リンク
- Go言語の
testing
パッケージのドキュメント - Go言語の
os
パッケージのドキュメント(特にos.Pipe
) - Go言語の
io
パッケージのドキュメント(特にio.Copy
) - Unix系OSにおけるファイルディスクリプタとパイプに関する一般的な知識
- GitHubのGoリポジトリのIssue #4551の議論内容
- Go言語のコードレビューシステム(Gerrit)のCL 6962049の変更履歴とコメント