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

[インデックス 11827] ファイルの概要

このコミットは、Go言語のコマンドラインツール cmd/go におけるテスト実行時のタイムアウト処理に関する改善です。具体的には、go test コマンドに渡される -timeout フラグの値を cmd/go 自身が適切に解釈し、テスト全体の実行時間に対する内部的な「キルタイムアウト」を調整するように変更されています。これにより、テストが予期せずハングアップした場合でも、より正確な時間で強制終了できるようになります。

コミット

commit d318ab22647205c4d6d3aa32bb027a434f7b16d3
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 12 23:19:24 2012 -0500

    cmd/go: respect test -timeout flag
    
    I thought that -timeout was per-test, but it is for the
    whole program execution, so cmd/go can adjust its timer
    (also for whole program execution) accordingly.
    
    Fixes #2993.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5650070

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d318ab22647205c4d6d3aa32bb027a434f7b16d3

元コミット内容

cmd/go: respect test -timeout flag

I thought that -timeout was per-test, but it is for the
whole program execution, so cmd/go can adjust its timer
(also for whole program execution) accordingly.

Fixes #2993.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5650070

変更の背景

この変更の背景には、go test コマンドの -timeout フラグの挙動に関する誤解と、それによって発生する可能性のある問題がありました。コミットメッセージによると、当初開発者は -timeout フラグが個々のテスト関数に対して適用されるものと考えていたようです。しかし、実際にはこのフラグは go test プロセス全体の実行時間に対して適用されるものでした。

この認識のずれが問題となるのは、cmd/go 自身がテストバイナリのハングアップを検出するために内部的なタイムアウト(「キルタイムアウト」)を設定している点です。もしユーザーが -timeout フラグで短い時間を指定しているにもかかわらず、cmd/go の内部タイムアウトがそれよりもはるかに長い時間(例えばデフォルトの10分)に設定されている場合、ユーザーが期待するよりもテストプロセスが長く実行され続ける可能性がありました。特に、テストが無限ループに陥ったり、デッドロックしたりした場合、ユーザーが指定したタイムアウトで終了せず、ビルドシステムやCI/CDパイプラインをブロックしてしまう恐れがありました。

このコミットは、この問題を解決するために、cmd/go がユーザーが指定した -timeout フラグの値を読み取り、その値に基づいて自身の内部的なキルタイムアウトを調整するように修正しています。これにより、ユーザーの意図がより正確に反映され、テストの信頼性と効率が向上します。コミットメッセージにある Fixes #2993 は、この問題がGoプロジェクトのイシュートラッカー(またはGerritの変更リスト)で報告されていたことを示しています。

前提知識の解説

go test コマンド

go test はGo言語の標準的なテスト実行ツールです。Goのテストは、_test.go で終わるファイルに記述されたテスト関数(TestXxx)、ベンチマーク関数(BenchmarkXxx)、および例(ExampleXxx)を実行します。

go test -timeout フラグ

-timeout duration フラグは、テストバイナリ全体の実行時間に対するタイムアウトを設定します。指定された duration(例: 10s, 1m, 1h30m)を超えてテストが実行された場合、go test プロセスは強制終了されます。このフラグは、テストが無限ループに陥ったり、外部リソースの応答待ちでハングアップしたりするのを防ぐために重要です。

time.ParseDuration 関数

Go言語の time パッケージに含まれる time.ParseDuration(s string) (Duration, error) 関数は、文字列で表現された期間(例: "300ms", "1.5h", "2h45m")を time.Duration 型の値に変換します。この関数は、ユーザーからの入力や設定ファイルから時間情報を読み取る際に非常に便利です。

テストの「キルタイムアウト」

テストの「キルタイムアウト」とは、テストプロセスが予期せずハングアップした場合に、それを強制的に終了させるための最終的なデッドラインを指します。go test コマンド自体がテストバイナリを起動し、その実行を監視しています。テストバイナリが応答しなくなった場合や、指定されたタイムアウトを超過した場合に、go test はテストバイナリを強制終了する責任があります。この「キルタイムアウト」は、ビルドシステムやCI/CD環境において、テストが無限に実行され続けることを防ぎ、リソースの枯渇やパイプラインの停止を回避するために不可欠な安全機構です。

