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

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

このコミットは、Go言語のコマンドラインツール cmd/go におけるテスト実行時のタイムアウト処理に関するものです。具体的には、テストが長時間ハングアップするのを防ぐため、テストプロセスが1分以上実行された場合に強制終了するメカニズムを導入しています。これにより、Goのビルドシステム(ビルダー)がテストのハングアップによって停止するのを防ぐことが目的です。

コミット

commit 0c012af11464ad1d5f2f188f6026c3b8a5483ca4
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 10 21:01:58 2012 -0800

    cmd/go: kill test.out after 1 minute
    
    Will have to do better but this is enough to
    stop the builders from hanging, I hope.
    
    R=golang-dev, dsymonds, adg
    CC=golang-dev
    https://golang.org/cl/5533066

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

https://github.com/golang/go/commit/0c012af11464ad1d5f2f188f6026c3b8a5483ca4

元コミット内容

cmd/go: kill test.out after 1 minute

Will have to do better but this is enough to
stop the builders from hanging, I hope.

R=golang-dev, dsymonds, adg
CC=golang-dev
https://golang.org/cl/5533066

変更の背景

このコミットの主な背景は、Go言語の自動ビルドシステム(通称「ビルダー」)が、テストプロセスのハングアップによって停止してしまう問題に対処することです。テストが無限ループに陥ったり、外部リソースの応答待ちでデッドロックしたりすると、テストプロセスが終了せず、結果としてビルダーが次のタスクに進めなくなります。

コミットメッセージにある「Will have to do better but this is enough to stop the builders from hanging, I hope.」という記述から、これは一時的または緊急の対策であり、より洗練された解決策が将来的に必要であるという認識があったことが伺えます。しかし、差し当たってビルダーの安定稼働を確保することが最優先であったため、1分というタイムアウトを設定し、それを超えたテストプロセスを強制終了するシンプルなメカニズムが導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念と標準ライブラリの知識が必要です。

  • cmd/go: Go言語の公式ツールチェーンの一部であり、Goプログラムのビルド、テスト、実行などを管理するコマンドラインツールです。このコミットは、go test コマンドの内部実装に関連しています。
  • os/exec パッケージ: 外部コマンドを実行するためのGoの標準ライブラリパッケージです。exec.Command は実行するコマンドと引数を設定し、cmd.Start() はコマンドを非同期で開始し、cmd.Wait() はコマンドの終了を待ちます。cmd.CombinedOutput() はコマンドの実行を待ち、標準出力と標準エラー出力を結合して返します。
  • time パッケージ: 時間の計測と操作を行うためのGoの標準ライブラリパッケージです。
    • time.Minute: 1分を表す time.Duration 型の定数です。
    • time.NewTimer(d time.Duration): 指定された期間 d が経過した後に、チャネルに現在の時刻を送信する Timer を作成します。
    • tick.C: Timer が時間切れになったときに値が送信されるチャネルです。
    • tick.Stop(): Timer を停止し、チャネルへの送信を防ぎます。
  • Goroutine (ゴルーチン): Go言語における軽量な並行実行単位です。関数呼び出しの前に go キーワードを付けることで、その関数を新しいゴルーチンとして実行できます。これにより、複数の処理を同時に実行することが可能になります。
  • Channel (チャネル): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期とデータ転送を可能にします。
  • select ステートメント: 複数のチャネル操作を待機し、準備ができた最初の操作を実行するためのGoの制御構造です。select は、並行処理における非ブロッキング操作やタイムアウト処理の実装に不可欠です。

技術的詳細

このコミットの技術的な核心は、外部プロセス(テスト実行)のタイムアウト処理を実装するために、Goの並行処理機能(ゴルーチン、チャネル、select)を効果的に利用している点です。

変更前は、cmd.CombinedOutput() を使用してテストプロセスを実行していました。この関数は、外部コマンドの実行が完了するまでブロックし、その出力とエラーを結合して返します。このアプローチでは、外部コマンドがハングアップした場合、cmd.CombinedOutput() も無限にブロックし続け、呼び出し元のプログラム(cmd/go)も停止してしまいます。

