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

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

このコミットは、Goプロジェクトのダッシュボードビルダ (misc/dashboard/builder/main.go) における重要な変更を扱っています。主な目的は、goroot ディレクトリへのアクセスを同期させ、Mercurial (Hg) のロックに関する問題を回避することです。また、ビルダが常に新しいコミットをポーリングするように変更されています。

コミット

commit df21f06fdfa393747634d378a5a4344d0a7b54c3
Author: Andrew Gerrand <adg@golang.org>
Date:   Fri Jan 25 10:06:18 2013 +1100

    misc/dashboard/builder: synchronize accesses to goroot, always -commit
    
    This prevents the occasional issue when Mercurial screws up the locking
    itself, and by moving the locking into this process we can use the
    goroot for other things (such as automatically updating the builder
    binary).
    
    It also asks all builders to poll for new commits.
    
    R=bradfitz, dave, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7178046

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

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

元コミット内容

このコミットは、Goプロジェクトのビルドダッシュボードシステムの一部であるビルダプログラム (misc/dashboard/builder) の動作を改善します。具体的には、以下の2つの主要な変更が含まれています。

  1. goroot ディレクトリへのアクセス同期: Mercurial (Hg) がリポジトリのロックを適切に処理できない場合に発生する問題を回避するため、ビルダプロセス内で goroot ディレクトリへのアクセスを明示的に同期するメカニズムが導入されました。これにより、goroot がビルダバイナリの自動更新など、他の目的でも安全に使用できるようになります。
  2. 常時コミットポーリング: ビルダが常に新しいコミットをポーリングするように変更されました。以前は -commit フラグが指定された場合にのみコミットの監視が行われていましたが、この変更により、ビルダは常に最新のコミットを追跡し、ビルドプロセスを開始できるようになります。

変更の背景

この変更の背景には、Goプロジェクトの継続的インテグレーション (CI) システムにおける安定性と効率性の向上が挙げられます。

  • Mercurialのロック問題: Goプロジェクトは当時、バージョン管理システムとしてMercurial (Hg) を使用していました。Mercurialはリポジトリへのアクセスを同期するために内部的なロックメカニズムを持っていますが、コミットメッセージによると、このロックが「時折台無しになる」問題が発生していたようです。これにより、複数のプロセスが同時に goroot ディレクトリにアクセスしようとした際に競合状態が発生し、ビルドの失敗や予期せぬ動作を引き起こす可能性がありました。特に、ビルダが goroot を更新している最中に別の操作が行われると、データの破損や不整合が生じるリスクがありました。
  • ビルダの柔軟性向上: goroot ディレクトリへのアクセスをビルダプロセス内で同期することで、Mercurialの外部ロックに依存することなく、goroot をより柔軟に利用できるようになります。例えば、ビルダ自身のバイナリを goroot 内に配置し、新しいコミットがプッシュされた際に自動的に更新するといった運用が可能になります。これは、ビルダのメンテナンスとデプロイメントを簡素化する上で重要です。
  • 継続的インテグレーションの効率化: ビルダが常に新しいコミットをポーリングするように変更されたことで、ビルドのトリガーがより迅速かつ確実になります。以前のように特定のフラグに依存するのではなく、常に最新のコードベースでビルドが実行されることで、開発サイクルが加速し、問題の早期発見に繋がります。

これらの問題に対処し、GoプロジェクトのCIインフラストラクチャの堅牢性を高めることが、このコミットの主要な動機となっています。

前提知識の解説

このコミットを理解するためには、以下の前提知識が役立ちます。

  • Go言語: GoはGoogleによって開発された静的型付けのコンパイル型言語です。並行処理に強く、シンプルで効率的なプログラミングを目的としています。このコミットはGo言語で書かれたビルダプログラムの改善に関するものです。
  • Mercurial (Hg): Mercurialは分散型バージョン管理システム (DVCS) の一つです。Gitと同様に、コードの変更履歴を管理し、複数の開発者間での共同作業を可能にします。Goプロジェクトは歴史的にMercurialを使用していましたが、後にGitに移行しました。このコミットの時点ではMercurialが使用されており、そのロックメカニズムに関する問題が焦点となっています。
  • 継続的インテグレーション (CI): CIは、開発者がコードの変更を共有リポジトリに頻繁に統合するソフトウェア開発プラクティスです。各統合は自動化されたビルドとテストによって検証され、早期に統合の問題を検出します。Goプロジェクトのダッシュボードビルダは、このCIプロセスの一部として機能し、新しいコミットがプッシュされるたびにGoのソースコードをビルドし、テストを実行します。
  • goroot: goroot はGoのインストールディレクトリを指す環境変数です。Goの標準ライブラリ、ツール、およびソースコードがこのディレクトリに格納されています。ビルダプログラムは、この goroot ディレクトリ内のGoソースコードを操作してビルドを実行します。
  • 並行処理と同期: 複数のプロセスやゴルーチン (Goにおける軽量スレッド) が共有リソース (この場合は goroot ディレクトリ) に同時にアクセスしようとすると、競合状態が発生する可能性があります。これを防ぐためには、ミューテックス (Mutex) などの同期プリミティブを使用して、一度に一つのプロセスのみがリソースにアクセスできるように制御する必要があります。

