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

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

このコミットは、Go言語のダッシュボードビルダにおけるビルド結果の記録方法を改善し、ビルダの失敗時のデバッグを容易にすることを目的としています。具体的には、サブコマンドの出力を制御するための新しい関数 runOutput を導入し、ビルドステータスをダッシュボードに送信できるように変更しています。

コミット

commit ea0292c61bfe85d85ef6f81fe1874227c5fc674a
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Feb 15 10:44:29 2013 +1100

    misc/dashboard/builder: record build result on dashboard
    
    This is part one of two changes intended to make it easier to debug builder failures.
    
    runOutput allows us to control the io.Writer passed to a subcommand. The intention is to add additional debugging information before and after the build which will then be capture and sent to the dashboard.
    
    In this proposal, the only additional information is the build status. See http://build.golang.org/log/e7b5bf435b4de1913fc61781b3295fb3f03aeb6e
    
    R=adg
    CC=golang-dev
    https://golang.org/cl/7303090

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

https://github.com/golang/go/commit/ea0292c61bfe85d85ef6f81fe1874227c5fc674a

元コミット内容

misc/dashboard/builder: record build result on dashboard

This is part one of two changes intended to make it easier to debug builder failures.

runOutput allows us to control the io.Writer passed to a subcommand. The intention is to add additional debugging information before and after the build which will then be capture and sent to the dashboard.

In this proposal, the only additional information is the build status. See http://build.golang.org/log/e7b5bf435b4de1913fc61781b3295fb3f03aeb6e

変更の背景

この変更は、Go言語のビルドダッシュボード(build.golang.org)におけるビルダの失敗をデバッグしやすくするための2段階の変更の第一段階です。既存のビルドプロセスでは、サブコマンドの出力が直接ログファイルに書き込まれるか、バッファに格納されるだけで、ビルドの成功/失敗ステータスや追加のデバッグ情報をダッシュボードに効率的に送信する仕組みが不足していました。

このコミットの主な目的は、サブコマンドの標準出力(stdout)と標準エラー出力(stderr)を柔軟に制御できるようにすることです。これにより、ビルドの前後に追加のデバッグ情報を挿入し、それらの情報をキャプチャしてダッシュボードに送信することが可能になります。この提案では、まずビルドステータス(成功/失敗)をダッシュボードに記録することに焦点を当てています。

前提知識の解説

  • Go言語のビルドダッシュボード (build.golang.org): Go言語の公式ビルドシステムであり、様々なプラットフォームやアーキテクチャでのGoのビルドとテストの状況を監視するウェブサービスです。継続的インテグレーション(CI)システムとして機能し、Goの変更が様々な環境で正しく動作するかを確認します。ビルダは、このダッシュボードにビルド結果を報告します。
  • io.Writer インターフェース: Go言語の標準ライブラリ io パッケージで定義されているインターフェースです。Write([]byte) (n int, err error) メソッドを持つ型は io.Writer として扱われます。これにより、ファイル、ネットワーク接続、メモリバッファなど、様々な出力先に統一された方法でデータを書き込むことができます。
  • bytes.Buffer: bytes パッケージで提供される可変長のバイトバッファです。io.Writer インターフェースを実装しており、メモリ上にデータを蓄積するのに便利です。
  • os.OpenFile: os パッケージの関数で、指定されたファイルを開きます。os.O_WRONLY (書き込み専用)、os.O_CREATE (ファイルが存在しない場合に作成)、os.O_APPEND (書き込み時にファイルの末尾に追加) などのフラグを指定できます。
  • io.MultiWriter: io パッケージの関数で、複数の io.Writer を結合し、単一の io.Writer として扱えるようにします。MultiWriter に書き込まれたデータは、結合されたすべての Writer に書き込まれます。これにより、例えば、出力をファイルに書き込みつつ、同時にメモリバッファにも書き込むといったことが可能になります。
  • os/exec パッケージ: Go言語で外部コマンドを実行するためのパッケージです。exec.Command でコマンドを構築し、cmd.Start() で実行を開始し、cmd.Wait() で完了を待ちます。cmd.Stdoutcmd.Stderr フィールドに io.Writer を設定することで、コマンドの標準出力と標準エラー出力のリダイレクトを制御できます。
  • waitWithTimeout: このコードベースで定義されているヘルパー関数で、指定されたタイムアウト時間内に外部コマンドの完了を待ちます。
  • 終了ステータス (Exit Status): 外部コマンドが終了する際に返す数値です。通常、0 は成功を示し、非ゼロの値はエラーを示します。
  • ブール値 (Boolean) での成功/失敗表現: 従来の終了ステータス(整数)の代わりに、true(成功)または false(失敗)のブール値でコマンドの実行結果を示すアプローチです。これは、Go言語の慣習として、エラーが error 型で返されるため、成功/失敗の論理的な結果をブール値で表現する方が自然な場合があります。

