[インデックス 16933] ファイルの概要
このコミットは、Go言語のテスト実行コマンド (cmd/go
) において、タイムアウトしたテストプロセスを終了させる際の挙動を改善するものです。具体的には、従来の SIGKILL
による即時終了の前に SIGQUIT
シグナルを送信することで、プロセスが終了する際にスタックトレースを出力する機会を与え、デバッグ情報の収集を試みる変更が加えられています。
コミット
commit 57933b86b1a09a44d1350437f42a3305a30ad2b3
Author: Russ Cox <rsc@golang.org>
Date: Tue Jul 30 22:52:10 2013 -0400
cmd/go: send timed out test SIGQUIT before SIGKILL
There is a chance that the SIGQUIT will make the test process
dump its stacks as part of exiting, which would be nice for
finding out what it is doing.
Right now the builders are occasionally timing out running
the runtime test. I hope this will give us some information
about the state of the runtime.
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/12041051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/57933b86b1a09a44d1350437f42a3305a30ad2b3
元コミット内容
このコミットは、Goのテスト実行コマンド (go test
) がテストプロセスをタイムアウトで終了させる際に、SIGKILL
を送る前に SIGQUIT
シグナルを送るように変更します。これにより、タイムアウトしたテストプロセスが終了時にスタックトレースを出力する可能性があり、そのプロセスの状態に関するデバッグ情報を収集できることが期待されます。
変更の背景
コミットメッセージに明記されているように、当時のGoのビルドシステム(Go builders)では、runtime
テストが時折タイムアウトする問題が発生していました。このタイムアウトの原因を特定するためには、タイムアウト時にテストプロセスがどのような状態にあったかを知る必要がありました。しかし、単に SIGKILL
でプロセスを強制終了するだけでは、その時点でのプロセスの内部状態に関する情報は得られません。
そこで、SIGQUIT
シグナルを利用することで、プロセスが終了する際にスタックトレースを生成し、その情報をログに残すことを試みました。これにより、タイムアウトの原因となっている可能性のある無限ループやデッドロックなどの問題を特定するための手がかりを得ることが目的でした。
前提知識の解説
このコミットの変更を理解するためには、以下の概念についての知識が必要です。
1. Unixシグナル
Unix系OSでは、プロセス間通信やプロセス制御のために「シグナル」という仕組みが使われます。シグナルは、特定のイベントが発生したことをプロセスに通知するソフトウェア割り込みのようなものです。
-
SIGQUIT
(Signal Quit):- プロセスに終了を要求するシグナルの一つです。
- 通常、プロセスは
SIGQUIT
を受け取ると、コアダンプ(プロセスのメモリイメージをファイルに保存したもの)を生成して終了します。このコアダンプには、プロセスが終了した時点でのスタックトレースなどの情報が含まれるため、デバッグに非常に有用です。 SIGQUIT
は、SIGTERM
と異なり、通常はプロセスによって捕捉(catch)されたり無視(ignore)されたりすることはありませんが、SIGKILL
ほど強制的な終了ではありません。プロセスはシグナルを受け取ってから終了処理を行う猶予があります。
-
SIGKILL
(Signal Kill):- プロセスを即座に強制終了させるシグナルです。
- このシグナルはプロセスによって捕捉、ブロック、無視することができません。つまり、
SIGKILL
を受け取ったプロセスは、いかなる状況でも直ちに終了します。 - 非常に強力なシグナルであるため、プロセスが応答しない場合の最終手段として使用されます。しかし、プロセスがクリーンアップ処理を行う機会がないため、データ損失やリソースリークの原因となる可能性があります。また、デバッグ情報(スタックトレースなど)を得ることは困難です。
2. Go言語の os/exec
パッケージ
Go言語の os/exec
パッケージは、外部コマンドを実行するための機能を提供します。
-
cmd.Process.Signal(sig os.Signal)
:- 実行中の外部プロセスに指定されたシグナルを送信します。
- このコミットでは、
SIGQUIT
を送信するために使用されます。
-
cmd.Process.Kill()
:- 実行中の外部プロセスに
SIGKILL
シグナルを送信し、強制終了させます。 - これは
cmd.Process.Signal(syscall.SIGKILL)
と同等です。
- 実行中の外部プロセスに
3. Go言語の select
ステートメントと time.After
-
select
ステートメント:- 複数の通信操作(チャネルの送受信)を待機し、準備ができた最初の操作を実行します。
- このコミットでは、テストプロセスの終了を待つチャネル (
done
) と、タイムアウトを通知するチャネル (tick.C
またはtime.After
が返すチャネル) の両方を待機するために使用されます。
-
time.After(d Duration)
:- 指定された期間
d
が経過した後に、現在の時刻を送信するチャネルを返します。 - このコミットでは、テストのタイムアウトや、
SIGQUIT
送信後の猶予期間を設定するために使用されます。
- 指定された期間
4. Go言語の go test
コマンド
go test
コマンドは、Goパッケージ内のテストを実行するための標準的なツールです。テストの実行、結果の表示、ベンチマークの実行など、様々な機能を提供します。このコマンドは、テストプロセスがタイムアウトした場合に、そのプロセスを終了させるロジックを含んでいます。
技術的詳細
このコミットの主要な変更は、src/cmd/go/test.go
ファイル内の runTest
関数にあります。この関数は、go test
コマンドが個々のテストを実行する際のロジックをカプセル化しています。
変更前は、テストがタイムアウトした場合、select
ステートメントの case <-tick.C:
ブロック内で直接 cmd.Process.Kill()
が呼び出され、テストプロセスは即座に SIGKILL
によって強制終了されていました。
変更後は、タイムアウトが発生した際に、以下のロジックが追加されました。
-
signalTrace
の確認:if signalTrace != nil
という条件が追加されました。signalTrace
は、Unix系システムではsyscall.SIGQUIT
に、非Unix系システムではnil
に設定されます。これにより、SIGQUIT
が利用可能な環境でのみ以下の処理が実行されます。
-
SIGQUIT
の送信:cmd.Process.Signal(signalTrace)
を呼び出し、テストプロセスにSIGQUIT
シグナルを送信します。これにより、プロセスがスタックトレースをダンプして終了する機会が与えられます。
-
猶予期間の待機:
select
ステートメント内でtime.After(5 * time.Second)
を使用して、5秒間の猶予期間を設けます。- この5秒間にテストプロセスが終了した場合(
err = <-done
)、SIGQUIT
が効果を発揮したと判断し、Outer
ラベル付きのselect
ループをbreak
します。この際、標準エラー出力に「*** Test killed with SIGQUIT: ran too long (...)」というメッセージが出力されます。
-
SIGKILL
へのフォールバック:- もし5秒間の猶予期間内にテストプロセスが終了しなかった場合、最終手段として
cmd.Process.Kill()
が呼び出され、SIGKILL
によってプロセスが強制終了されます。この場合も、標準エラー出力に「*** Test killed: ran too long (...)」というメッセージが出力されます。
- もし5秒間の猶予期間内にテストプロセスが終了しなかった場合、最終手段として
このシーケンスにより、タイムアウトしたテストから可能な限り多くのデバッグ情報を引き出すための試みがなされています。
コアとなるコードの変更箇所
src/cmd/go/signal_notunix.go
--- a/src/cmd/go/signal_notunix.go
+++ b/src/cmd/go/signal_notunix.go
@@ -11,3 +11,7 @@ import (
)
var signalsToIgnore = []os.Signal{os.Interrupt}
+
+// signalTrace is the signal to send to make a Go program
+// crash with a stack trace.
+var signalTrace os.Signal = nil
非Unix系システムでは SIGQUIT
が存在しないため、signalTrace
は nil
に設定されます。
src/cmd/go/signal_unix.go
--- a/src/cmd/go/signal_unix.go
+++ b/src/cmd/go/signal_unix.go
@@ -12,3 +12,7 @@ import (
)
var signalsToIgnore = []os.Signal{os.Interrupt, syscall.SIGQUIT}
+
+// signalTrace is the signal to send to make a Go program
+// crash with a stack trace.
+var signalTrace os.Signal = syscall.SIGQUIT
Unix系システムでは SIGQUIT
が存在するため、signalTrace
は syscall.SIGQUIT
に設定されます。
src/cmd/go/test.go
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -896,10 +896,23 @@ func (b *builder) runTest(a *action) error {
go func() {
done <- cmd.Wait()
}()
+ Outer:
select {
case err = <-done:
// ok
case <-tick.C:
+ if signalTrace != nil {
+ // Send a quit signal in the hope that the program will print
+ // a stack trace and exit. Give it five seconds before resorting
+ // to Kill.
+ cmd.Process.Signal(signalTrace)
+ select {
+ case err = <-done:
+ fmt.Fprintf(&buf, "*** Test killed with %v: ran too long (%v).\n", signalTrace, testKillTimeout)
+ break Outer
+ case <-time.After(5 * time.Second):
+ }
+ }
cmd.Process.Kill()
err = <-done
fmt.Fprintf(&buf, "*** Test killed: ran too long (%v).\n", testKillTimeout)
runTest
関数内のタイムアウト処理ロジックが変更され、SIGQUIT
を試行するステップが追加されました。
コアとなるコードの解説
このコミットの核心は、src/cmd/go/test.go
の runTest
関数におけるタイムアウト処理の変更です。
select {
case err = <-done:
// ok
case <-tick.C: // テストがタイムアウトした場合
if signalTrace != nil { // Unix系システムの場合 (signalTrace が SIGQUIT)
// プロセスにSIGQUITを送信し、スタックトレースを出力して終了することを期待する
cmd.Process.Signal(signalTrace)
select {
case err = <-done: // SIGQUIT送信後5秒以内にプロセスが終了した場合
fmt.Fprintf(&buf, "*** Test killed with %v: ran too long (%v).\n", signalTrace, testKillTimeout)
break Outer // 外側のselectループを抜ける
case <-time.After(5 * time.Second): // 5秒以内に終了しなかった場合
// 5秒待っても終了しなかった場合は、次の行でSIGKILLが実行される
}
}
cmd.Process.Kill() // 最終的にSIGKILLで強制終了
err = <-done
fmt.Fprintf(&buf, "*** Test killed: ran too long (%v).\n", testKillTimeout)
このコードは、テストプロセスが testKillTimeout
で設定された時間を超えて実行された場合に発動します。
case <-tick.C:
が選択されると、テストがタイムアウトしたことを意味します。if signalTrace != nil
のチェックにより、現在のOSがSIGQUIT
をサポートしているか(つまりUnix系OSか)を確認します。- サポートしている場合、
cmd.Process.Signal(signalTrace)
を呼び出して、テストプロセスにSIGQUIT
シグナルを送信します。これにより、プロセスはスタックトレースを生成し、クリーンに終了しようと試みる可能性があります。 - 次に、ネストされた
select
ステートメントで5秒間待ちます。- もしこの5秒間にテストプロセスが終了した場合(
err = <-done
)、それはSIGQUIT
が効果を発揮したことを意味します。その旨のメッセージをログに出力し、break Outer
によって外側のselect
ループ全体を終了します。 - もし5秒経ってもプロセスが終了しなかった場合(
<-time.After(5 * time.Second)
)、SIGQUIT
ではプロセスを終了させられなかったと判断し、次の行のcmd.Process.Kill()
が実行されます。
- もしこの5秒間にテストプロセスが終了した場合(
- 最終的に、
cmd.Process.Kill()
が呼び出され、テストプロセスはSIGKILL
によって強制終了されます。これは、SIGQUIT
が成功しなかった場合、または非Unix系システムの場合に実行されます。
この変更により、Goのテストランナーは、タイムアウトしたテストからより有用なデバッグ情報を引き出すための、より洗練された終了戦略を採用するようになりました。
関連リンク
- Go言語の
os/exec
パッケージ: https://pkg.go.dev/os/exec - Go言語の
syscall
パッケージ: https://pkg.go.dev/syscall - Go言語の
time
パッケージ: https://pkg.go.dev/time - Unixシグナルに関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB_(Unix)
参考にした情報源リンク
- コミットメッセージとコード差分 (GitHub): https://github.com/golang/go/commit/57933b86b1a09a44d1350437f42a3305a30ad2b3
- Go Code Review (Gerrit): https://golang.org/cl/12041051 (コミットメッセージに記載されているCLリンク)