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

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

このコミットは、Goプロジェクトのmisc/dashboard/builderディレクトリにあるビルダツールの様々なクリーンアップと改善を目的としています。具体的には、コミットウォッチャーの無効化オプションの追加、ビルダが常にローカルのmasterワーキングコピーを更新する動作の導入、そしてMercurial (hg) リポジトリ操作を新しいRepo型にリファクタリングすることが含まれています。

コミット

commit 4036d876eab44f83d71e668e666acc5cd3997373
Author: Dave Cheney <dave@cheney.net>
Date:   Thu Feb 21 13:11:58 2013 +1100

    misc/dashboard/builder: various cleanups
    
    * allow commit watcher to be disabled, useful for small slow builders who will never be the first to notice a commit.
    * builders always update their local master working copy before cloning a specific revision.
    * refactor hg repo operations into a new type, Repo.
    
    R=adg, shanemhansen, luitvd
    CC=golang-dev
    https://golang.org/cl/7326053

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

https://github.com/golang/go/commit/4036d876eab44f83d71e668e666acc5cd3997373

元コミット内容

このコミットは、Goプロジェクトのビルドダッシュボードシステムの一部であるmisc/dashboard/builderコンポーネントに対する複数の改善を含んでいます。主な変更点は以下の通りです。

  1. コミットウォッチャーの無効化機能: コミットウォッチャー(新しいコミットをポーリングする機能)を無効にできるようにしました。これは、処理能力が低い「遅いビルダ」にとって特に有用です。これらのビルダは、新しいコミットを最初に検出する役割を担うことがないため、ポーリングのオーバーヘッドを避けることができます。
  2. ビルダの動作改善: ビルダが特定のリビジョンをクローンする前に、常にローカルのmasterワーキングコピーを更新するように変更されました。これにより、ビルドの信頼性と一貫性が向上します。
  3. Mercurialリポジトリ操作のリファクタリング: Mercurial (hg) リポジトリに関連する操作(クローン、更新、プル、ログ取得、ハッシュ取得など)が、新しく導入されたRepo型に集約され、コードのモジュール化と再利用性が向上しました。

変更の背景

Goプロジェクトは、継続的インテグレーション(CI)システムとして、様々なプラットフォームや環境でコードのビルドとテストを行うためのダッシュボードとビルダ群を運用していました。このシステムは、新しいコミットがリポジトリにプッシュされるたびにそれを検出し、対応するビルドジョブをトリガーする仕組みを持っていました。

しかし、以下のような課題がありました。

  • リソースの非効率性: すべてのビルダが新しいコミットをポーリングすることは、特にリソースが限られている、またはネットワークが遅い環境で動作するビルダ(「small slow builders」)にとって非効率的でした。これらのビルダは、他の高速なビルダが既にコミットを検出して処理を開始しているため、自身が最初にコミットを検出する必要がありませんでした。
  • Mercurial操作の分散: Mercurialリポジトリに対する操作がmain.goファイル内に散在しており、コードの重複や保守性の低下を招いていました。リポジトリ操作のロジックが複数の関数にまたがって存在するため、変更やデバッグが困難でした。
  • ビルドの一貫性: ビルダが常に最新のmasterブランチの状態から作業を開始することを保証するメカニズムが不足しており、特定のコミットをビルドする際に、そのコミットがローカルリポジトリに存在しない場合に問題が発生する可能性がありました。

これらの課題に対処するため、本コミットでは、システムの効率性、堅牢性、およびコードの保守性を向上させるための改善が導入されました。

前提知識の解説

  • Go言語: Googleによって開発されたオープンソースのプログラミング言語。並行処理に強く、高速なコンパイルと実行が特徴です。
  • Mercurial (hg): 分散型バージョン管理システム(DVCS)の一つ。Gitと同様に、リポジトリのクローン、コミット、プッシュ、プルなどの操作をサポートします。Goプロジェクトは、初期にはMercurialを主要なバージョン管理システムとして使用していましたが、後にGitに移行しました。このコミットが作成された2013年時点では、まだMercurialが使用されていました。
  • 継続的インテグレーション (CI): ソフトウェア開発手法の一つで、開発者がコードベースに加えた変更を頻繁にメインブランチにマージし、自動化されたビルドとテストを実行することで、早期に問題を検出します。
  • ビルドダッシュボード: CIシステムの一部として、ビルドのステータス、テスト結果、コミット履歴などを視覚的に表示するウェブインターフェース。Goプロジェクトのダッシュボードは、様々なビルダからの結果を集約して表示していました。
  • コミットウォッチャー: バージョン管理システムのリポジトリを定期的にポーリングし、新しいコミットがプッシュされたことを検出するコンポーネント。検出されたコミットは、ビルドジョブのトリガーとなります。
  • go.googlecode.com/p/go: GoプロジェクトのMercurialリポジトリのURL。このコミットが作成された時点では、Google Codeでホストされていました。
  • hg clone: Mercurialコマンドで、リモートリポジトリのコピーをローカルに作成します。
  • hg pull: Mercurialコマンドで、リモートリポジトリから変更をローカルリポジトリに取得します。
  • hg update: Mercurialコマンドで、ワーキングディレクトリを特定のリビジョンに更新します。
  • hg log: Mercurialコマンドで、リポジトリのコミット履歴を表示します。