技術的詳細

このコミットの核心は、misc/dashboard/builder/exec.go にある runLog 関数の変更と、新しい runOutput 関数の導入、そして misc/dashboard/builder/main.go でこれらの関数がどのように利用されるかの変更です。

misc/dashboard/builder/exec.go の変更

  1. runLog 関数の変更:

    • 変更前: func runLog(timeout time.Duration, envv []string, logfile, dir string, argv ...string) (string, int, error)
      • logfile という引数を受け取り、そのファイルにログを書き込む責任を持っていました。
      • コマンドの終了ステータスを int 型で返していました。
    • 変更後: func runLog(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error)
      • logfile 引数が削除され、ログファイルへの書き込みの責任がなくなりました。
      • 終了ステータスを int から bool に変更しました。true は成功、false は失敗を示します。これは、Goのエラーハンドリングの慣習(エラーは error 型で返す)により適合しています。
      • 内部的には、新しく導入された runOutput を呼び出すように変更されました。runLog は、runOutput の結果を bytes.Buffer に書き込み、その内容を文字列として返します。
  2. runOutput 関数の新規導入:

    • func runOutput(timeout time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error)
    • この関数は、外部コマンドの標準出力と標準エラー出力を、引数として渡された io.Writer (out) に直接リダイレクトします。
    • runLog とは異なり、コマンドの出力を文字列として返す責任はありません。その代わりに、io.Writer を通じて出力の制御を呼び出し元に委ねます。
    • コマンドの実行が成功したかどうかを bool で返します。エラーが発生した場合は error を返します。
    • この関数が導入されたことで、呼び出し元は io.MultiWriter などを使用して、コマンドの出力を複数の場所に同時に送ることができるようになり、柔軟性が大幅に向上しました。

misc/dashboard/builder/main.go の変更

main.go では、主に Builder 型の buildHash メソッドと buildSubrepo メソッドで、runLog の呼び出しが新しい runOutput の利用に合わせて変更されています。

  1. buildHash メソッドの変更:

    • ビルドコマンドの実行部分で、runLog の代わりに runOutput が使用されるようになりました。
    • var buildlog bytes.Bufferf, err := os.Create(logfile) で、メモリバッファとログファイルの両方を作成します。
    • w := io.MultiWriter(f, &buildlog) を使用して、コマンドの出力をログファイルとメモリバッファの両方に同時に書き込む io.Writer を作成します。
    • ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd) のように runOutput を呼び出し、ビルドの成功/失敗を ok ブール値で受け取ります。
    • ビルドの終了時に、fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf()) を使って、ビルドの完了時間と結果(成功/失敗/エラー)をログファイルとメモリバッファの両方に追記しています。これにより、ビルドの最終ステータスがログに明示的に記録されるようになりました。
    • b.recordResult を呼び出す際に、buildlog.String() を渡すことで、メモリバッファに蓄積されたビルドログ全体をダッシュボードに記録できるようになりました。
  2. buildSubrepo メソッドの変更:

    • go get および go test コマンドの実行部分で、runLog の戻り値の status (int) が ok (bool) に変更されました。
    • if err == nil && status != 0 のような条件が if err == nil && !ok に変更され、ブール値の ok を直接評価するようになりました。これにより、コードがより簡潔になり、Goのエラーハンドリングの慣習に沿った形になりました。

これらの変更により、ビルドプロセスの出力制御がより柔軟になり、ビルドの成功/失敗ステータスをより明確に記録し、デバッグ情報をダッシュボードに送信する基盤が強化されました。

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

