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

[インデックス 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 によって強制終了されていました。

変更後は、タイムアウトが発生した際に、以下のロジックが追加されました。

  1. signalTrace の確認:

    • if signalTrace != nil という条件が追加されました。signalTrace は、Unix系システムでは syscall.SIGQUIT に、非Unix系システムでは nil に設定されます。これにより、SIGQUIT が利用可能な環境でのみ以下の処理が実行されます。
  2. SIGQUIT の送信:

    • cmd.Process.Signal(signalTrace) を呼び出し、テストプロセスに SIGQUIT シグナルを送信します。これにより、プロセスがスタックトレースをダンプして終了する機会が与えられます。
  3. 猶予期間の待機:

    • select ステートメント内で time.After(5 * time.Second) を使用して、5秒間の猶予期間を設けます。
    • この5秒間にテストプロセスが終了した場合(err = <-done)、SIGQUIT が効果を発揮したと判断し、Outer ラベル付きの select ループを break します。この際、標準エラー出力に「*** Test killed with SIGQUIT: ran too long (...)」というメッセージが出力されます。
  4. SIGKILL へのフォールバック:

    • もし5秒間の猶予期間内にテストプロセスが終了しなかった場合、最終手段として cmd.Process.Kill() が呼び出され、SIGKILL によってプロセスが強制終了されます。この場合も、標準エラー出力に「*** Test killed: ran too long (...)」というメッセージが出力されます。

このシーケンスにより、タイムアウトしたテストから可能な限り多くのデバッグ情報を引き出すための試みがなされています。

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

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 が存在しないため、signalTracenil に設定されます。

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 が存在するため、signalTracesyscall.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.gorunTest 関数におけるタイムアウト処理の変更です。

		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 で設定された時間を超えて実行された場合に発動します。

  1. case <-tick.C: が選択されると、テストがタイムアウトしたことを意味します。
  2. if signalTrace != nil のチェックにより、現在のOSが SIGQUIT をサポートしているか(つまりUnix系OSか)を確認します。
  3. サポートしている場合、cmd.Process.Signal(signalTrace) を呼び出して、テストプロセスに SIGQUIT シグナルを送信します。これにより、プロセスはスタックトレースを生成し、クリーンに終了しようと試みる可能性があります。
  4. 次に、ネストされた 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のテストランナーは、タイムアウトしたテストからより有用なデバッグ情報を引き出すための、より洗練された終了戦略を採用するようになりました。

関連リンク

参考にした情報源リンク