変更後は、このブロッキングの問題を解決するために、以下の手順でタイムアウト処理が導入されました。

  1. 非同期実行: cmd.CombinedOutput() の代わりに cmd.Start() を使用して、テストプロセスを非同期で開始します。これにより、cmd/go はテストプロセスの開始後すぐに次の処理に進むことができます。
  2. ゴルーチンでの待機: テストプロセスの終了を待機するために、新しいゴルーチンが起動されます。このゴルーチンは cmd.Wait() を呼び出し、テストプロセスの終了ステータスを done チャネルに送信します。
  3. タイマーの設定: time.NewTimer(deadline) を使用して、1分間のタイムアウトタイマーを設定します。このタイマーは、指定された時間が経過すると tick.C チャネルにイベントを送信します。
  4. select による競合: select ステートメントを使用して、以下の2つのイベントのいずれかが発生するのを待ちます。
    • done チャネルからのイベント(テストプロセスの終了)
    • tick.C チャネルからのイベント(タイムアウト)
  5. タイムアウト処理:
    • もし tick.C からイベントが先に届いた場合(タイムアウトが発生した場合)、cmd.Process.Kill() を呼び出してテストプロセスを強制終了します。
    • その後、再度 <-done を待機して、強制終了されたプロセスの終了ステータスを取得します。
    • 標準エラー出力バッファに「*** Test killed: ran too long.\n」というメッセージを追加し、ユーザーにテストがタイムアウトで終了したことを通知します。
  6. タイマーの停止: tick.Stop() を呼び出して、タイマーが不要になった場合にリソースを解放します。

このメカニズムにより、テストプロセスが1分を超えて実行された場合でも、cmd/go はハングアップすることなく、テストを強制終了して次の処理に進むことができるようになりました。

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

変更は src/cmd/go/test.go ファイルの func (b *builder) runTest(a *action) error 関数内で行われています。

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -463,8 +463,30 @@ func (b *builder) runTest(a *action) error {
 
 	cmd := exec.Command(args[0], args[1:]...)
 	cmd.Dir = a.p.Dir
+	var buf bytes.Buffer
+	cmd.Stdout = &buf
+	cmd.Stderr = &buf
+
 	t0 := time.Now()
-	out, err := cmd.CombinedOutput()
+	err := cmd.Start()
+	const deadline = 1 * time.Minute
+	tick := time.NewTimer(deadline)
+	if err == nil {
+		done := make(chan error)
+		go func() {
+			done <- cmd.Wait()
+		}()
+		select {
+		case err = <-done:
+			// ok
+		case <-tick.C:
+			cmd.Process.Kill()
+			err = <-done
+			fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n")
+		}
+		tick.Stop()
+	}
+	out := buf.Bytes()
 	t1 := time.Now()
 	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())
 	if err == nil {

コアとなるコードの解説

変更されたコードブロックを詳細に見ていきます。

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = a.p.Dir
	var buf bytes.Buffer
	cmd.Stdout = &buf
	cmd.Stderr = &buf

exec.Command で実行するコマンドを設定し、その作業ディレクトリを a.p.Dir に設定します。bytes.Buffer を作成し、コマンドの標準出力と標準エラー出力をこのバッファにリダイレクトします。これにより、コマンドの出力はメモリ上のバッファに蓄積され、後で取得できるようになります。

	t0 := time.Now()
	err := cmd.Start()
	const deadline = 1 * time.Minute
	tick := time.NewTimer(deadline)

cmd.Start() を呼び出して、設定されたコマンドを非同期で実行します。これにより、cmd/go はコマンドの終了を待たずに次の行に進むことができます。deadline 定数でタイムアウト時間を1分に設定し、time.NewTimer でその時間後にイベントを発生させるタイマーを作成します。

	if err == nil {
		done := make(chan error)
		go func() {
			done <- cmd.Wait()
		}()

cmd.Start() がエラーなく成功した場合にのみ、タイムアウト処理に進みます。done というエラー型のチャネルを作成します。そして、新しいゴルーチンを起動し、その中で cmd.Wait() を呼び出します。cmd.Wait() は実行中のコマンドが終了するまでブロックし、その終了ステータス(エラー)を done チャネルに送信します。

		select {
		case err = <-done:
			// ok
		case <-tick.C:
			cmd.Process.Kill()
			err = <-done
			fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n")
		}
		tick.Stop()
	}

ここがタイムアウト処理の核心部分です。select ステートメントは、複数のチャネル操作のうち、準備ができた最初のものを実行します。

  • case err = <-done:: done チャネルから値が受信された場合、つまりテストプロセスが正常に(またはエラーで)終了した場合です。この場合、err 変数にその終了ステータスが代入され、select ブロックを抜けます。
  • case <-tick.C:: tick.C チャネルから値が受信された場合、つまり1分のタイムアウトが経過した場合です。
    • cmd.Process.Kill(): テストプロセスを強制終了します。
    • err = <-done: 強制終了されたプロセスの終了を待ち、その終了ステータスを取得します。Kill() を呼び出した後でも、プロセスが完全に終了するのを待つ必要があります。
    • fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n"): テストがタイムアウトで強制終了されたことを示すメッセージを、テスト出力バッファに追加します。 select ブロックの実行後、tick.Stop() を呼び出してタイマーを停止します。これは、タイマーがすでに発火している場合でも、不要なリソースの消費を防ぐために重要です。
	out := buf.Bytes()
	t1 := time.Now()
	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())
	if err == nil {
		// ... (後続の処理)
	}

