[インデックス 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つの主要な変更が含まれています。
goroot
ディレクトリへのアクセス同期: Mercurial (Hg) がリポジトリのロックを適切に処理できない場合に発生する問題を回避するため、ビルダプロセス内でgoroot
ディレクトリへのアクセスを明示的に同期するメカニズムが導入されました。これにより、goroot
がビルダバイナリの自動更新など、他の目的でも安全に使用できるようになります。- 常時コミットポーリング: ビルダが常に新しいコミットをポーリングするように変更されました。以前は
-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
パッケージと、ビルダのメインループの変更に集約されます。
-
sync.Mutex
の導入:sync
パッケージからsync.Mutex
がインポートされています。gorootMu sync.Mutex
というグローバル変数が宣言され、goroot
ディレクトリへのアクセスを保護するためのミューテックスとして機能します。- コメントには「Mercurialが使用中にリポジトリをロックすると理論的には思われるが、実際にはうまく機能しない」と明記されており、このミューテックスの必要性が強調されています。
run
またはrunLog
関数(外部コマンドを実行する関数)を呼び出す際にのみ、このロックを保持するというルールが定められています。これは、外部コマンドがgoroot
ディレクトリを操作する際に競合が発生するのを防ぐためです。
-
goroot
アクセスの同期:hgClone
関数:url
がgoroot
と同じ場合、gorootMu.Lock()
とdefer gorootMu.Unlock()
を使用して、hg clone
コマンドの実行中にgoroot
への排他的アクセスを保証します。commitPoll
関数:pkgRoot
がgoroot
と同じ場合、hg pull
およびhg log
コマンドの実行前後にgorootMu
をロック/アンロックするロジックが追加されています。これにより、これらのMercurial操作がgoroot
に対して安全に実行されることが保証されます。fullHash
関数:root
がgoroot
と同じ場合、hg log
コマンドの実行前後にgorootMu
をロック/アンロックします。Builder.build
メソッド内でのhg pull
呼び出し:hg pull
の呼び出しもgorootMu.Lock()
とgorootMu.Unlock()
で囲まれています。
-
コミットポーリングの常時化:
- 以前は
commitFlag
というコマンドラインフラグがtrue
の場合にのみcommitWatcher()
が実行されていましたが、このフラグが削除されました。 main
関数内で、コマンドライン引数が指定されていない場合(つまり、ビルドターゲットが指定されていない場合)は、commitWatcher()
が直接呼び出され、プログラムがコミット監視のみを行うように変更されました。- ビルドターゲットが指定されている場合でも、
go commitWatcher()
としてゴルーチンとしてcommitWatcher()
が起動されるようになりました。これにより、ビルダは常にバックグラウンドで新しいコミットをポーリングし続けることができます。 commitWatcher
のスリープ間隔が60e9
(60秒) から*commitInterval
(デフォルト1分) に変更され、より柔軟な設定が可能になりました。
- 以前は
-
-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
の導入と、ビルダのコミット監視ロジックの変更です。
-
gorootMu sync.Mutex
:- これはGoの標準ライブラリ
sync
パッケージが提供するミューテックスです。ミューテックスは、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。 gorootMu.Lock()
を呼び出すと、ミューテックスがロックされ、他のゴルーチンが同じミューテックスをロックしようとすると、ロックが解除されるまでブロックされます。gorootMu.Unlock()
を呼び出すと、ミューテックスが解除され、ブロックされていた他のゴルーチンがロックを取得できるようになります。- このミューテックスは、
goroot
ディレクトリに対してMercurialコマンド(hg pull
,hg clone
,hg log
など)を実行する際に使用されます。これにより、複数の操作が同時にgoroot
を変更しようとすることによる競合状態やデータ破損を防ぎます。特に、Mercurial自身のロックメカニズムが信頼できないという背景があるため、アプリケーションレベルでの明示的な同期が重要になります。
- これはGoの標準ライブラリ
-
コミット監視の常時化:
- 以前のコードでは、コマンドライン引数に
-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
参考にした情報源リンク
- Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/7178046
はGerritの変更リストへのリンクです) - Mercurialのドキュメント (特にロックに関する情報): https://www.mercurial-scm.org/doc/
- Goの並行処理に関するドキュメントやチュートリアル (ミューテックスの利用例など): https://go.dev/doc/effective_go#concurrency
- 一般的な継続的インテグレーションの概念に関する情報源。