技術的詳細

このコミットの技術的詳細は、主にGoの sync パッケージと、ビルダのメインループの変更に集約されます。

  1. sync.Mutex の導入:

    • sync パッケージから sync.Mutex がインポートされています。
    • gorootMu sync.Mutex というグローバル変数が宣言され、goroot ディレクトリへのアクセスを保護するためのミューテックスとして機能します。
    • コメントには「Mercurialが使用中にリポジトリをロックすると理論的には思われるが、実際にはうまく機能しない」と明記されており、このミューテックスの必要性が強調されています。
    • run または runLog 関数(外部コマンドを実行する関数)を呼び出す際にのみ、このロックを保持するというルールが定められています。これは、外部コマンドが goroot ディレクトリを操作する際に競合が発生するのを防ぐためです。
  2. goroot アクセスの同期:

    • hgClone 関数: urlgoroot と同じ場合、gorootMu.Lock()defer gorootMu.Unlock() を使用して、hg clone コマンドの実行中に goroot への排他的アクセスを保証します。
    • commitPoll 関数: pkgRootgoroot と同じ場合、hg pull および hg log コマンドの実行前後に gorootMu をロック/アンロックするロジックが追加されています。これにより、これらのMercurial操作が goroot に対して安全に実行されることが保証されます。
    • fullHash 関数: rootgoroot と同じ場合、hg log コマンドの実行前後に gorootMu をロック/アンロックします。
    • Builder.build メソッド内での hg pull 呼び出し: hg pull の呼び出しも gorootMu.Lock()gorootMu.Unlock() で囲まれています。
  3. コミットポーリングの常時化:

    • 以前は commitFlag というコマンドラインフラグが true の場合にのみ commitWatcher() が実行されていましたが、このフラグが削除されました。
    • main 関数内で、コマンドライン引数が指定されていない場合(つまり、ビルドターゲットが指定されていない場合)は、commitWatcher() が直接呼び出され、プログラムがコミット監視のみを行うように変更されました。
    • ビルドターゲットが指定されている場合でも、go commitWatcher() としてゴルーチンとして commitWatcher() が起動されるようになりました。これにより、ビルダは常にバックグラウンドで新しいコミットをポーリングし続けることができます。
    • commitWatcher のスリープ間隔が 60e9 (60秒) から *commitInterval (デフォルト1分) に変更され、より柔軟な設定が可能になりました。
  4. -commit フラグの削除と commitInterval フラグの追加:

    • commitFlag (-commit) が削除され、代わりに commitInterval (-commitInterval) という新しいフラグが追加されました。これは、コミットポーリングの頻度を設定するためのものです。これにより、ポーリング動作がより制御可能になります。

これらの変更により、goroot ディレクトリへのアクセスがより堅牢になり、Mercurialの潜在的なロック問題を回避しつつ、ビルダが常に最新のコミットを追跡するようになりました。

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