最終的に、buf に蓄積されたテストの標準出力と標準エラー出力を out 変数にバイトスライスとして取得します。テストの実行時間を計算し、その後の処理(テスト結果の解析など)に進みます。

この変更により、cmd/go はテストプロセスのハングアップに対してより堅牢になり、Goのビルドシステムの安定性に貢献しました。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語のコマンドラインツール cmd/go におけるテスト実行時のタイムアウト処理に関するものです。具体的には、テストが長時間ハングアップするのを防ぐため、テストプロセスが1分以上実行された場合に強制終了するメカニズムを導入しています。これにより、Goのビルドシステム(ビルダー)がテストのハングアップによって停止するのを防ぐことが目的です。

コミット

commit 0c012af11464ad1d5f2f188f6026c3b8a5483ca4
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 10 21:01:58 2012 -0800

    cmd/go: kill test.out after 1 minute
    
    Will have to do better but this is enough to
    stop the builders from hanging, I hope.
    
    R=golang-dev, dsymonds, adg
    CC=golang-dev
    https://golang.org/cl/5533066

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

https://github.com/golang/go/commit/0c012af11464ad1d5f2f188f6026c3b8a5483ca4

元コミット内容

cmd/go: kill test.out after 1 minute

Will have to do better but this is enough to
stop the builders from hanging, I hope.

R=golang-dev, dsymonds, adg
CC=golang-dev
https://golang.org/cl/5533066

変更の背景

このコミットの主な背景は、Go言語の自動ビルドシステム(通称「ビルダー」)が、テストプロセスのハングアップによって停止してしまう問題に対処することです。テストが無限ループに陥ったり、外部リソースの応答待ちでデッドロックしたりすると、テストプロセスが終了せず、結果としてビルダーが次のタスクに進めなくなります。

