[インデックス 13836] ファイルの概要
このコミットは、Goプロジェクトのダッシュボードビルダ (misc/dashboard/builder
) において、外部コマンドの実行にタイムアウト機構を導入するものです。これにより、ハングアップした外部プロセスがビルドプロセス全体を停止させることを防ぎ、ビルドシステムの堅牢性を向上させます。
コミット
commit f005fedfa94bb2da726024953996bde40fc1e0fd
Author: Andrew Gerrand <adg@golang.org>
Date: Mon Sep 17 10:41:47 2012 -0700
misc/dashboard/builder: add timeout to all external command invocations
Fixes #4083.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6498136
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f005fedfa94bb2da726024953996bde40fc1e0fd
元コミット内容
このコミットは、misc/dashboard/builder
ディレクトリ内のコードに対して、すべての外部コマンド呼び出しにタイムアウトを追加します。これはIssue #4083を修正するためのものです。
変更の背景
Goプロジェクトの継続的インテグレーション(CI)システムの一部であるダッシュボードビルダは、ビルドやテストのために様々な外部コマンド(例: hg pull
, hg clone
, go get
, go test
など)を実行します。これらの外部コマンドが何らかの理由でハングアップしたり、非常に長い時間実行され続けたりすると、ビルドプロセス全体が停止し、ビルド結果が得られなくなるという問題がありました。
Issue #4083は、まさにこの問題、すなわち外部コマンドがタイムアウトせずにハングアップし、ビルダが応答しなくなるという状況を報告していました。このコミットは、このような状況を回避し、ビルドシステムの信頼性と安定性を向上させることを目的としています。具体的には、各外部コマンドの実行に最大許容時間を設定し、その時間を超えた場合には強制的にプロセスを終了させるメカニズムを導入します。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
- Go言語の
os/exec
パッケージ: Go言語で外部コマンドを実行するための標準ライブラリです。exec.Command
でコマンドを構築し、cmd.Run()
、cmd.Start()
、cmd.Wait()
などのメソッドで実行を制御します。 - Go言語の
time
パッケージ: 時間の計測、期間の表現、タイマーの設定など、時間に関連する操作を行うためのパッケージです。time.Duration
型は時間の長さを表し、time.After
は指定された期間後に値が送信されるチャネルを返します。 - Go言語の
select
ステートメントとチャネル:select
ステートメントは、複数の通信操作(チャネルの送受信)を待機するために使用されます。このコミットでは、外部コマンドの完了を待つチャネルと、タイムアウトを待つチャネルのどちらかが先に準備できた場合に処理を進めるために利用されています。 - 継続的インテグレーション (CI) システム: ソフトウェア開発において、開発者がコードの変更を共有リポジトリに頻繁にマージし、その都度自動的にビルドとテストを実行するプラクティスです。Goプロジェクトのダッシュボードビルダは、このCIプロセスの一部を担っています。
- Mercurial (Hg): 当時のGoプロジェクトがバージョン管理システムとして使用していた分散型バージョン管理システムです。
hg pull
やhg clone
といったコマンドは、リポジトリの更新やクローンを行うために使用されます。 - Goコマンド (
go get
,go test
): Go言語のツールチェインに含まれるコマンドで、それぞれ依存関係の取得やパッケージのテスト実行に使用されます。
技術的詳細
このコミットの主要な技術的変更点は、外部コマンドの実行にタイムアウト機構を導入したことです。
-
waitWithTimeout
関数の導入:- この新しい関数は、
*exec.Cmd
とtime.Duration
を引数にとります。 cmd.Wait()
の呼び出しをゴルーチン内で実行し、その結果をチャネルerrc
に送信します。select
ステートメントを使用して、errc
からの結果(コマンドの終了)とtime.After(timeout)
チャネルからの信号(タイムアウト)のどちらが先に発生するかを待ちます。- もし
time.After(timeout)
が先に発生した場合、cmd.Process.Kill()
を呼び出して実行中のプロセスを強制終了させ、タイムアウトエラーを返します。 - これにより、外部コマンドがハングアップした場合でも、指定された時間で確実に終了させることができます。
- この新しい関数は、
-
run
およびrunLog
関数のシグネチャ変更:- 外部コマンドを実行する既存のヘルパー関数
run
とrunLog
に、新たにtimeout time.Duration
引数が追加されました。 - これらの関数内で
cmd.Run()
やcmd.Wait()
の代わりに、新しく導入されたwaitWithTimeout
関数が呼び出されるようになりました。
- 外部コマンドを実行する既存のヘルパー関数
-
タイムアウト設定の追加:
misc/dashboard/builder/main.go
に、コマンドラインフラグとしてbuildTimeout
とcmdTimeout
が追加されました。buildTimeout
はビルドとテスト全体の最大時間を設定し、デフォルトは60分です。cmdTimeout
は個々の外部コマンドの最大時間を設定し、デフォルトは2分です。- これにより、ビルダのオペレータがタイムアウト値を柔軟に設定できるようになりました。
-
既存の外部コマンド呼び出しの更新:
main.go
内のhg pull
,hg clone
,hg update
,go get
,go test
,hg log
など、すべての外部コマンド呼び出しが、新しいtimeout
引数を伴ってrun
またはrunLog
関数を呼び出すように変更されました。- ほとんどのケースで
*cmdTimeout
が使用されていますが、ビルドやテストの実行など、より長い時間を要する可能性のある操作には*buildTimeout
が適用されています。
これらの変更により、Goダッシュボードビルダは外部コマンドの実行が予期せず長引いた場合でも、自動的にそれを検出し、終了させることができるようになり、ビルドシステムの安定性が大幅に向上しました。
コアとなるコードの変更箇所
misc/dashboard/builder/exec.go
// run is a simple wrapper for exec.Run/Close
-func run(envv []string, dir string, argv ...string) error {
+func run(timeout time.Duration, envv []string, dir string, argv ...string) error {
if *verbose {
log.Println("run", argv)
}
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Dir = dir
cmd.Env = envv
cmd.Stderr = os.Stderr
- return cmd.Run()
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ return waitWithTimeout(timeout, cmd)
}
// runLog runs a process and returns the combined stdout/stderr,
@@ -29,7 +34,7 @@
// process combined stdout and stderr output, exit status and error.
// The error returned is nil, if process is started successfully,
// even if exit status is not successful.
-func runLog(envv []string, logfile, dir string, argv ...string) (string, int, error) {
+func runLog(timeout time.Duration, envv []string, logfile, dir string, argv ...string) (string, int, error) {
if *verbose {
log.Println("runLog", argv)
}
@@ -56,8 +61,23 @@
return "", 1, startErr
}
exitStatus := 0
- if err := cmd.Wait(); err != nil {
+ if err := waitWithTimeout(timeout, cmd); err != nil {
exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead.
}
return b.String(), exitStatus, nil
}
+func waitWithTimeout(timeout time.Duration, cmd *exec.Cmd) error {
+ errc := make(chan error, 1)
+ go func() {
+ errc <- cmd.Wait()
+ }()
+ var err error
+ select {
+ case <-time.After(timeout):
+ cmd.Process.Kill()
+ err = fmt.Errorf("timed out after %v", timeout)
+ case err = <-errc:
+ }
+ return err
+}
misc/dashboard/builder/main.go
@@ -54,6 +54,8 @@
buildCmd = flag.String("cmd", filepath.Join(".", allCmd), "Build command (specify relative to go/src/)")
failAll = flag.Bool("fail", false, "fail all builds")
parallel = flag.Bool("parallel", false, "Build multiple targets in parallel")
+ buildTimeout = flag.Duration("buildTimeout", 60*time.Minute, "Maximum time to wait for builds and tests")
+ cmdTimeout = flag.Duration("cmdTimeout", 2*time.Minute, "Maximum time to wait for an external command")
verbose = flag.Bool("v", false, "verbose")
)
@@ -220,7 +222,7 @@
// Look for hash locally before running hg pull.
if _, err := fullHash(goroot, hash[:12]); err != nil {
// Don't have hash, so run hg pull.
- if err := run(nil, goroot, hgCmd("pull")...); err != nil {
+ if err := run(*cmdTimeout, nil, goroot, hgCmd("pull")...); err != nil {
log.Println("hg pull failed:", err)
return false
}
@@ -243,12 +245,12 @@
defer os.RemoveAll(workpath)
// clone repo
- if err := run(nil, workpath, hgCmd("clone", goroot, "go")...); err != nil {
+ if err := run(*cmdTimeout, nil, workpath, hgCmd("clone", goroot, "go")...); err != nil {
return err
}
// update to specified revision
- if err := run(nil, filepath.Join(workpath, "go"), hgCmd("update", hash)...); err != nil {
+ if err := run(*cmdTimeout, nil, filepath.Join(workpath, "go"), hgCmd("update", hash)...); err != nil {
return err
}
@@ -261,7 +263,7 @@
cmd = filepath.Join(srcDir, cmd)
}
startTime := time.Now()
- buildLog, status, err := runLog(b.envv(), logfile, srcDir, cmd)
+ buildLog, status, err := runLog(*buildTimeout, b.envv(), logfile, srcDir, cmd)
runTime := time.Now().Sub(startTime)
if err != nil {
return fmt.Errorf("%s: %s", *buildCmd, err)
@@ -353,28 +355,22 @@
}
// fetch package and dependencies
- log, status, err := runLog(env, "", goRoot, goTool, "get", "-d", pkg)
+ log, status, err := runLog(*cmdTimeout, env, "", goRoot, goTool, "get", "-d", pkg)
if err == nil && status != 0 {
err = fmt.Errorf("go exited with status %d", status)
}
if err != nil {
- // 'go get -d' will fail for a subrepo because its top-level
- // directory does not contain a go package. No matter, just
- // check whether an hg directory exists and proceed.
- // hgDir := filepath.Join(goRoot, "src/pkg", pkg, ".hg")
- // if fi, e := os.Stat(hgDir); e != nil || !fi.IsDir() {
- // return log, err
- // }
+ return log, err
}
// hg update to the specified hash
pkgPath := filepath.Join(goRoot, "src/pkg", pkg)
- if err := run(nil, pkgPath, hgCmd("update", hash)...); err != nil {
+ if err := run(*cmdTimeout, nil, pkgPath, hgCmd("update", hash)...); err != nil {
return "", err
}
// test the package
- log, status, err = runLog(env, "", goRoot, goTool, "test", "-short", pkg+"/...")
+ log, status, err = runLog(*buildTimeout, env, "", goRoot, goTool, "test", "-short", pkg+"/...")
if err == nil && status != 0 {
err = fmt.Errorf("go exited with status %d", status)
}
@@ -475,7 +471,7 @@
}
func hgClone(url, path string) error {
- return run(nil, *buildroot, hgCmd("clone", url, path)...)
+ return run(*cmdTimeout, nil, *buildroot, hgCmd("clone", url, path)...)
}
func hgRepoExists(path string) bool {
@@ -532,14 +528,14 @@
}
}
- if err := run(nil, pkgRoot, hgCmd("pull")...); err != nil {
+ if err := run(*cmdTimeout, nil, pkgRoot, hgCmd("pull")...); err != nil {
log.Printf("hg pull: %v", err)
return
}
const N = 50 // how many revisions to grab
- data, _, err := runLog(nil, "", pkgRoot, hgCmd("log",
+ data, _, err := runLog(*cmdTimeout, nil, "", pkgRoot, hgCmd("log",
"--encoding=utf-8",
"--limit="+strconv.Itoa(N),
"--template="+xmlLogTemplate)...,
@@ -627,7 +623,7 @@
// fullHash returns the full hash for the given Mercurial revision.
func fullHash(root, rev string) (string, error) {
- s, _, err := runLog(nil, "", root,
+ s, _, err := runLog(*cmdTimeout, nil, "", root,
hgCmd("log",
"--encoding=utf-8",
"--rev="+rev,
コアとなるコードの解説
misc/dashboard/builder/exec.go
-
run
関数とrunLog
関数の変更:- これらの関数は、外部コマンドを実行するためのラッパーです。変更前は
cmd.Run()
またはcmd.Wait()
を直接呼び出していましたが、変更後はwaitWithTimeout
関数を呼び出すように修正されました。 - これにより、外部コマンドの実行が
waitWithTimeout
によって監視され、指定されたタイムアウト時間を超えた場合に強制終了されるようになりました。 - 関数のシグネチャに
timeout time.Duration
が追加され、呼び出し元からタイムアウト値を指定できるようになりました。
- これらの関数は、外部コマンドを実行するためのラッパーです。変更前は
-
waitWithTimeout
関数の新規追加:- この関数は、
*exec.Cmd
(実行中のコマンド)とtime.Duration
(タイムアウト時間)を受け取ります。 cmd.Wait()
はコマンドの終了を待機するブロッキング呼び出しですが、これを独立したゴルーチンで実行することで、メインのゴルーチンがタイムアウトの監視と並行して待機できるようになります。select
ステートメントは、errc
チャネル(コマンドの終了を通知)とtime.After(timeout)
チャネル(タイムアウトを通知)のどちらかが先にイベントを発生させるかを待ちます。- もし
time.After(timeout)
が先にイベントを発生させた場合、それはコマンドがタイムアウトしたことを意味します。この場合、cmd.Process.Kill()
を呼び出してプロセスを強制終了し、fmt.Errorf("timed out after %v", timeout)
でタイムアウトエラーを生成して返します。 - これにより、外部コマンドが無限に実行され続けることを防ぎ、ビルドプロセスのハングアップを回避します。
- この関数は、
misc/dashboard/builder/main.go
-
フラグの追加:
flag.Duration
を使用して、buildTimeout
とcmdTimeout
という2つの新しいコマンドラインフラグが定義されました。buildTimeout
はビルドとテスト全体のタイムアウト(デフォルト60分)を、cmdTimeout
は個々の外部コマンドのタイムアウト(デフォルト2分)を設定します。これにより、ビルダの動作を柔軟に設定できるようになりました。
-
既存の外部コマンド呼び出しの更新:
main.go
内のhgCmd
やgoTool
を使ったすべての外部コマンド呼び出し(例:hg pull
,hg clone
,go get
,go test
,hg log
など)が、run
またはrunLog
関数に*cmdTimeout
または*buildTimeout
を渡すように変更されました。- 特に、
b.buildHash
メソッド内の実際のビルドコマンド実行には*buildTimeout
が適用されており、これはビルドプロセス全体が長引く可能性を考慮したものです。 go get -d
がサブモジュールに対して失敗する可能性に関するコメントアウトされたロジックが削除されました。これは、タイムアウト機構が導入されたことで、ハングアップによる問題が解決され、この特定の回避策が不要になったためと考えられます。
これらの変更により、Goダッシュボードビルダは、外部コマンドの実行が予期せず長引いた場合でも、自動的にそれを検出し、終了させることができるようになり、ビルドシステムの安定性が大幅に向上しました。
関連リンク
- Go Code Review (CL) 6498136: https://golang.org/cl/6498136
- このコミットの元となったコードレビューのページです。詳細な議論や変更履歴を確認できます。
- Go Issue 4083: このコミットが修正したとされるIssue #4083は、GoプロジェクトのIssueトラッカー上では直接見つかりませんでした。しかし、CLのリンクから、この変更が外部コマンドのハングアップ問題に対処するためのものであることが確認できます。当時のGoプロジェクトのIssue管理システムや、内部的なトラッキング番号である可能性があります。
参考にした情報源リンク
- Go言語の
os/exec
パッケージドキュメント: https://pkg.go.dev/os/exec - Go言語の
time
パッケージドキュメント: https://pkg.go.dev/time - Go言語の
select
ステートメントに関するドキュメント: https://go.dev/tour/concurrency/5 - Mercurial (Hg) 公式サイト: https://www.mercurial-scm.org/
- Goコマンドに関するドキュメント: https://go.dev/cmd/go/
- 継続的インテグレーション (CI) の概念に関する一般的な情報源 (例: Wikipediaなど)