技術的詳細

このコミットの技術的な核心は、cmd/go がユーザー指定の -timeout フラグの値を読み取り、その値に基づいて自身の内部的なテストキルタイムアウト testKillTimeout を動的に調整する点にあります。

変更前は、testKillTimeout10 * time.Minute という固定値でハードコードされていました。これは、ユーザーが -timeout で短い時間を指定しても、cmd/go 自身は最大10分間テストの終了を待つ可能性があることを意味していました。

変更後、src/cmd/go/test.gorunTest 関数内で、testTimeout 変数(-timeout フラグの値が格納される)が time.ParseDuration を使って解析されます。

	// If a test timeout was given and is parseable, set our kill timeout
	// to that timeout plus one minute.  This is a backup alarm in case
	// the test wedges with a goroutine spinning and its background
	// timer does not get a chance to fire.
	if dt, err := time.ParseDuration(testTimeout); err == nil {
		testKillTimeout = dt + 1*time.Minute
	}

このコードスニペットは以下のロジックを実装しています。

  1. testTimeout (文字列) を time.Duration 型に変換しようと試みます。
  2. 変換が成功した場合(err == nil)、testKillTimeout の値を dt + 1*time.Minute に設定します。ここで dt はユーザーが指定したタイムアウト期間です。
  3. + 1*time.Minute という加算は、ユーザーが指定したタイムアウトに加えて1分間の猶予期間を設けることを意味します。これは、コミットメッセージにもあるように、「テストがゴルーチンをスピンさせてウェッジし、そのバックグラウンドタイマーが発火する機会を得られない場合のバックアップアラーム」として機能します。つまり、テストバイナリ自身のタイムアウト機構が何らかの理由で機能しなかった場合でも、cmd/go が少し遅れて確実にテストを強制終了するためのセーフティネットです。

この調整により、cmd/go の内部的なキルタイムアウトがユーザーの意図により近づき、テストのハングアップに対する応答性が向上します。

また、src/cmd/go/testflag.go では、-timeout フラグの値を testTimeout 変数に適切に格納するための変更が加えられています。

		case "timeout":
			testTimeout = value

これにより、go test コマンドに -timeout フラグが渡された際に、その値が cmd/go の内部で利用可能になります。

コアとなるコードの変更箇所

