[インデックス 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
)も停止してしまいます。
変更後は、このブロッキングの問題を解決するために、以下の手順でタイムアウト処理が導入されました。
- 非同期実行:
cmd.CombinedOutput()
の代わりにcmd.Start()
を使用して、テストプロセスを非同期で開始します。これにより、cmd/go
はテストプロセスの開始後すぐに次の処理に進むことができます。 - ゴルーチンでの待機: テストプロセスの終了を待機するために、新しいゴルーチンが起動されます。このゴルーチンは
cmd.Wait()
を呼び出し、テストプロセスの終了ステータスをdone
チャネルに送信します。 - タイマーの設定:
time.NewTimer(deadline)
を使用して、1分間のタイムアウトタイマーを設定します。このタイマーは、指定された時間が経過するとtick.C
チャネルにイベントを送信します。 select
による競合:select
ステートメントを使用して、以下の2つのイベントのいずれかが発生するのを待ちます。done
チャネルからのイベント(テストプロセスの終了)tick.C
チャネルからのイベント(タイムアウト)
- タイムアウト処理:
- もし
tick.C
からイベントが先に届いた場合(タイムアウトが発生した場合)、cmd.Process.Kill()
を呼び出してテストプロセスを強制終了します。 - その後、再度
<-done
を待機して、強制終了されたプロセスの終了ステータスを取得します。 - 標準エラー出力バッファに「*** Test killed: ran too long.\n」というメッセージを追加し、ユーザーにテストがタイムアウトで終了したことを通知します。
- もし
- タイマーの停止:
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のビルドシステムの安定性に貢献しました。
関連リンク
- Go CL 5533066: https://golang.org/cl/5533066
参考にした情報源リンク
- Go言語
os/exec
パッケージ: https://pkg.go.dev/os/exec - Go言語
time
パッケージ: https://pkg.go.dev/time - Go言語における並行処理 (Goroutines and Channels): https://go.dev/tour/concurrency/1
- Go言語
select
ステートメント: https://go.dev/tour/concurrency/5 - Go言語におけるタイムアウト処理のパターン: (一般的なGoの並行処理に関する記事やドキュメントを参照)
- 例: https://gobyexample.com/timeouts (Go by Example - Timeouts)
- 例: https://go.dev/blog/pipelines (Go Concurrency Patterns: Pipelines and Cancellation)```markdown
[インデックス 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
)も停止してしまいます。
変更後は、このブロッキングの問題を解決するために、以下の手順でタイムアウト処理が導入されました。
- 非同期実行:
cmd.CombinedOutput()
の代わりにcmd.Start()
を使用して、テストプロセスを非同期で開始します。これにより、cmd/go
はテストプロセスの開始後すぐに次の処理に進むことができます。 - ゴルーチンでの待機: テストプロセスの終了を待機するために、新しいゴルーチンが起動されます。このゴルーチンは
cmd.Wait()
を呼び出し、テストプロセスの終了ステータスをdone
チャネルに送信します。 - タイマーの設定:
time.NewTimer(deadline)
を使用して、1分間のタイムアウトタイマーを設定します。このタイマーは、指定された時間が経過するとtick.C
チャネルにイベントを送信します。 select
による競合:select
ステートメントを使用して、以下の2つのイベントのいずれかが発生するのを待ちます。done
チャネルからのイベント(テストプロセスの終了)tick.C
チャネルからのイベント(タイムアウト)
- タイムアウト処理:
- もし
tick.C
からイベントが先に届いた場合(タイムアウトが発生した場合)、cmd.Process.Kill()
を呼び出してテストプロセスを強制終了します。 - その後、再度
<-done
を待機して、強制終了されたプロセスの終了ステータスを取得します。 - 標準エラー出力バッファに「*** Test killed: ran too long.\n」というメッセージを追加し、ユーザーにテストがタイムアウトで終了したことを通知します。
- もし
- タイマーの停止:
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のビルドシステムの安定性に貢献しました。
関連リンク
- Go CL 5533066: https://golang.org/cl/5533066
参考にした情報源リンク
- Go言語
os/exec
パッケージ: https://pkg.go.dev/os/exec - Go言語
time
パッケージ: https://pkg.go.dev/time - Go言語における並行処理 (Goroutines and Channels): https://go.dev/tour/concurrency/1
- Go言語
select
ステートメント: https://go.dev/tour/concurrency/5 - Go言語におけるタイムアウト処理のパターン: (一般的なGoの並行処理に関する記事やドキュメントを参照)
- 例: https://gobyexample.com/timeouts (Go by Example - Timeouts)
- 例: https://go.dev/blog/pipelines (Go Concurrency Patterns: Pipelines and Cancellation)