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

[インデックス 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)関数は、srcio.Readerインターフェースを実装するオブジェクト)からデータを読み込み、dstio.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言語のtestingパッケージのドキュメント
  • Go言語のosパッケージのドキュメント(特にos.Pipe
  • Go言語のioパッケージのドキュメント(特にio.Copy
  • Unix系OSにおけるファイルディスクリプタとパイプに関する一般的な知識
  • GitHubのGoリポジトリのIssue #4551の議論内容
  • Go言語のコードレビューシステム(Gerrit)のCL 6962049の変更履歴とコメント