変更は misc/dashboard/builder/main.go ファイルに集中しています。

  • sync パッケージのインポート:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -17,6 +17,7 @@ import (
      	"runtime"
      	"strconv"
      	"strings"
    +	"sync"
      	"time"
      )
    
  • gorootMu ミューテックスの宣言:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -47,21 +48,29 @@ type Builder struct {
     }
     
     var (
    -	buildroot     = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build")
    -	commitFlag    = flag.Bool("commit", false, "upload information about new commits")
    -	dashboard     = flag.String("dashboard", "build.golang.org", "Go Dashboard Host")
    -	buildRelease  = flag.Bool("release", false, "Build and upload binary release archives")
    -	buildRevision = flag.String("rev", "", "Build specified revision and exit")
    -	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", 5*time.Minute, "Maximum time to wait for an external command")
    -	verbose       = flag.Bool("v", false, "verbose")
    +	buildroot      = flag.String("buildroot", defaultBuildRoot(), "Directory under which to build")
    +	dashboard      = flag.String("dashboard", "build.golang.org", "Go Dashboard Host")
    +	buildRelease   = flag.Bool("release", false, "Build and upload binary release archives")
    +	buildRevision  = flag.String("rev", "", "Build specified revision and exit")
    +	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", 5*time.Minute, "Maximum time to wait for an external command")
    +	commitInterval = flag.Duration("commitInterval", 1*time.Minute, "Time to wait between polling for new commits")
    +	verbose        = flag.Bool("v", false, "verbose")
    +	)
    +
    +// Use a mutex to prevent the commit poller and builders from using the primary
    +// local goroot simultaneously. Theoretically, Mercurial locks the repo when
    +// it's in use. Practically, it does a bad job of this.
    +// As a rule, only hold this lock while calling run or runLog.
    +var (
    +	goroot   string
    +	gorootMu sync.Mutex
     )
     
     var (
    -	goroot      string
      	binaryTagRe = regexp.MustCompile(`^(release\.r|weekly\.)[0-9\-.]+`)
      	releaseRe   = regexp.MustCompile(`^release\.r[0-9\-.]+`)
      	allCmd      = "all" + suffix
    
  • main 関数におけるコミット監視ロジックの変更:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -76,7 +85,7 @@ func main() {
      	\tos.Exit(2)
      	}
      	flag.Parse()
    -	if len(flag.Args()) == 0 && !*commitFlag {
    +	if len(flag.Args()) == 0 {
      		flag.Usage()
      	}
      	goroot = filepath.Join(*buildroot, "goroot")
    @@ -109,14 +118,6 @@ func main() {
      	}
      }
     
    -	if *commitFlag {
    -		if len(flag.Args()) == 0 {
    -			commitWatcher()
    -			return
    -		}
    -		go commitWatcher()
    -	}
    -
      	// if specified, build revision and return
      	if *buildRevision != "" {
      		hash, err := fullHash(goroot, *buildRevision)
    @@ -131,6 +132,14 @@ func main() {
      		return
      	}
     
    +	// Start commit watcher, and exit if that's all we're doing.
    +	if len(flag.Args()) == 0 {
    +		log.Print("no build targets specified; watching commits only")
    +		commitWatcher()
    +		return
    +	}
    +	go commitWatcher()
    +
      	// go continuous build mode (default)
      	// check for new commits and build them
      	for {
    
  • hg pull 呼び出しの同期:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -220,14 +229,19 @@ func (b *Builder) build() bool {
      	if hash == "" {
      		return false
      	}
    +
      	// 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(*cmdTimeout, nil, goroot, hgCmd("pull")...); err != nil {
    +		gorootMu.Lock()
    +		err = run(*cmdTimeout, nil, goroot, hgCmd("pull")...)
    +		gorootMu.Unlock()
    +		if err != nil {
      			log.Println("hg pull failed:", err)
      			return false
      		}
      	}
    +
      	err = b.buildHash(hash)
      	if err != nil {
      		log.Println(err)
    
  • hgClone 関数の同期:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -469,11 +483,15 @@ func commitWatcher() {
      	\tif *verbose {
      	\t\tlog.Printf("sleep...")
      	\t}
    -	\ttime.Sleep(60e9)
    +	\ttime.Sleep(*commitInterval)
      	}
      }
     
      func hgClone(url, path string) error {
    +	if url == goroot {
    +		gorootMu.Lock()
    +		defer gorootMu.Unlock()
    +	}
      	return run(*cmdTimeout, nil, *buildroot, hgCmd("clone", url, path)...)
      }
     
    
  • commitPoll 関数の同期:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -531,18 +549,34 @@ func commitPoll(key, pkg string) {
      	\t}
      	}
     
    -	if err := run(*cmdTimeout, nil, pkgRoot, hgCmd("pull")...); err != nil {
    +	lockGoroot := func() {
    +		if pkgRoot == goroot {
    +			gorootMu.Lock()
    +		}
    +	}
    +	unlockGoroot := func() {
    +		if pkgRoot == goroot {
    +			gorootMu.Unlock()
    +		}
    +	}
    +
    +	lockGoroot()
    +	err := run(*cmdTimeout, nil, pkgRoot, hgCmd("pull")...)
    +	unlockGoroot()
    +	if err != nil {
      		log.Printf("hg pull: %v", err)
      		return
      	}
     
      	const N = 50 // how many revisions to grab
     
    +	lockGoroot()
      	data, _, err := runLog(*cmdTimeout, nil, "", pkgRoot, hgCmd("log",
      	\t"--encoding=utf-8",
      	\t"--limit="+strconv.Itoa(N),
      	\t"--template="+xmlLogTemplate)...,
      	)
    +	unlockGoroot()
      	if err != nil {
      		log.Printf("hg log: %v", err)
      		return
    
  • fullHash 関数の同期:

    --- a/misc/dashboard/builder/main.go
    +++ b/misc/dashboard/builder/main.go
    @@ -626,6 +660,9 @@ func addCommit(pkg, hash, key string) bool {
     
      // fullHash returns the full hash for the given Mercurial revision.
      func fullHash(root, rev string) (string, error) {
    +	if root == goroot {
    +		gorootMu.Lock()
    +	}
      	s, _, err := runLog(*cmdTimeout, nil, "", root,
      	\thgCmd("log",
      	\t\t"--encoding=utf-8",
    @@ -633,6 +670,9 @@ func fullHash(root, rev string) (string, error) {
      	\t\t"--limit=1",
      	\t\t"--template={node}")...,
      	)
    +	if root == goroot {
    +		gorootMu.Unlock()
    +	}
      	if err != nil {
      		return "", nil
      	}
    

コアとなるコードの解説

このコミットの核心は、goroot ディレクトリへのアクセスを保護するための sync.Mutex の導入と、ビルダのコミット監視ロジックの変更です。

  1. gorootMu sync.Mutex:

    • これはGoの標準ライブラリ sync パッケージが提供するミューテックスです。ミューテックスは、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。
    • gorootMu.Lock() を呼び出すと、ミューテックスがロックされ、他のゴルーチンが同じミューテックスをロックしようとすると、ロックが解除されるまでブロックされます。
    • gorootMu.Unlock() を呼び出すと、ミューテックスが解除され、ブロックされていた他のゴルーチンがロックを取得できるようになります。
    • このミューテックスは、goroot ディレクトリに対してMercurialコマンド(hg pull, hg clone, hg log など)を実行する際に使用されます。これにより、複数の操作が同時に goroot を変更しようとすることによる競合状態やデータ破損を防ぎます。特に、Mercurial自身のロックメカニズムが信頼できないという背景があるため、アプリケーションレベルでの明示的な同期が重要になります。
  2. コミット監視の常時化:

    • 以前のコードでは、コマンドライン引数に -commit フラグが指定された場合にのみ、commitWatcher() 関数が実行され、新しいコミットのポーリングが行われていました。
    • このコミットでは、-commit フラグが削除され、main 関数内のロジックが変更されました。
    • もしビルダがコマンドライン引数なしで起動された場合(つまり、特定のビルドターゲットが指定されていない場合)、ビルダは「ビルドターゲットが指定されていないため、コミット監視のみを行います」というメッセージを出力し、commitWatcher() を直接呼び出してコミット監視モードに入ります。
    • もしビルダがコマンドライン引数付きで起動された場合(つまり、特定のビルドターゲットをビルドする場合)、go commitWatcher() を使用して commitWatcher() が別のゴルーチンとして起動されます。これにより、ビルダは指定されたターゲットのビルドを実行しながら、バックグラウンドで常に新しいコミットをポーリングし続けることができます。
    • この変更により、ビルダは常に最新のコミットを認識し、必要に応じてビルドを開始できるようになり、CIプロセスの応答性と効率が向上します。

これらの変更は、Goのビルドダッシュボードシステムの堅牢性と自律性を高める上で重要な役割を果たしています。

関連リンク

  • Go言語公式ウェブサイト: https://go.dev/
  • Mercurial公式ウェブサイト: https://www.mercurial-scm.org/
  • Go Dashboard (当時のビルドダッシュボード): 現在はGoのCIシステムはGerritとGitHub Actionsに移行していますが、当時のビルドダッシュボードの概念を理解する上で参考になるかもしれません。
  • Goの sync パッケージドキュメント: https://pkg.go.dev/sync

参考にした情報源リンク