misc/dashboard/builder/exec.go

--- a/misc/dashboard/builder/exec.go
+++ b/misc/dashboard/builder/exec.go
@@ -29,42 +29,38 @@ func run(timeout time.Duration, envv []string, dir string, argv ...string) error
 	return waitWithTimeout(timeout, cmd)
 }
 
-// runLog runs a process and returns the combined stdout/stderr,
-// as well as writing it to logfile (if specified). It returns
-// 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(timeout time.Duration, envv []string, logfile, dir string, argv ...string) (string, int, error) {
-	if *verbose {
-		log.Println("runLog", argv)
-	}
+// runLog runs a process and returns the combined stdout/stderr. It returns
+// 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(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error) {
+	var b bytes.Buffer
+	ok, err := runOutput(timeout, envv, &b, dir, argv...)
+	return b.String(), ok, err
+}
 
-	b := new(bytes.Buffer)
-	var w io.Writer = b
-	if logfile != "" {
-		f, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
-		if err != nil {
-			return "", 0, err
-		}
-		defer f.Close()
-		w = io.MultiWriter(f, b)
-	}
+// runOutput runs a process and directs any output to the supplied writer.
+// It returns exit status and error. The error returned is nil, if process
+// is started successfully, even if exit status is not successful.
+func runOutput(timeout time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error) {
+	if *verbose {
+		log.Println("runOutput", argv)
+	}
 
 	cmd := exec.Command(argv[0], argv[1:]...)
 	cmd.Dir = dir
 	cmd.Env = envv
-	cmd.Stdout = w
-	cmd.Stderr = w
+	cmd.Stdout = out
+	cmd.Stderr = out
 
 	startErr := cmd.Start()
 	if startErr != nil {
-		return "", 1, startErr
+		return false, startErr
 	}
-	exitStatus := 0
 	if err := waitWithTimeout(timeout, cmd); err != nil {
-		exitStatus = 1 // TODO(bradfitz): this is fake. no callers care, so just return a bool instead.
+		return false, err
 	}
-	return b.String(), exitStatus, nil
+	return true, nil
 }
 
 func waitWithTimeout(timeout time.Duration, cmd *exec.Cmd) error

misc/dashboard/builder/main.go