技術的詳細

このコミットの技術的詳細は、主にGo言語で書かれたmisc/dashboard/builderアプリケーションの内部構造と、Mercurialリポジトリとのインタラクションの改善に焦点を当てています。

  1. Repo型の導入:

    • vcs.goという新しいファイルが作成され、Repoという構造体が定義されました。この構造体は、Mercurialリポジトリのパスと、並行アクセスを制御するためのsync.Mutexを含んでいます。
    • Repo型には、Clone, UpdateTo, Exists, Pull, Log, FullHash, hgCmdといったメソッドが追加されました。これにより、Mercurialリポジトリに対するすべての操作がこの型にカプセル化され、main.goからMercurialコマンドを直接呼び出すロジックが大幅に削減されました。
    • 特に、Logメソッドは内部でPullを呼び出すことで、ログを取得する前にリポジトリが最新の状態であることを保証しています。
    • hgCmdメソッドは、Mercurialコマンドの引数を構築し、extensions.codereview拡張を無効にするための--configオプションを自動的に追加します。
  2. コミットウォッチャーの制御:

    • main.gocommitIntervalフラグが変更され、0を設定することでコミットウォッチャーを無効にできるようになりました。
    • commitWatcher関数は、Repo型のインスタンスを引数として受け取るように変更され、gorootリポジトリの操作がRepoメソッドを通じて行われるようになりました。
    • commitWatcherの開始ロジックが修正され、commitInterval0の場合はウォッチャーが起動しないようになりました。
  3. ビルダの動作ロジックの変更:

    • Builder構造体にgoroot *Repoフィールドが追加され、各ビルダが自身のgorootリポジトリインスタンスを持つようになりました。
    • NewBuilder関数もgoroot *Repoを引数として受け取るように変更されました。
    • buildメソッド内で、特定のハッシュをビルドする前にb.goroot.Pull()が呼び出されるようになりました。これにより、ビルド対象のハッシュがローカルリポジトリに存在しない場合に、まずリモートから変更を取得する動作が保証されます。
    • リポジトリのクローン操作もb.goroot.Clone()メソッドを使用するように変更され、より堅牢になりました。
  4. 並行処理の改善:

    • 以前はgorootMu sync.Mutexというグローバルなミューテックスを使用していましたが、Repo型にミューテックスが内包されたことで、リポジトリごとのロックが可能になり、より粒度の細かい並行処理制御が実現されました。これにより、異なるリポジトリに対する操作が互いにブロックし合うことが少なくなります。
  5. XML解析の変更:

    • main.goからencoding/xmlパッケージのインポートが削除されました。これは、HgLog構造体とxmlLogTemplatevcs.goに移動し、Repo.Log()メソッド内でXMLのアンマーシャリングが行われるようになったためです。

これらの変更により、コードの構造が整理され、Mercurialリポジトリ操作のロジックが集中管理されることで、保守性と拡張性が向上しました。また、ビルダの動作がより予測可能になり、リソースの効率的な利用が可能になりました。

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

このコミットにおける主要なコード変更は以下の2つのファイルに集中しています。

  1. misc/dashboard/builder/main.go:

    • goroot変数が*Repo型に変更され、Repo構造体のインスタンスとして初期化されるようになりました。
    • NewBuilder関数のシグネチャが変更され、*Repo型のgoroot引数を受け取るようになりました。
    • commitWatcher関数も*Repo型のgoroot引数を受け取るように変更されました。
    • Mercurial操作(hgClone, hgRepoExists, fullHashなど)を直接呼び出していた箇所が、新しく導入されたRepo型のメソッド呼び出しに置き換えられました。
    • sync.MutexであるgorootMuが削除されました。
    • encoding/xmlパッケージのインポートが削除されました。
  2. misc/dashboard/builder/vcs.go (新規ファイル):

    • Repo構造体が定義されました。
    • RemoteRepo, Clone, UpdateTo, Exists, Pull, Log, FullHash, hgCmdといったRepo型のメソッドが実装されました。
    • HgLog構造体とxmlLogTemplate定数がmain.goからこのファイルに移動されました。

コアとなるコードの解説

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