コミットメッセージにある「Will have to do better but this is enough to stop the builders from hanging, I hope.」という記述から、これは一時的または緊急の対策であり、より洗練された解決策が将来的に必要であるという認識があったことが伺えます。しかし、差し当たってビルダーの安定稼働を確保することが最優先であったため、1分というタイムアウトを設定し、それを超えたテストプロセスを強制終了するシンプルなメカニズムが導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念と標準ライブラリの知識が必要です。

  • cmd/go: Go言語の公式ツールチェーンの一部であり、Goプログラムのビルド、テスト、実行などを管理するコマンドラインツールです。このコミットは、go test コマンドの内部実装に関連しています。
  • os/exec パッケージ: 外部コマンドを実行するためのGoの標準ライブラリパッケージです。exec.Command は実行するコマンドと引数を設定し、cmd.Start() はコマンドを非同期で開始し、cmd.Wait() はコマンドの終了を待ちます。cmd.CombinedOutput() はコマンドの実行を待ち、標準出力と標準エラー出力を結合して返します。
  • time パッケージ: 時間の計測と操作を行うためのGoの標準ライブラリパッケージです。
    • time.Minute: 1分を表す time.Duration 型の定数です。
    • time.NewTimer(d time.Duration): 指定された期間 d が経過した後に、チャネルに現在の時刻を送信する Timer を作成します。
    • tick.C: Timer が時間切れになったときに値が送信されるチャネルです。
    • tick.Stop(): Timer を停止し、チャネルへの送信を防ぎます。
  • Goroutine (ゴルーチン): Go言語における軽量な並行実行単位です。関数呼び出しの前に go キーワードを付けることで、その関数を新しいゴルーチンとして実行できます。これにより、複数の処理を同時に実行することが可能になります。
  • Channel (チャネル): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期とデータ転送を可能にします。
  • select ステートメント: 複数のチャネル操作を待機し、準備ができた最初の操作を実行するためのGoの制御構造です。select は、並行処理における非ブロッキング操作やタイムアウト処理の実装に不可欠です。

技術的詳細

このコミットの技術的な核心は、外部プロセス(テスト実行)のタイムアウト処理を実装するために、Goの並行処理機能(ゴルーチン、チャネル、select)を効果的に利用している点です。

変更前は、cmd.CombinedOutput() を使用してテストプロセスを実行していました。この関数は、外部コマンドの実行が完了するまでブロックし、その出力とエラーを結合して返します。このアプローチでは、外部コマンドがハングアップした場合、cmd.CombinedOutput() も無限にブロックし続け、呼び出し元のプログラム(cmd/go)も停止してしまいます。

変更後は、このブロッキングの問題を解決するために、以下の手順でタイムアウト処理が導入されました。

  1. 非同期実行: cmd.CombinedOutput() の代わりに cmd.Start() を使用して、テストプロセスを非同期で開始します。これにより、cmd/go はテストプロセスの開始後すぐに次の処理に進むことができます。
  2. ゴルーチンでの待機: テストプロセスの終了を待機するために、新しいゴルーチンが起動されます。このゴルーチンは cmd.Wait() を呼び出し、テストプロセスの終了ステータスを done チャネルに送信します。
  3. タイマーの設定: time.NewTimer(deadline) を使用して、1分間のタイムアウトタイマーを設定します。このタイマーは、指定された時間が経過すると tick.C チャネルにイベントを送信します。
  4. select による競合: select ステートメントを使用して、以下の2つのイベントのいずれかが発生するのを待ちます。
    • done チャネルからのイベント(テストプロセスの終了)
    • tick.C チャネルからのイベント(タイムアウト)
  5. タイムアウト処理:
    • もし tick.C からイベントが先に届いた場合(タイムアウトが発生した場合)、cmd.Process.Kill() を呼び出してテストプロセスを強制終了します。
    • その後、再度 <-done を待機して、強制終了されたプロセスの終了ステータスを取得します。
    • 標準エラー出力バッファに「*** Test killed: ran too long.\n」というメッセージを追加し、ユーザーにテストがタイムアウトで終了したことを通知します。
  6. タイマーの停止: tick.Stop() を呼び出して、タイマーが不要になった場合にリソースを解放します。

このメカニズムにより、テストプロセスが1分を超えて実行された場合でも、cmd/go はハングアップすることなく、テストを強制終了して次の処理に進むことができるようになりました。

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

