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

[インデックス 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です。
  • ループの各イテレーションで、openlsof を取得し、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言語の公式ドキュメント (os/exec, testing, time パッケージ)
  • Linuxの /proc ファイルシステムに関するドキュメント
  • lsof コマンドのマニュアルページ
  • Go言語のIssueトラッカーおよびコードレビューシステム
  • Web検索: "golang issue 7808", "Linux /proc filesystem lsof race condition"