--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -9,6 +9,7 @@ import (
 	"encoding/xml"
 	"flag"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"log"
 	"os"
@@ -272,21 +273,36 @@ func (b *Builder) buildHash(hash string) error {
 	srcDir := filepath.Join(workpath, "go", "src")
 
 	// build
+	var buildlog bytes.Buffer
 	logfile := filepath.Join(workpath, "build.log")
+	f, err := os.Create(logfile)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	w := io.MultiWriter(f, &buildlog)
+
 	cmd := *buildCmd
 	if !filepath.IsAbs(cmd) {
 		cmd = filepath.Join(srcDir, cmd)
 	}
 	startTime := time.Now()
-	buildLog, status, err := runLog(*buildTimeout, b.envv(), logfile, srcDir, cmd)
+	ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, cmd)
 	runTime := time.Now().Sub(startTime)
-	if err != nil {
-		return fmt.Errorf("%s: %s", *buildCmd, err)
+	errf := func() string {
+		if err != nil {
+			return fmt.Sprintf("error: %v", err)
+		}
+		if !ok {
+			return "failed"
+		}
+		return "success"
 	}
+	fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf())
 
-	if status != 0 {
+	if err != nil || !ok {
 		// record failure
-		return b.recordResult(false, "", hash, "", buildLog, runTime)
+		return b.recordResult(false, "", hash, "", buildlog.String(), runTime)
 	}
 
 	// record success
@@ -372,9 +388,9 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
 	}
 
 	// fetch package and dependencies
-	log, status, err := runLog(*cmdTimeout, env, "", goPath, goTool, "get", "-d", pkg+"/...")
-	if err == nil && status != 0 {
-		err = fmt.Errorf("go exited with status %d", status)
+	log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...")
+	if err == nil && !ok {
+		err = fmt.Errorf("go exited with status 1")
 	}
 	if err != nil {
 		return log, err
@@ -387,9 +403,9 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
 	}
 
 	// test the package
-	log, status, err = runLog(*buildTimeout, env, "", goPath, goTool, "test", "-short", pkg+"/...")
-	if err == nil && status != 0 {
-		err = fmt.Errorf("go exited with status %d", status)
+	log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...")
+	if err == nil && !ok {
+		err = fmt.Errorf("go exited with status 1")
 	}
 	return log, err
 }
@@ -571,7 +587,7 @@ func commitPoll(key, pkg string) {
 	const N = 50 // how many revisions to grab
 
 	lockGoroot()
-	data, _, err := runLog(*cmdTimeout, nil, "", pkgRoot, hgCmd("log",
+	data, _, err := runLog(*cmdTimeout, nil, pkgRoot, hgCmd("log",
 		"--encoding=utf-8",
 		"--limit="+strconv.Itoa(N),
 		"--template="+xmlLogTemplate)...,
@@ -663,7 +679,7 @@ func fullHash(root, rev string) (string, error) {
 	if root == goroot {
 		gorootMu.Lock()
 	}
-	s, _, err := runLog(*cmdTimeout, nil, "", root,
+	s, _, err := runLog(*cmdTimeout, nil, root,
 		hgCmd("log",
 			"--encoding=utf-8",
 			"--rev="+rev,

コアとなるコードの解説

このコミットの主要な変更点は、外部コマンドの実行と出力処理の抽象化と柔軟性の向上です。

  1. runLog から runOutput への責任の分離:

    • 以前の runLog は、ログファイルへの書き込み、メモリバッファへの書き込み、そしてコマンドの終了ステータス(int)の返却という複数の責任を持っていました。
    • 新しい runOutput 関数は、外部コマンドの出力を任意の io.Writer にリダイレクトするという単一の責任に特化しています。これにより、runOutput はより汎用的なユーティリティ関数となりました。
    • runLogrunOutput を内部的に呼び出すことで、その責任の一部(メモリバッファへの書き込みと文字列としての出力返却)を維持しつつ、ログファイルへの書き込みの責任は呼び出し元に委ねる形になりました。
  2. io.Writer を介した出力制御の柔軟性:

    • runOutputio.Writer を引数として受け取るようになったことで、呼び出し元はコマンドの出力を自由に制御できるようになりました。
    • main.gobuildHash メソッドでは、この柔軟性を活用して io.MultiWriter を使用しています。これにより、ビルドコマンドの出力が同時にログファイルとメモリバッファ(buildlog)の両方に書き込まれるようになりました。
    • この変更は、ビルドログをファイルに永続化しつつ、メモリ上のバッファにも保持することで、後でダッシュボードに送信する際にログ全体を簡単に取得できるようにするという、非常に実用的なメリットをもたらします。
  3. 終了ステータスの int から bool への変更:

    • runLog および runOutput の戻り値で、コマンドの成功/失敗を示すステータスが int から bool に変更されました。
    • Go言語では、エラーは通常 error 型で返されるため、コマンドの実行が成功したかどうかを bool で示すことは、よりGoらしい慣習に沿っています。これにより、呼び出し元のコード(特に main.gobuildHashbuildSubrepo)での条件分岐が if err != nil || !ok のように簡潔になり、可読性が向上しました。
  4. ビルド結果の明示的な記録:

    • buildHash メソッドの最後に、fmt.Fprintf(w, "Build complete, duration %v. Result: %v\\n", runTime, errf()) という行が追加されました。
    • これは、ビルドの完了時間と最終的な結果(成功、失敗、エラー)を、io.MultiWriter を通じてログファイルとメモリバッファの両方に明示的に書き込むものです。これにより、ログの末尾を見るだけでビルドの最終ステータスがすぐにわかるようになり、デバッグが容易になります。

これらの変更は、Goのビルドダッシュボードの信頼性とデバッグ能力を向上させるための重要なステップであり、特にビルダの失敗時に詳細な情報を効率的に収集・報告するための基盤を強化しています。

関連リンク

参考にした情報源リンク

  • この解説は、提供されたコミット情報とGo言語の標準ライブラリに関する一般的な知識に基づいて作成されました。外部のウェブ検索は特に使用していません。