変更は src/cmd/go/test.go ファイルの func (b *builder) runTest(a *action) error 関数内で行われています。

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -463,8 +463,30 @@ func (b *builder) runTest(a *action) error {
 
 	cmd := exec.Command(args[0], args[1:]...)
 	cmd.Dir = a.p.Dir
+	var buf bytes.Buffer
+	cmd.Stdout = &buf
+	cmd.Stderr = &buf
+
 	t0 := time.Now()
-	out, err := cmd.CombinedOutput()
+	err := cmd.Start()
+	const deadline = 1 * time.Minute
+	tick := time.NewTimer(deadline)
+	if err == nil {
+		done := make(chan error)
+		go func() {
+			done <- cmd.Wait()
+		}()
+		select {
+		case err = <-done:
+			// ok
+		case <-tick.C:
+			cmd.Process.Kill()
+			err = <-done
+			fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n")
+		}
+		tick.Stop()
+	}
+	out := buf.Bytes()
 	t1 := time.Now()
 	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())
 	if err == nil {

コアとなるコードの解説

変更されたコードブロックを詳細に見ていきます。

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = a.p.Dir
	var buf bytes.Buffer
	cmd.Stdout = &buf
	cmd.Stderr = &buf

exec.Command で実行するコマンドを設定し、その作業ディレクトリを a.p.Dir に設定します。bytes.Buffer を作成し、コマンドの標準出力と標準エラー出力をこのバッファにリダイレクトします。これにより、コマンドの出力はメモリ上のバッファに蓄積され、後で取得できるようになります。

	t0 := time.Now()
	err := cmd.Start()
	const deadline = 1 * time.Minute
	tick := time.NewTimer(deadline)

cmd.Start() を呼び出して、設定されたコマンドを非同期で実行します。これにより、cmd/go はコマンドの終了を待たずに次の行に進むことができます。deadline 定数でタイムアウト時間を1分に設定し、time.NewTimer でその時間後にイベントを発生させるタイマーを作成します。

	if err == nil {
		done := make(chan error)
		go func() {
			done <- cmd.Wait()
		}()

cmd.Start() がエラーなく成功した場合にのみ、タイムアウト処理に進みます。done というエラー型のチャネルを作成します。そして、新しいゴルーチンを起動し、その中で cmd.Wait() を呼び出します。cmd.Wait() は実行中のコマンドが終了するまでブロックし、その終了ステータス(エラー)を done チャネルに送信します。

		select {
		case err = <-done:
			// ok
		case <-tick.C:
			cmd.Process.Kill()
			err = <-done
			fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n")
		}
		tick.Stop()
	}

ここがタイムアウト処理の核心部分です。select ステートメントは、複数のチャネル操作のうち、準備ができた最初のものを実行します。

  • case err = <-done:: done チャネルから値が受信された場合、つまりテストプロセスが正常に(またはエラーで)終了した場合です。この場合、err 変数にその終了ステータスが代入され、select ブロックを抜けます。
  • case <-tick.C:: tick.C チャネルから値が受信された場合、つまり1分のタイムアウトが経過した場合です。
    • cmd.Process.Kill(): テストプロセスを強制終了します。
    • err = <-done: 強制終了されたプロセスの終了を待ち、その終了ステータスを取得します。Kill() を呼び出した後でも、プロセスが完全に終了するのを待つ必要があります。
    • fmt.Fprintf(&buf, "*** Test killed: ran too long.\\n"): テストがタイムアウトで強制終了されたことを示すメッセージを、テスト出力バッファに追加します。 select ブロックの実行後、tick.Stop() を呼び出してタイマーを停止します。これは、タイマーがすでに発火している場合でも、不要なリソースの消費を防ぐために重要です。
	out := buf.Bytes()
	t1 := time.Now()
	t := fmt.Sprintf("%.3fs", t1.Sub(t0).Seconds())
	if err == nil {
		// ... (後続の処理)
	}

最終的に、buf に蓄積されたテストの標準出力と標準エラー出力を out 変数にバイトスライスとして取得します。テストの実行時間を計算し、その後の処理(テスト結果の解析など)に進みます。

この変更により、cmd/go はテストプロセスのハングアップに対してより堅牢になり、Goのビルドシステムの安定性に貢献しました。

関連リンク

参考にした情報源リンク