main.goでは、Mercurialリポジトリ操作の責任がRepo型に委譲されたことが最も大きな変更点です。

// 変更前: グローバルなgoroot文字列とミューテックス
// var (
// 	goroot   string
// 	gorootMu sync.Mutex
// )

// 変更後: main関数内でRepo型として初期化
func main() {
	// ...
	goroot := &Repo{
		Path: filepath.Join(*buildroot, "goroot"),
	}
	// ...
}

// 変更前: hgRepoExists(goroot)
// 変更後: goroot.Exists()
// if hgRepoExists(goroot) {
if goroot.Exists() {
	log.Print("Found old workspace, will use it")
} else {
	// ...
	// 変更前: if err := hgClone(hgUrl, goroot); err != nil {
	// 変更後: goroot, err = RemoteRepo(hgUrl).Clone(goroot.Path, "tip")
	var err error
	goroot, err = RemoteRepo(hgUrl).Clone(goroot.Path, "tip")
	if err != nil {
		log.Fatal("Error cloning repository:", err)
	}
}

// Builderの初期化も変更
// 変更前: b, err := NewBuilder(builder)
// 変更後: b, err := NewBuilder(goroot, name)
func NewBuilder(goroot *Repo, name string) (*Builder, error) {
	b := &Builder{
		goroot: goroot,
		name:   name,
	}
	// ...
}

// buildメソッド内の変更
func (b *Builder) build() bool {
	// ...
	// 変更前: if _, err := fullHash(goroot, hash[:12]); err != nil { ... hg pull ... }
	// 変更後: b.goroot.Pull() が buildHash の中で呼ばれる
	// ...
	// 変更前: if err := hgClone(goroot, filepath.Join(workpath, "go")); err != nil {
	// 変更後: if err := b.goroot.Pull(); err != nil { // pull before cloning to ensure we have the revision
	if err := b.goroot.Pull(); err != nil {
		return err
	}
	// 変更前: if err := run(*cmdTimeout, nil, filepath.Join(workpath, "go"), hgCmd("update", hash)...); err != nil {
	// 変更後: if _, err := b.goroot.Clone(filepath.Join(workpath, "go"), hash); err != nil {
	if _, err := b.goroot.Clone(filepath.Join(workpath, "go"), hash); err != nil {
		return err
	}
	// ...
}

// commitWatcher関数の変更
// 変更前: func commitWatcher() {
// 変更後: func commitWatcher(goroot *Repo) {
func commitWatcher(goroot *Repo) {
	if *commitInterval == 0 {
		log.Printf("commitInterval is %s, disabling commitWatcher", *commitInterval)
		return
	}
	// ...
	// 変更前: commitPoll(key, "")
	// 変更後: commitPoll(goroot, "", key)
	commitPoll(goroot, "", key)
	// ...
	// 変更前: commitPoll(key, pkg)
	// 変更後: commitPoll(pkgroot, pkg, key)
	for _, pkg := range dashboardPackages("subrepo") {
		pkgroot := &Repo{
			Path: filepath.Join(*buildroot, pkg),
		}
		commitPoll(pkgroot, pkg, key)
	}
	// ...
}

// commitPoll関数の変更
// 変更前: func commitPoll(key, pkg string) {
// 変更後: func commitPoll(repo *Repo, pkg, key string) {
func commitPoll(repo *Repo, pkg, key string) {
	// ...
	// 変更前: if !hgRepoExists(pkgRoot) { ... hgClone ... }
	// 変更後: if !repo.Exists() { ... RemoteRepo(...).Clone(...) ... }
	if !repo.Exists() {
		var err error
		repo, err = RemoteRepo(repoURL(pkg)).Clone(repo.Path, "tip")
		// ...
	}
	// ...
	// 変更前: err := run(*cmdTimeout, nil, pkgRoot, hgCmd("pull")...)
	// 変更後: logs, err := repo.Log() // repo.Log calls repo.Pull internally
	logs, err := repo.Log() // repo.Log calls repo.Pull internally
	if err != nil {
		log.Printf("hg log: %v", err) // エラーメッセージもhg logに変わっている
		return
	}
	// ...
	// 変更前: l.Parent, _ = fullHash(pkgRoot, l.Parent)
	// 変更後: l.Parent, _ = repo.FullHash(l.Parent)
	l.Parent, _ = repo.FullHash(l.Parent)
	// ...
}

misc/dashboard/builder/vcs.go の新規追加と解説

このファイルは、Mercurialリポジトリ操作をカプセル化するためのRepo型とそのメソッドを定義しています。

// Repo represents a mercurial repository.
type Repo struct {
	Path string      // リポジトリのローカルパスまたはリモートURL
	sync.Mutex      // 並行アクセス制御のためのミューテックス
}

