[インデックス 19225] ファイルの概要
このコミットは、Go言語の os/exec
パッケージ内のテスト TestPipeLookPathLeak
における、Linux環境での不安定性(flakiness)を解消するためのものです。具体的には、/proc
ファイルシステムと lsof
コマンドの挙動に起因する競合状態を回避するために、テストにリトライロジックが追加されました。
コミット
os/exec
パッケージのテスト TestPipeLookPathLeak
がLinux上で不安定になる問題を修正しました。これは、Linuxの /proc
ファイルシステムが持つ、lsof
コマンドによるファイルディスクリプタの報告に関するバグ(または競合状態)を回避するためのものです。テストが失敗した場合、数回のリトライと短い待機を行うことで、この不安定性を解消しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/56005722493b044109103d0ebb867561f1c71e3c
元コミット内容
os/exec: deflake a test on Linux
Work around buggy(?) Linux /proc filesystem.
Fixes #7808
LGTM=iant
R=golang-codereviews, iant
CC=adg, golang-codereviews
https://golang.org/cl/90400044
変更の背景
この変更は、Goの os/exec
パッケージのテストスイートに含まれる TestPipeLookPathLeak
というテストが、特定のLinux環境で時折失敗するという問題(flakiness)に対応するために行われました。このテストは、プロセスがファイルディスクリプタ(FD)を適切にクリーンアップしているかを確認することを目的としています。
問題の根本原因は、Linuxの /proc
ファイルシステムと、オープンされているファイルディスクリプタをリストアップするために使用される lsof
コマンドの間の競合状態にあると考えられました。lsof
は /proc/<pid>/fd
ディレクトリを読み取ることでプロセスのオープンFD情報を取得しますが、プロセスがFDを閉じた直後であっても、/proc
ファイルシステムがその変更を即座に反映しない場合がありました。これにより、テストがFDリークを誤って報告し、テストが不安定になることがありました。
この不安定性は、GoのCI/CDパイプラインにおいて、テストの信頼性を低下させる要因となっていました。そのため、テスト自体が誤った失敗を報告しないように、この競合状態を回避するメカニズムが必要とされました。この問題は、GoのIssueトラッカーで #7808 として報告されていました。
前提知識の解説
os/exec
パッケージ: Go言語の標準ライブラリの一部で、外部コマンドを実行するための機能を提供します。これにより、Goプログラムからシェルコマンドや他の実行可能ファイルを起動し、その入出力を制御することができます。- ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、プロセスがファイルやソケット、パイプなどのI/Oリソースにアクセスするために使用する抽象的な識別子です。各プロセスは、オープンしているFDのリストを管理しており、FDリークは、不要になったFDが閉じられずに残り続ける状態を指し、システムリソースの枯渇やパフォーマンス低下の原因となる可能性があります。
lsof
コマンド: "list open files" の略で、Unix系OSで実行中のプロセスがオープンしているファイルやネットワーク接続を一覧表示するためのコマンドです。lsof
は、主に/proc
ファイルシステム(特に/proc/<pid>/fd
)から情報を読み取って動作します。/proc
ファイルシステム: Linuxカーネルが提供する仮想ファイルシステムです。実行中のプロセスやシステムに関する情報がファイルやディレクトリとして表現されており、ユーザー空間のプログラムがカーネルの内部状態にアクセスするためのインターフェースとして機能します。例えば、/proc/<pid>/fd
には、プロセスID<pid>
がオープンしているファイルディスクリプタへのシンボリックリンクが含まれています。- テストの不安定性 (Test Flakiness): ソフトウェアテストにおいて、コードの変更がないにもかかわらず、テストが成功したり失敗したりする現象を指します。これは、テストが外部要因(例: ネットワークの遅延、OSのスケジューリング、ファイルシステムの競合状態など)に依存している場合に発生しやすく、CI/CDパイプラインの信頼性を損ないます。
- テストのデフレイク (Deflake a test): 不安定なテストを修正し、その信頼性を向上させるプロセスを指します。通常、競合状態の解消、タイムアウトの調整、リトライロジックの追加などが行われます。
技術的詳細
TestPipeLookPathLeak
テストは、os/exec
パッケージが外部コマンドを実行した際に、不要なファイルディスクリプタがリークしないことを検証します。このテストでは、numOpenFDS
というヘルパー関数を使用して、テスト実行前後のオープンFD数を比較し、FDの増加が許容範囲内(このケースでは2つ以下)であることを確認します。
問題は、numOpenFDS
関数が lsof
コマンドを利用してFD数を取得する点にありました。Linuxの /proc
ファイルシステムは、プロセスの状態をリアルタイムで反映しますが、特にFDのクローズのような操作に関しては、lsof
が /proc
から情報を読み取るタイミングと、カーネルが実際にFD情報を更新するタイミングとの間にわずかな遅延や競合状態が存在することがありました。これにより、FDが既に閉じられているにもかかわらず、lsof
が古い情報を報告し、テストがFDリークを誤って検出してしまうことがありました。
このコミットでは、この競合状態を回避するために、テストのFDチェック部分にリトライロジックを導入しました。具体的には、FDの増加が許容範囲を超えていた場合、即座にテストを失敗させるのではなく、最大3回までリトライを試みます。各リトライの間には100ミリ秒の短い遅延(time.Sleep(100 * time.Millisecond)
)を挿入します。この遅延により、/proc
ファイルシステムが最新のFD情報を反映するのに十分な時間を与え、lsof
が正しい情報を取得できる可能性を高めます。
このアプローチは、テストのロジック自体を変更するのではなく、外部システム(この場合はLinuxカーネルの/proc
ファイルシステム)の非同期性や競合状態に起因する不安定性を吸収するための一般的な手法です。これにより、テストの信頼性が向上し、CI/CDパイプラインでの誤った失敗が減少します。
コアとなるコードの変更箇所
--- a/src/pkg/os/exec/exec_test.go
+++ b/src/pkg/os/exec/exec_test.go
@@ -224,10 +224,21 @@ func TestPipeLookPathLeak(t *testing.T) {
t.Fatal("unexpected success")
}
}\n- open, lsof := numOpenFDS(t)\n- fdGrowth := open - fd0\n- if fdGrowth > 2 {\n-\t\tt.Errorf("leaked %d fds; want ~0; have:\\n%s\\noriginally:\\n%s", fdGrowth, lsof, lsof0)\n+\tfor triesLeft := 3; triesLeft >= 0; triesLeft-- {\n+\t\topen, lsof := numOpenFDS(t)\n+\t\tfdGrowth := open - fd0\n+\t\tif fdGrowth > 2 {\n+\t\t\tif triesLeft > 0 {\n+\t\t\t\t// Work around what appears to be a race with Linux\'s\n+\t\t\t\t// proc filesystem (as used by lsof). It seems to only\n+\t\t\t\t// be eventually consistent. Give it awhile to settle.\n+\t\t\t\t// See golang.org/issue/7808\n+\t\t\t\ttime.Sleep(100 * time.Millisecond)\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\tt.Errorf("leaked %d fds; want ~0; have:\\n%s\\noriginally:\\n%s", fdGrowth, lsof, lsof0)\n+\t\t}\n+\t\tbreak\n \t}\n }\n
コアとなるコードの解説
変更の核心は、TestPipeLookPathLeak
関数内のファイルディスクリプタのリークチェック部分に導入された for
ループです。
元のコードでは、numOpenFDS(t)
を呼び出して現在のオープンFD数を取得し、初期のFD数 fd0
と比較して fdGrowth
を計算していました。もし fdGrowth
が2を超えていれば、即座にエラーとして報告していました。
変更後のコードでは、このチェックを for triesLeft := 3; triesLeft >= 0; triesLeft--
というループで囲んでいます。
triesLeft
はリトライの残り回数を管理する変数で、初期値は3です。- ループの各イテレーションで、
open
とlsof
を取得し、fdGrowth
を計算します。 - もし
fdGrowth
が2を超えていた場合(つまり、FDリークが検出された場合):if triesLeft > 0
の条件で、まだリトライの機会が残っているかを確認します。- リトライの機会がある場合、コメントで示されているように、Linuxの
/proc
ファイルシステムとlsof
の間の競合状態を回避するために、time.Sleep(100 * time.Millisecond)
を呼び出して100ミリ秒間スリープします。その後、continue
で次のループイテレーションに進み、再度FDチェックを試みます。 - リトライの機会がない場合(
triesLeft
が0になった場合)、その時点でt.Errorf
を呼び出してエラーを報告し、テストを失敗させます。
- もし
fdGrowth
が2以下であった場合(つまり、FDリークが検出されなかった場合)、break
でループを抜け、テストは成功とみなされます。
このリトライロジックにより、一時的な競合状態によって lsof
が誤ったFD数を報告した場合でも、テストが即座に失敗することなく、/proc
ファイルシステムが安定するのを待ってから再チェックを行うことができます。これにより、テストの信頼性が大幅に向上しました。
関連リンク
- Go Issue #7808: https://golang.org/issue/7808
- Go Code Review (Gerrit): https://golang.org/cl/90400044
参考にした情報源リンク
- Go言語の公式ドキュメント (os/exec, testing, time パッケージ)
- Linuxの
/proc
ファイルシステムに関するドキュメント lsof
コマンドのマニュアルページ- Go言語のIssueトラッカーおよびコードレビューシステム
- Web検索: "golang issue 7808", "Linux /proc filesystem lsof race condition"