diff --git a/src/cmd/go/test.go b/src/cmd/go/test.go
index bc2af619c2..1633244556 100644
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -198,10 +198,13 @@ var (
 	testX            bool     // -x flag
 	testV            bool     // -v flag
 	testFiles        []string // -file flag(s)  TODO: not respected
+	testTimeout      string   // -timeout flag
 	testArgs         []string
 	testBench        bool
 	testStreamOutput bool // show output as it is generated
 	testShowPass     bool // show passing output
+
+	testKillTimeout = 10 * time.Minute
 )
 
 func runTest(cmd *Command, args []string) {
@@ -217,6 +220,14 @@ func runTest(cmd *Command, args []string) {
 		fatalf("cannot use -c flag with multiple packages")
 	}
 
+	// If a test timeout was given and is parseable, set our kill timeout
+	// to that timeout plus one minute.  This is a backup alarm in case
+	// the test wedges with a goroutine spinning and its background
+	// timer does not get a chance to fire.
+	if dt, err := time.ParseDuration(testTimeout); err == nil {
+		testKillTimeout = dt + 1*time.Minute
+	}
+
 	// show passing test output (after buffering) with -v flag.
 	// must buffer because tests are running in parallel, and
 	// otherwise the output will get mixed.
@@ -540,9 +551,7 @@ func (b *builder) runTest(a *action) error {
 	// This is a last-ditch deadline to detect and
 	// stop wedged test binaries, to keep the builders
 	// running.
-	const deadline = 10 * time.Minute
-
-	tick := time.NewTimer(deadline)
+	tick := time.NewTimer(testKillTimeout)
 	if err == nil {
 		done := make(chan error)
 		go func() {
diff --git a/src/cmd/go/testflag.go b/src/cmd/go/testflag.go
index 8913b9b504..7c9b7f16dd 100644
--- a/src/cmd/go/testflag.go
+++ b/src/cmd/go/testflag.go
@@ -133,6 +133,8 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 		case "bench":
 			// record that we saw the flag; don't care about the value
 			testBench = true
+		case "timeout":
+			testTimeout = value
 		}
 		if extraWord {
 			i++

コアとなるコードの解説

src/cmd/go/test.go の変更

  1. testTimeout 変数の追加: var ブロックに testTimeout string // -timeout flag が追加されました。これは、go test コマンドに渡された -timeout フラグの値を文字列として保持するための変数です。

  2. testKillTimeout の初期化: testKillTimeout = 10 * time.Minute が追加されました。これは、cmd/go がテストバイナリを強制終了する際のデフォルトのタイムアウトを10分に設定しています。この値は、ユーザーが -timeout フラグを指定しない場合に適用されます。

  3. runTest 関数内のタイムアウト調整ロジック: runTest 関数内に以下のコードブロックが追加されました。

    	// If a test timeout was given and is parseable, set our kill timeout
    	// to that timeout plus one minute.  This is a backup alarm in case
    	// the test wedges with a goroutine spinning and its background
    	// timer does not get a chance to fire.
    	if dt, err := time.ParseDuration(testTimeout); err == nil {
    		testKillTimeout = dt + 1*time.Minute
    	}
    

    この部分が、ユーザーが指定した -timeout の値を解析し、testKillTimeout を動的に設定する主要なロジックです。time.ParseDuration で文字列のタイムアウト値を time.Duration 型に変換し、成功すればその値に1分を加算して testKillTimeout に代入します。これにより、cmd/go の内部タイムアウトがユーザーの意図に沿って調整されます。

  4. time.NewTimer の引数変更: func (b *builder) runTest(a *action) error 内で、time.NewTimer の引数が const deadline = 10 * time.Minute から testKillTimeout に変更されました。

    -	const deadline = 10 * time.Minute
    -
    -	tick := time.NewTimer(deadline)
    +	tick := time.NewTimer(testKillTimeout)
    

    これにより、テストの監視タイマーが、固定値ではなく、ユーザーの指定またはデフォルト値に基づいて動的に設定された testKillTimeout の値を使用するようになります。

src/cmd/go/testflag.go の変更

  1. -timeout フラグの処理追加: testFlags 関数内の switch ステートメントに case "timeout": が追加されました。
    		case "timeout":
    			testTimeout = value
    
    この変更により、go test コマンドの引数を解析する際に -timeout フラグが認識され、その値(value)が testTimeout グローバル変数に格納されるようになります。これにより、test.go でその値を利用できるようになります。

これらの変更により、cmd/gogo test -timeout フラグの意図を正確に反映し、テストの実行管理をより堅牢に行えるようになりました。

関連リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/d318ab22647205c4d6d3aa32bb027a434f7b16d3
  • 関連するGo Gerrit Change-ID (CL): https://golang.org/cl/5650070
    • : このCLリンクは、Goプロジェクトがかつて使用していたGerritコードレビューシステムへの古い参照である可能性があり、現在の go.dev/cl の形式とは異なります。直接アクセスできない場合がありますが、コミットメッセージに記載されている元の参照としてここに含めます。Fixes #2993 は、Goプロジェクトの内部的なイシュートラッカーまたはGerritの変更リスト番号を指していると考えられます。

参考にした情報源リンク

  • Go言語の公式ドキュメント (go test コマンド、time パッケージなど)
  • Go言語のソースコード (特に src/cmd/go/test.go および src/cmd/go/testflag.go)
  • GoプロジェクトのGerritシステムに関する一般的な情報 (GoプロジェクトはGitHubのIssueトラッカーと並行してGerritをコードレビューに利用しています)
  • Web検索による golang/go issue 2993 および golang.org/cl/5650070 の調査結果 (これらの検索結果は、#2993 がGitHubのIssueではなくGerritのCLである可能性が高いこと、および古いCLリンクが直接アクセスできない場合があることを示唆しています。)