// RemoteRepo constructs a *Repo representing a remote repository.
func RemoteRepo(url string) *Repo {
	return &Repo{
		Path: url,
	}
}

// Clone clones the current Repo to a new destination
// returning a new *Repo if successful.
// 指定されたパスとリビジョンでリポジトリをクローンし、新しいRepoインスタンスを返す。
func (r *Repo) Clone(path, rev string) (*Repo, error) {
	r.Lock()
	defer r.Unlock()
	// hgCmdはRepoのメソッドとして定義されており、適切な引数を生成する
	if err := run(*cmdTimeout, nil, *buildroot, r.hgCmd("clone", "-r", rev, r.Path, path)...); err != nil {
		return nil, err
	}
	return &Repo{
		Path: path,
	}, nil
}

// UpdateTo updates the working copy of this Repo to the
// supplied revision.
// ワーキングコピーを指定されたハッシュに更新する。
func (r *Repo) UpdateTo(hash string) error {
	r.Lock()
	defer r.Unlock()
	return run(*cmdTimeout, nil, r.Path, r.hgCmd("update", hash)...)
}

// Exists reports whether this Repo represents a valid Mecurial repository.
// .hgディレクトリの存在を確認し、有効なMercurialリポジトリであるかを報告する。
func (r *Repo) Exists() bool {
	fi, err := os.Stat(filepath.Join(r.Path, ".hg"))
	if err != nil {
		return false
	}
	return fi.IsDir()
}

// Pull pulls changes from the default path, that is, the path
// this Repo was cloned from.
// リモートから変更をプルする。
func (r *Repo) Pull() error {
	r.Lock()
	defer r.Unlock()
	return run(*cmdTimeout, nil, r.Path, r.hgCmd("pull")...)
}

// Log returns the changelog for this repository.
// リポジトリの変更ログ(コミット履歴)を取得する。
// 内部でPull()を呼び出し、最新の状態を保証する。
func (r *Repo) Log() ([]HgLog, error) {
	if err := r.Pull(); err != nil {
		return nil, err
	}
	const N = 50 // how many revisions to grab

	r.Lock()
	defer r.Unlock()
	data, _, err := runLog(*cmdTimeout, nil, r.Path, r.hgCmd("log",
		"--encoding=utf-8",
		"--limit="+strconv.Itoa(N),
		"--template="+xmlLogTemplate)...,
	)
	if err != nil {
		return nil, err
	}

	var logStruct struct {
		Log []HgLog
	}
	err = xml.Unmarshal([]byte("<Top>"+data+"</Top>"), &logStruct)
	if err != nil {
		log.Printf("unmarshal hg log: %v", err)
		return nil, err
	}
	return logStruct.Log, nil
}

// FullHash returns the full hash for the given Mercurial revision.
// 指定されたリビジョンの完全なハッシュを取得する。
func (r *Repo) FullHash(rev string) (string, error) {
	r.Lock()
	defer r.Unlock()
	s, _, err := runLog(*cmdTimeout, nil, r.Path,
		r.hgCmd("log",
			"--encoding=utf-8",
			"--rev="+rev,
			"--limit=1",
			"--template={node}")...,
	)
	if err != nil {
		return "", nil
	}
	s = strings.TrimSpace(s)
	if s == "" {
		return "", fmt.Errorf("cannot find revision")
	}
	if len(s) != 40 {
		return "", fmt.Errorf("hg returned invalid hash " + s)
	}
	return s, nil
}

// hgCmd constructs the command arguments for Mercurial.
// Mercurialコマンドの引数を構築し、codereview拡張を無効にするオプションを追加する。
func (r *Repo) hgCmd(args ...string) []string {
	return append([]string{"hg", "--config", "extensions.codereview=!"}, args...)
}

// HgLog represents a single Mercurial revision.
// Mercurialのログエントリを表す構造体。
type HgLog struct {
	Hash   string
	Author string
	Date   string
	Desc   string
	Parent string

	// Internal metadata
	added bool
}

// xmlLogTemplate is a template to pass to Mercurial to make
// hg log print the log in valid XML for parsing with xml.Unmarshal.
// hg logコマンドでXML形式の出力を得るためのテンプレート。
const xmlLogTemplate = `
        <Log>
        <Hash>{node|escape}</Hash>
        <Parent>{parent|escape}</Parent>
        <Author>{author|escape}</Author>
        <Date>{date|rfc3339date}</Date>
        <Desc>{desc|escape}</Desc>
        </Log>
`

これらの変更により、MercurialリポジトリとのインタラクションがRepo型に集約され、main.goのコードが大幅に簡素化され、より読みやすく、保守しやすくなりました。また、Repo型内のミューテックスにより、リポジトリ操作の並行処理が安全に管理されるようになりました。

関連リンク

参考にした情報源リンク