[インデックス 10485] ファイルの概要
このコミットは、Go言語プロジェクトのダッシュボードアプリケーションにおける新しいデータ構造の設計と実装を導入するものです。具体的には、Google App Engine (GAE) のDatastoreを利用して、Goパッケージ、コミット、ビルド結果、ログ、タグといった情報を効率的に管理するためのGo言語の構造体と、それらを操作するためのHTTPハンドラが定義されています。これにより、Goプロジェクトの継続的インテグレーション(CI)システムが生成するビルド結果やコミット情報を、より堅牢かつスケーラブルに保存・取得できるようになります。
コミット
dashboard: new Go dashboard data structure design
R=rsc, r, dsymonds, bradfitz CC=golang-dev https://golang.org/cl/5416056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/79bce499a336471638f5aa02bd258e516019ad6f
元コミット内容
commit 79bce499a336471638f5aa02bd258e516019ad6f
Author: Andrew Gerrand <adg@golang.org>
Date: Wed Nov 23 08:13:05 2011 +1100
dashboard: new Go dashboard data structure design
R=rsc, r, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/5416056
---
misc/dashboard/app/app.yaml | 8 +++
misc/dashboard/app/build/build.go | 133 ++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
diff --git a/misc/dashboard/app/app.yaml b/misc/dashboard/app/app.yaml
new file mode 100644
index 0000000000..695c04e78a
--- /dev/null
+++ b/misc/dashboard/app/app.yaml
@@ -0,0 +1,8 @@
+application: godashboard
+version: go
+runtime: go
+api_version: 3
+
+handlers:
+- url: /(commit|tag|todo|result)
+ script: _go_app
diff --git a/misc/dashboard/app/build/build.go b/misc/dashboard/app/build/build.go
new file mode 100644
index 0000000000..138a86bc5e
--- /dev/null
+++ b/misc/dashboard/app/build/build.go
@@ -0,0 +1,133 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package build
+
+import (
+ "appengine"
+ "appengine/datastore"
+ "http"
+)
+
+// A Package describes a package that is listed on the dashboard.
+type Package struct {
+ Name string
+ Path string // (empty for the main Go tree)
+}
+
+func (p *Package) Key(c appengine.Context) *datastore.Key {
+ key := p.Path
+ if key == "" {
+ key = "go"
+ }
+ return datastore.NewKey(c, "Package", key, 0, nil)
+}
+
+// A Commit describes an individual commit in a package.
+//
+// Each Commit entity is a descendant of its associated Package entity.
+// In other words, all Commits with the same PackagePath belong to the same
+// datastore entity group.
+type Commit struct {
+ PackagePath string // (empty for Go commits)
+ Num int // Internal monotonic counter unique to this package.
+ Hash string
+ ParentHash string
+
+ User string
+ Desc string `datastore:",noindex"`
+ Time datastore.Time
+
+ // Result is the Data string of each build Result for this Commit.
+ // For non-Go commits, only the Results for the current Go tip, weekly,
+ // and release Tags are stored here. This is purely de-normalized data.
+ // The complete data set is stored in Result entities.
+ Result []string `datastore:",noindex"`
+}
+
+func (com *Commit) Key(c appengine.Context) *datastore.Key {
+ key := com.PackagePath + ":" + com.Hash
+ return datastore.NewKey(c, "Commit", key, 0, nil)
+}
+
+// A Result describes a build result for a Commit on an OS/architecture.
+//
+// Each Result entity is a descendant of its associated Commit entity.
+type Result struct {
+ Builder string // "arch-os[-note]"
+ Hash string
+ PackagePath string // (empty for Go commits)
+
+ // The Go Commit this was built against (empty for Go commits).
+ GoHash string
+
+ OK bool
+ Log string `datastore:"-"` // for JSON unmarshaling
+ LogHash string `datastore:",noindex"` // Key to the Log record.
+}
+
+func (r *Result) Data() string {
+ return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
+}
+
+// A Log is a gzip-compressed log file stored under the SHA1 hash of the
+// uncompressed log text.
+type Log struct {
+ CompressedLog []byte
+}
+
+// A Tag is used to keep track of the most recent weekly and release tags.
+// Typically there will be one Tag entity for each kind of hg tag.
+type Tag struct {
+ Kind string // "weekly", "release", or "tip"
+ Name string // the tag itself (for example: "release.r60")
+ Hash string
+}
+
+func (t *Tag) Key(c appengine.Context) *datastore.Key {
+ return datastore.NewKey(c, "Tag", t.Kind, 0, nil)
+}
+
+// commitHandler records a new commit. It reads a JSON-encoded Commit value
+// from the request body and creates a new Commit entity.
+// commitHandler also updates the "tip" Tag for each new commit at tip.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func commitHandler(w http.ResponseWriter, r *http.Request)
+
+// tagHandler records a new tag. It reads a JSON-encoded Tag value from the
+// request body and updates the Tag entity for the Kind of tag provided.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func tagHandler(w http.ResponseWriter, r *http.Request)
+
+// todoHandler returns a JSON-encoded string of the hash of the next of Commit
+// to be built. It expects a "builder" query parameter.
+//
+// By default it scans the first 20 Go Commits in Num-descending order and
+// returns the first one it finds that doesn't have a Result for this builder.
+//
+// If provided with additional packagePath and goHash query parameters,
+// and scans the first 20 Commits in Num-descending order for the specified
+// packagePath and returns the first that doesn't have a Result for this builder
+// and goHash combination.
+func todoHandler(w http.ResponseWriter, r *http.Request)
+
+// resultHandler records a build result.
+// It reads a JSON-encoded Result value from the request body,
+// creates a new Result entity, and updates the relevant Commit entity.
+// If the Log field is not empty, resultHandler creates a new Log entity
+// and updates the LogHash field before putting the Commit entity.
+func resultHandler(w http.ResponseWriter, r *http.Request)
+
+// AuthHandler wraps a http.HandlerFunc with a handler that validates the
+// supplied key and builder query parameters.
+func AuthHandler(http.HandlerFunc) http.HandlerFunc
+
+func init() {
+ http.HandleFunc("/commit", AuthHandler(commitHandler))
+ http.HandleFunc("/result", AuthHandler(commitHandler))
+ http.HandleFunc("/tag", AuthHandler(tagHandler))
+ http.HandleFunc("/todo", AuthHandler(todoHandler))
+}
変更の背景
Go言語プロジェクトは、その開発プロセスにおいて継続的インテグレーション(CI)システムを運用しており、様々なプラットフォームやアーキテクチャでコードのビルドとテストを行っています。このCIシステムは「Go dashboard」として知られるウェブインターフェースを通じて、ビルドの状況や結果を開発者に可視化していました。
このコミットが行われた2011年当時、Go言語はまだ比較的新しい言語であり、そのエコシステムやツールも進化の途上にありました。既存のダッシュボードのデータ構造は、プロジェクトの成長やCIシステムの複雑化に対応しきれていない可能性がありました。例えば、ビルド結果の保存、コミット履歴の追跡、異なるパッケージの管理、そしてそれらの間の関係性を効率的に表現・クエリするための、より堅牢でスケーラブルなデータモデルが求められていたと考えられます。
このコミットは、Google App Engine (GAE) のDatastoreをバックエンドとして利用し、Goダッシュボードのデータ管理を根本から再設計することを目的としています。これにより、ビルドプロセスの各段階で生成される大量のデータを効率的に保存し、ダッシュボード上で迅速に表示できるようになることが期待されます。特に、コミットとビルド結果の関連付け、ログの保存、そして特定のビルドが必要なコミットを特定する機能(todoHandler
)などが、新しいデータ構造によって改善されることになります。
前提知識の解説
Go言語 (Golang)
GoはGoogleによって開発されたオープンソースのプログラミング言語です。静的型付け、コンパイル型言語でありながら、動的型付け言語のような簡潔さと生産性を提供することを目指しています。並行処理を強力にサポートするgoroutineとchannel、高速なコンパイル、シンプルな構文が特徴です。サーバーサイドアプリケーション、ネットワークプログラミング、CLIツールなどで広く利用されています。
Google App Engine (GAE)
Google App Engineは、Googleが提供するPlatform as a Service (PaaS) です。開発者はインフラストラクチャの管理を気にすることなく、アプリケーションをデプロイ・実行できます。Go言語はGAEのサポートするランタイムの一つであり、Goで書かれたウェブアプリケーションをGAE上で簡単にホストできます。GAEは、スケーラブルなウェブサービスを構築するために必要な様々なサービス(Datastore、Memcache、Task Queuesなど)を提供します。
Google App Engine Datastore
Datastoreは、GAEが提供するNoSQLドキュメントデータベースサービスです。スケーラビリティと可用性を重視して設計されており、大量のデータを効率的に保存・クエリできます。
- エンティティ (Entity): Datastoreに保存される個々のデータレコードです。リレーショナルデータベースの「行」に相当しますが、スキーマレスであるため、各エンティティは異なるプロパティを持つことができます。
- 種類 (Kind): エンティティのタイプを識別する文字列です。リレーショナルデータベースの「テーブル名」に似ています。
- キー (Key): Datastore内の各エンティティを一意に識別するものです。キーは、エンティティの種類、識別子(IDまたは名前)、およびオプションで祖先パス(後述)から構成されます。
- プロパティ (Property): エンティティが持つ個々のデータ項目です。リレーショナルデータベースの「列」に相当します。
- エンティティグループ (Entity Group): 関連するエンティティの集合です。同じエンティティグループに属するエンティティは、共通の祖先キーを持ちます。エンティティグループは、Datastoreのトランザクションのスコープを定義し、グループ内のエンティティに対する強い整合性(Strong Consistency)を保証します。つまり、グループ内のデータ変更はアトミックに処理され、常に最新のデータが読み取られます。ただし、エンティティグループ内の書き込みスループットには制限があります。
継続的インテグレーション (CI)
継続的インテグレーションは、ソフトウェア開発のプラクティスの一つで、開発者がコードの変更を頻繁に共有リポジトリにマージし、その都度自動的にビルドとテストを実行するものです。これにより、統合の問題を早期に発見し、ソフトウェアの品質を維持・向上させることができます。Goダッシュボードは、このCIプロセスの一部として、ビルド結果を可視化する役割を担っています。
技術的詳細
このコミットは、GoダッシュボードのバックエンドにおけるデータモデルとAPIエンドポイントを定義しています。
misc/dashboard/app/app.yaml
このファイルはGoogle App Engineアプリケーションの設定ファイルです。
application: godashboard
version: go
runtime: go
api_version: 3
handlers:
- url: /(commit|tag|todo|result)
script: _go_app
application: godashboard
: アプリケーションのIDをgodashboard
と設定しています。version: go
: アプリケーションのバージョンをgo
と設定しています。runtime: go
: アプリケーションがGo言語で書かれていることを指定します。api_version: 3
: App Engine Go SDKのAPIバージョン3を使用することを示します。handlers
: URLパスとそれに対応するスクリプトのマッピングを定義します。- url: /(commit|tag|todo|result)
:/commit
,/tag
,/todo
,/result
のいずれかのパスにマッチするリクエストを処理します。script: _go_app
: マッチしたリクエストをGoアプリケーションのメインスクリプト(コンパイルされたGoバイナリ)にルーティングします。
この設定により、外部からのHTTPリクエストがGoアプリケーション内の適切なハンドラにルーティングされるようになります。
misc/dashboard/app/build/build.go
このファイルは、GoダッシュボードがDatastoreに保存するデータ構造と、それらを操作するためのHTTPハンドラを定義しています。
データ構造の定義
-
Package
struct:- Goのパッケージを表します。
Name
: パッケージ名。Path
: パッケージのパス(メインのGoツリーの場合は空)。Key(c appengine.Context) *datastore.Key
:Package
エンティティのDatastoreキーを生成するメソッド。Path
が空の場合は"go"
をキー名として使用します。
-
Commit
struct:- 各パッケージ内の個々のコミットを表します。
PackagePath
: コミットが属するパッケージのパス(Goコミットの場合は空)。Num
: このパッケージ内で一意な内部的な単調増加カウンタ。Hash
: コミットのハッシュ値。ParentHash
: 親コミットのハッシュ値。User
: コミットの作者。Desc
: コミットメッセージ。datastore:",noindex"
タグが付いているため、このフィールドはDatastoreのインデックスには含まれません。これにより、クエリのパフォーマンスが向上し、ストレージコストが削減されますが、Desc
フィールドで直接クエリすることはできません。Time
: コミット日時。Result []string
: このコミットに対する各ビルド結果のData()
文字列を格納する、デノーマライズされたフィールド。非Goコミットの場合、現在のGoのtip、weekly、releaseタグに対する結果のみがここに保存されます。完全なデータはResult
エンティティに保存されます。これもdatastore:",noindex"
タグが付いています。Key(c appengine.Context) *datastore.Key
:Commit
エンティティのDatastoreキーを生成するメソッド。キー名はPackagePath + ":" + Hash
の形式です。- エンティティグループ: 各
Commit
エンティティは、関連するPackage
エンティティの子孫(descendant)として定義されます。これは、同じPackagePath
を持つすべてのCommit
が同じDatastoreエンティティグループに属することを意味します。これにより、Package
とCommit
間の強い整合性が保証されます。
-
Result
struct:- 特定のOS/アーキテクチャ上でのコミットに対するビルド結果を表します。
Builder
: ビルダーの名前(例: "arch-os[-note]")。Hash
: ビルド対象のコミットハッシュ。PackagePath
: パッケージのパス(Goコミットの場合は空)。GoHash
: このビルドが実行されたGoコミットのハッシュ(Goコミット自体に対するビルドの場合は空)。OK
: ビルドが成功したかどうかを示すブール値。Log
: ビルドログのテキスト。datastore:"-"
タグが付いているため、このフィールドはDatastoreに保存されません。これはJSONアンマーシャリングのための一時的なフィールドです。LogHash
: ログレコードへのキー。datastore:",noindex"
タグが付いています。Data() string
: ビルド結果の情報を文字列としてフォーマットするヘルパーメソッド。この文字列はCommit
エンティティのResult
フィールドにデノーマライズされて保存されます。- エンティティグループ: 各
Result
エンティティは、関連するCommit
エンティティの子孫として定義されます。これにより、Commit
とResult
間の強い整合性が保証されます。
-
Log
struct:- gzip圧縮されたビルドログファイルを格納します。
CompressedLog []byte
: 圧縮されたログデータ。ログは非圧縮ログテキストのSHA1ハッシュをキーとして保存されます。
-
Tag
struct:- 最新のweeklyおよびreleaseタグを追跡するために使用されます。
Kind
: タグの種類("weekly", "release", "tip")。Name
: タグ自体(例: "release.r60")。Hash
: タグが指すコミットのハッシュ。Key(c appengine.Context) *datastore.Key
:Tag
エンティティのDatastoreキーを生成するメソッド。Kind
をキー名として使用します。
HTTPハンドラの定義
このファイルでは、GoダッシュボードのバックエンドAPIとして機能するHTTPハンドラが定義されています。これらのハンドラは、app.yaml
で設定されたURLパスに対応しています。
-
commitHandler(w http.ResponseWriter, r *http.Request)
:- 新しいコミットを記録します。
- リクエストボディからJSONエンコードされた
Commit
値を読み取り、新しいCommit
エンティティをDatastoreに作成します。 - また、tipの新しいコミットごとに"tip"
Tag
を更新します。 - このハンドラは、
gobuilder
プロセスが-commit
モードで使用します。
-
tagHandler(w http.ResponseWriter, r *http.Request)
:- 新しいタグを記録します。
- リクエストボディからJSONエンコードされた
Tag
値を読み取り、提供されたタグの種類(Kind
)に対応するTag
エンティティを更新します。 - このハンドラも、
gobuilder
プロセスが-commit
モードで使用します。
-
todoHandler(w http.ResponseWriter, r *http.Request)
:- 次にビルドされるべきコミットのハッシュをJSONエンコードされた文字列で返します。
"builder"
クエリパラメータを期待します。- デフォルトでは、Goコミットを
Num
の降順で最初の20件スキャンし、このビルダーに対するResult
を持たない最初のコミットを返します。 packagePath
とgoHash
の追加クエリパラメータが提供された場合、指定されたpackagePath
のコミットをスキャンし、このビルダーとgoHash
の組み合わせに対するResult
を持たない最初のコミットを返します。
-
resultHandler(w http.ResponseWriter, r *http.Request)
:- ビルド結果を記録します。
- リクエストボディからJSONエンコードされた
Result
値を読み取り、新しいResult
エンティティをDatastoreに作成し、関連するCommit
エンティティを更新します。 Log
フィールドが空でない場合、resultHandler
は新しいLog
エンティティを作成し、Commit
エンティティを保存する前にLogHash
フィールドを更新します。
-
AuthHandler(http.HandlerFunc) http.HandlerFunc
:http.HandlerFunc
をラップするハンドラで、提供されたキーとビルダーのクエリパラメータを検証します。これにより、APIエンドポイントへのアクセスが認証されたクライアントに限定されます。
init()
関数
func init() {
http.HandleFunc("/commit", AuthHandler(commitHandler))
http.HandleFunc("/result", AuthHandler(commitHandler))
http.HandleFunc("/tag", AuthHandler(tagHandler))
http.HandleFunc("/todo", AuthHandler(todoHandler))
}
Goのinit()
関数は、パッケージがインポートされたときに自動的に実行されます。ここでは、各URLパスと対応するHTTPハンドラを登録しています。AuthHandler
で各ハンドラをラップすることで、認証ロジックが適用されます。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルが新規に追加されています。
-
misc/dashboard/app/app.yaml
:- Google App Engineアプリケーションの構成ファイル。
- アプリケーションID、ランタイム(Go)、APIバージョン、およびHTTPリクエストのルーティングルールが定義されています。
-
misc/dashboard/app/build/build.go
:- Goダッシュボードのデータ構造(
Package
,Commit
,Result
,Log
,Tag
)が定義されています。 - これらのデータ構造をDatastoreに保存・取得するための
Key()
メソッドが実装されています。 - ダッシュボードのAPIエンドポイントとして機能するHTTPハンドラ(
commitHandler
,tagHandler
,todoHandler
,resultHandler
)が定義されています。 - ハンドラを認証でラップする
AuthHandler
と、ハンドラを登録するinit()
関数が含まれています。
- Goダッシュボードのデータ構造(
コアとなるコードの解説
app.yaml
の役割
app.yaml
は、GoアプリケーションがGoogle App Engine上でどのようにデプロイされ、動作するかをGAEに指示するものです。このコミットで追加されたapp.yaml
は、godashboard
というアプリケーションIDを持ち、Goランタイムを使用し、/commit
, /tag
, /todo
, /result
といった特定のURLパスへのリクエストをGoアプリケーションのメインエントリーポイント(_go_app
)にルーティングするように設定しています。これにより、外部からのAPI呼び出しがGoアプリケーション内の適切なハンドラに到達できるようになります。
build.go
におけるデータ構造とDatastoreの設計
build.go
の核心は、GoダッシュボードのデータをDatastoreに効率的かつ整合性を持って保存するためのデータモデルです。
Package
: 最上位のエンティティとして機能します。Goのメインツリーや個別のGoパッケージを表します。Commit
とエンティティグループ:Commit
エンティティは、その親であるPackage
エンティティの子孫として設計されています。
この親子関係は、Datastoreのエンティティグループの概念を利用しています。同じエンティティグループに属するエンティティは、強い整合性(Strong Consistency)が保証されます。つまり、// Each Commit entity is a descendant of its associated Package entity. // In other words, all Commits with the same PackagePath belong to the same // datastore entity group. type Commit struct { // ... }
Package
とCommit
に対する更新はアトミックに処理され、常に最新のデータが読み取られることが保証されます。これは、CIシステムにおいてコミットとそれに関連する情報の一貫性が非常に重要であるため、理にかなった設計です。Result
とエンティティグループ: 同様に、Result
エンティティは、その親であるCommit
エンティティの子孫として設計されています。
これにより、特定のコミットに対するビルド結果の強い整合性が保証されます。// Each Result entity is a descendant of its associated Commit entity. type Result struct { // ... }
datastore:",noindex"
タグ:Commit
構造体のDesc
フィールドとResult
フィールド、およびResult
構造体のLogHash
フィールドにはdatastore:",noindex"
タグが付与されています。
このタグは、Datastoreがこれらのフィールドにインデックスを作成しないように指示します。インデックスを作成しないことで、書き込み操作のパフォーマンスが向上し、ストレージコストが削減されます。しかし、これらのフィールドを直接クエリのフィルタ条件として使用することはできなくなります。Desc string `datastore:",noindex"` Result []string `datastore:",noindex"` LogHash string `datastore:",noindex"`
Desc
(コミットメッセージ)やResult
(デノーマライズされたビルド結果のリスト)は、通常、全文検索の対象ではなく、特定のコミットに関連付けられた情報として取得されることが多いため、この設計は適切です。LogHash
もログ本体への参照であり、直接クエリされることは稀です。- デノーマライズ化:
Commit
構造体のResult []string
フィールドは、関連するResult
エンティティのData()
文字列を直接格納することで、データをデノーマライズしています。
これにより、特定のコミットのビルド結果の概要を、追加のDatastoreクエリなしで// Result is the Data string of each build Result for this Commit. // For non-Go commits, only the Results for the current Go tip, weekly, // and release Tags are stored here. This is purely de-normalized data. // The complete data set is stored in Result entities. Result []string `datastore:",noindex"`
Commit
エンティティから直接取得できるようになり、読み込みパフォーマンスが向上します。完全な詳細が必要な場合は、個別のResult
エンティティをクエリします。 Log
構造体: ビルドログは、そのサイズが大きくなる可能性があるため、CompressedLog []byte
として別途Log
エンティティに保存されます。これにより、ログの保存と取得が効率的に行われ、メインのResult
エンティティのサイズを小さく保つことができます。
HTTPハンドラの機能
定義されたHTTPハンドラは、GoダッシュボードのCIシステムとのインタラクションを可能にします。
commitHandler
とresultHandler
は、gobuilder
プロセスからの新しいコミット情報とビルド結果を受け取り、Datastoreに永続化します。tagHandler
は、Goのバージョンタグ(weekly, release, tip)の更新を処理します。todoHandler
は、特定のビルダーが次にビルドすべきコミットを効率的に特定するためのAPIを提供します。これは、CIシステムがビルドキューを管理する上で不可欠な機能です。AuthHandler
は、これらのAPIエンドポイントへのアクセスを保護し、認証されたgobuilder
プロセスのみがデータを送信できるようにします。
これらのデータ構造とハンドラの組み合わせにより、Goダッシュボードは、Goプロジェクトの継続的な開発とテストのプロセスから生成される大量のデータを、スケーラブルかつ整合性を持って管理できるようになります。
関連リンク
- Go Code Review: https://golang.org/cl/5416056
参考にした情報源リンク
- Google App Engine Documentation (Go): (当時のドキュメントは直接リンクできませんが、App EngineのGoランタイムとDatastoreに関する公式ドキュメントが参考になります)
- Google Cloud Datastore Documentation: (当時のDatastoreの概念に関する公式ドキュメントが参考になります)
- Go言語公式ドキュメント: https://go.dev/
- 継続的インテグレーション (CI) の概念: (一般的なCIに関する情報源)
[インデックス 10485] ファイルの概要
このコミットは、Go言語プロジェクトのダッシュボードアプリケーションにおける新しいデータ構造の設計と実装を導入するものです。具体的には、Google App Engine (GAE) のDatastoreを利用して、Goパッケージ、コミット、ビルド結果、ログ、タグといった情報を効率的に管理するためのGo言語の構造体と、それらを操作するためのHTTPハンドラが定義されています。これにより、Goプロジェクトの継続的インテグレーション(CI)システムが生成するビルド結果やコミット情報を、より堅牢かつスケーラブルに保存・取得できるようになります。
コミット
dashboard: new Go dashboard data structure design
R=rsc, r, dsymonds, bradfitz CC=golang-dev https://golang.org/cl/5416056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/79bce499a336471638f5aa02bd258e516019ad6f
元コミット内容
commit 79bce499a336471638f5aa02bd258e516019ad6f
Author: Andrew Gerrand <adg@golang.org>
Date: Wed Nov 23 08:13:05 2011 +1100
dashboard: new Go dashboard data structure design
R=rsc, r, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/5416056
---
misc/dashboard/app/app.yaml | 8 +++
misc/dashboard/app/build/build.go | 133 ++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+)
diff --git a/misc/dashboard/app/app.yaml b/misc/dashboard/app/app.yaml
new file mode 100644
index 0000000000..695c04e78a
--- /dev/null
+++ b/misc/dashboard/app/app.yaml
@@ -0,0 +1,8 @@
+application: godashboard
+version: go
+runtime: go
+api_version: 3
+
+handlers:
+- url: /(commit|tag|todo|result)
+ script: _go_app
diff --git a/misc/dashboard/app/build/build.go b/misc/dashboard/app/build/build.go
new file mode 100644
index 0000000000..138a86bc5e
--- /dev/null
+++ b/misc/dashboard/app/build/build.go
@@ -0,0 +1,133 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package build
+
+import (
+ "appengine"
+ "appengine/datastore"
+ "http"
+)
+
+// A Package describes a package that is listed on the dashboard.
+type Package struct {
+ Name string
+ Path string // (empty for the main Go tree)
+}
+
+func (p *Package) Key(c appengine.Context) *datastore.Key {
+ key := p.Path
+ if key == "" {
+ key = "go"
+ }
+ return datastore.NewKey(c, "Package", key, 0, nil)
+}
+
+// A Commit describes an individual commit in a package.
+//
+// Each Commit entity is a descendant of its associated Package entity.
+// In other words, all Commits with the same PackagePath belong to the same
+// datastore entity group.
+type Commit struct {
+ PackagePath string // (empty for Go commits)
+ Num int // Internal monotonic counter unique to this package.
+ Hash string
+ ParentHash string
+
+ User string
+ Desc string `datastore:",noindex"`
+ Time datastore.Time
+
+ // Result is the Data string of each build Result for this Commit.
+ // For non-Go commits, only the Results for the current Go tip, weekly,
+ // and release Tags are stored here. This is purely de-normalized data.
+ // The complete data set is stored in Result entities.
+ Result []string `datastore:",noindex"`
+}
+
+func (com *Commit) Key(c appengine.Context) *datastore.Key {
+ key := com.PackagePath + ":" + com.Hash
+ return datastore.NewKey(c, "Commit", key, 0, nil)
+}
+
+// A Result describes a build result for a Commit on an OS/architecture.
+//
+// Each Result entity is a descendant of its associated Commit entity.
+type Result struct {
+ Builder string // "arch-os[-note]"
+ Hash string
+ PackagePath string // (empty for Go commits)
+
+ // The Go Commit this was built against (empty for Go commits).
+ GoHash string
+
+ OK bool
+ Log string `datastore:"-"` // for JSON unmarshaling
+ LogHash string `datastore:",noindex"` // Key to the Log record.
+}
+
+func (r *Result) Data() string {
+ return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
+}
+
+// A Log is a gzip-compressed log file stored under the SHA1 hash of the
+// uncompressed log text.
+type Log struct {
+ CompressedLog []byte
+}
+
+// A Tag is used to keep track of the most recent weekly and release tags.
+// Typically there will be one Tag entity for each kind of hg tag.
+type Tag struct {
+ Kind string // "weekly", "release", or "tip"
+ Name string // the tag itself (for example: "release.r60")
+ Hash string
+}
+
+func (t *Tag) Key(c appengine.Context) *datastore.Key {
+ return datastore.NewKey(c, "Tag", t.Kind, 0, nil)
+}
+
+// commitHandler records a new commit. It reads a JSON-encoded Commit value
+// from the request body and creates a new Commit entity.
+// commitHandler also updates the "tip" Tag for each new commit at tip.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func commitHandler(w http.ResponseWriter, r *http.Request)
+
+// tagHandler records a new tag. It reads a JSON-encoded Tag value from the
+// request body and updates the Tag entity for the Kind of tag provided.
+//
+// This handler is used by a gobuilder process in -commit mode.
+func tagHandler(w http.ResponseWriter, r *http.Request)
+
+// todoHandler returns a JSON-encoded string of the hash of the next of Commit
+// to be built. It expects a "builder" query parameter.
+//
+// By default it scans the first 20 Go Commits in Num-descending order and
+// returns the first one it finds that doesn't have a Result for this builder.
+//
+// If provided with additional packagePath and goHash query parameters,
+// and scans the first 20 Commits in Num-descending order for the specified
+// packagePath and returns the first that doesn't have a Result for this builder
+// and goHash combination.
+func todoHandler(w http.ResponseWriter, r *http.Request)
+
+// resultHandler records a build result.
+// It reads a JSON-encoded Result value from the request body,
+// creates a new Result entity, and updates the relevant Commit entity.
+// If the Log field is not empty, resultHandler creates a new Log entity
+// and updates the LogHash field before putting the Commit entity.
+func resultHandler(w http.ResponseWriter, r *http.Request)
+
+// AuthHandler wraps a http.HandlerFunc with a handler that validates the
+// supplied key and builder query parameters.
+func AuthHandler(http.HandlerFunc) http.HandlerFunc
+
+func init() {
+ http.HandleFunc("/commit", AuthHandler(commitHandler))
+ http.HandleFunc("/result", AuthHandler(commitHandler))
+ http.HandleFunc("/tag", AuthHandler(tagHandler))
+ http.HandleFunc("/todo", AuthHandler(todoHandler))
+}
変更の背景
Go言語プロジェクトは、その開発プロセスにおいて継続的インテグレーション(CI)システムを運用しており、様々なプラットフォームやアーキテクチャでコードのビルドとテストを行っています。このCIシステムは「Go dashboard」として知られるウェブインターフェースを通じて、ビルドの状況や結果を開発者に可視化していました。
このコミットが行われた2011年当時、Go言語はまだ比較的新しい言語であり、そのエコシステムやツールも進化の途上にありました。既存のダッシュボードのデータ構造は、プロジェクトの成長やCIシステムの複雑化に対応しきれていない可能性がありました。例えば、ビルド結果の保存、コミット履歴の追跡、異なるパッケージの管理、そしてそれらの間の関係性を効率的に表現・クエリするための、より堅牢でスケーラブルなデータモデルが求められていたと考えられます。
このコミットは、Google App Engine (GAE) のDatastoreをバックエンドとして利用し、Goダッシュボードのデータ管理を根本から再設計することを目的としています。これにより、ビルドプロセスの各段階で生成される大量のデータを効率的に保存し、ダッシュボード上で迅速に表示できるようになることが期待されます。特に、コミットとビルド結果の関連付け、ログの保存、そして特定のビルドが必要なコミットを特定する機能(todoHandler
)などが、新しいデータ構造によって改善されることになります。
前提知識の解説
Go言語 (Golang)
GoはGoogleによって開発されたオープンソースのプログラミング言語です。静的型付け、コンパイル型言語でありながら、動的型付け言語のような簡潔さと生産性を提供することを目指しています。並行処理を強力にサポートするgoroutineとchannel、高速なコンパイル、シンプルな構文が特徴です。サーバーサイドアプリケーション、ネットワークプログラミング、CLIツールなどで広く利用されています。
Google App Engine (GAE)
Google App Engineは、Googleが提供するPlatform as a Service (PaaS) です。開発者はインフラストラクチャの管理を気にすることなく、アプリケーションをデプロイ・実行できます。Go言語はGAEのサポートするランタイムの一つであり、Goで書かれたウェブアプリケーションをGAE上で簡単にホストできます。GAEは、スケーラブルなウェブサービスを構築するために必要な様々なサービス(Datastore、Memcache、Task Queuesなど)を提供します。
Google App Engine Datastore
Datastoreは、GAEが提供するNoSQLドキュメントデータベースサービスです。スケーラビリティと可用性を重視して設計されており、大量のデータを効率的に保存・クエリできます。
- エンティティ (Entity): Datastoreに保存される個々のデータレコードです。リレーショナルデータベースの「行」に相当しますが、スキーマレスであるため、各エンティティは異なるプロパティを持つことができます。
- 種類 (Kind): エンティティのタイプを識別する文字列です。リレーショナルデータベースの「テーブル名」に似ています。
- キー (Key): Datastore内の各エンティティを一意に識別するものです。キーは、エンティティの種類、識別子(IDまたは名前)、およびオプションで祖先パス(後述)から構成されます。
- プロパティ (Property): エンティティが持つ個々のデータ項目です。リレーショナルデータベースの「列」に相当します。
- エンティティグループ (Entity Group): 関連するエンティティの集合です。同じエンティティグループに属するエンティティは、共通の祖先キーを持ちます。エンティティグループは、Datastoreのトランザクションのスコープを定義し、グループ内のエンティティに対する強い整合性(Strong Consistency)を保証します。つまり、グループ内のデータ変更はアトミックに処理され、常に最新のデータが読み取られます。ただし、エンティティグループ内の書き込みスループットには制限があります。
継続的インテグレーション (CI)
継続的インテグレーションは、ソフトウェア開発のプラクティスの一つで、開発者がコードの変更を頻繁に共有リポジトリにマージし、その都度自動的にビルドとテストを実行するものです。これにより、統合の問題を早期に発見し、ソフトウェアの品質を維持・向上させることができます。Goダッシュボードは、このCIプロセスの一部として、ビルド結果を可視化する役割を担っています。
技術的詳細
このコミットは、GoダッシュボードのバックエンドにおけるデータモデルとAPIエンドポイントを定義しています。
misc/dashboard/app/app.yaml
このファイルはGoogle App Engineアプリケーションの設定ファイルです。
application: godashboard
version: go
runtime: go
api_version: 3
handlers:
- url: /(commit|tag|todo|result)
script: _go_app
application: godashboard
: アプリケーションのIDをgodashboard
と設定しています。version: go
: アプリケーションのバージョンをgo
と設定しています。runtime: go
: アプリケーションがGo言語で書かれていることを指定します。api_version: 3
: App Engine Go SDKのAPIバージョン3を使用することを示します。handlers
: URLパスとそれに対応するスクリプトのマッピングを定義します。- url: /(commit|tag|todo|result)
:/commit
,/tag
,/todo
,/result
のいずれかのパスにマッチするリクエストを処理します。script: _go_app
: マッチしたリクエストをGoアプリケーションのメインスクリプト(コンパイルされたGoバイナリ)にルーティングします。
この設定により、外部からのHTTPリクエストがGoアプリケーション内の適切なハンドラにルーティングされるようになります。
misc/dashboard/app/build/build.go
このファイルは、GoダッシュボードがDatastoreに保存するデータ構造と、それらを操作するためのHTTPハンドラを定義しています。
データ構造の定義
-
Package
struct:- Goのパッケージを表します。
Name
: パッケージ名。Path
: パッケージのパス(メインのGoツリーの場合は空)。Key(c appengine.Context) *datastore.Key
:Package
エンティティのDatastoreキーを生成するメソッド。Path
が空の場合は"go"
をキー名として使用します。
-
Commit
struct:- 各パッケージ内の個々のコミットを表します。
PackagePath
: コミットが属するパッケージのパス(Goコミットの場合は空)。Num
: このパッケージ内で一意な内部的な単調増加カウンタ。Hash
: コミットのハッシュ値。ParentHash
: 親コミットのハッシュ値。User
: コミットの作者。Desc
: コミットメッセージ。datastore:",noindex"
タグが付いているため、このフィールドはDatastoreのインデックスには含まれません。これにより、クエリのパフォーマンスが向上し、ストレージコストが削減されますが、Desc
フィールドで直接クエリすることはできません。Time
: コミット日時。Result []string
: このコミットに対する各ビルド結果のData()
文字列を格納する、デノーマライズされたフィールド。非Goコミットの場合、現在のGoのtip、weekly、releaseタグに対する結果のみがここに保存されます。完全なデータはResult
エンティティに保存されます。これもdatastore:",noindex"
タグが付いています。Key(c appengine.Context) *datastore.Key
:Commit
エンティティのDatastoreキーを生成するメソッド。キー名はPackagePath + ":" + Hash
の形式です。- エンティティグループ: 各
Commit
エンティティは、関連するPackage
エンティティの子孫(descendant)として定義されます。これは、同じPackagePath
を持つすべてのCommit
が同じDatastoreエンティティグループに属することを意味します。これにより、Package
とCommit
間の強い整合性が保証されます。
-
Result
struct:- 特定のOS/アーキテクチャ上でのコミットに対するビルド結果を表します。
Builder
: ビルダーの名前(例: "arch-os[-note]")。Hash
: ビルド対象のコミットハッシュ。PackagePath
: パッケージのパス(Goコミットの場合は空)。GoHash
: このビルドが実行されたGoコミットのハッシュ(Goコミット自体に対するビルドの場合は空)。OK
: ビルドが成功したかどうかを示すブール値。Log
: ビルドログのテキスト。datastore:"-"
タグが付いているため、このフィールドはDatastoreに保存されません。これはJSONアンマーシャリングのための一時的なフィールドです。LogHash
: ログレコードへのキー。datastore:",noindex"
タグが付いています。Data() string
: ビルド結果の情報を文字列としてフォーマットするヘルパーメソッド。この文字列はCommit
エンティティのResult
フィールドにデノーマライズされて保存されます。- エンティティグループ: 各
Result
エンティティは、関連するCommit
エンティティの子孫として定義されます。これにより、Commit
とResult
間の強い整合性が保証されます。
-
Log
struct:- gzip圧縮されたビルドログファイルを格納します。
CompressedLog []byte
: 圧縮されたログデータ。ログは非圧縮ログテキストのSHA1ハッシュをキーとして保存されます。
-
Tag
struct:- 最新のweeklyおよびreleaseタグを追跡するために使用されます。
Kind
: タグの種類("weekly", "release", "tip")。Name
: タグ自体(例: "release.r60")。Hash
: タグが指すコミットのハッシュ。Key(c appengine.Context) *datastore.Key
:Tag
エンティティのDatastoreキーを生成するメソッド。Kind
をキー名として使用します。
HTTPハンドラの定義
このファイルでは、GoダッシュボードのバックエンドAPIとして機能するHTTPハンドラが定義されています。これらのハンドラは、app.yaml
で設定されたURLパスに対応しています。
-
commitHandler(w http.ResponseWriter, r *http.Request)
:- 新しいコミットを記録します。
- リクエストボディからJSONエンコードされた
Commit
値を読み取り、新しいCommit
エンティティをDatastoreに作成します。 - また、tipの新しいコミットごとに"tip"
Tag
を更新します。 - このハンドラは、
gobuilder
プロセスが-commit
モードで使用します。
-
tagHandler(w http.ResponseWriter, r *http.Request)
:- 新しいタグを記録します。
- リクエストボディからJSONエンコードされた
Tag
値を読み取り、提供されたタグの種類(Kind
)に対応するTag
エンティティを更新します。 - このハンドラも、
gobuilder
プロセスが-commit
モードで使用します。
-
todoHandler(w http.ResponseWriter, r *http.Request)
:- 次にビルドされるべきコミットのハッシュをJSONエンコードされた文字列で返します。
"builder"
クエリパラメータを期待します。- デフォルトでは、Goコミットを
Num
の降順で最初の20件スキャンし、このビルダーに対するResult
を持たない最初のコミットを返します。 packagePath
とgoHash
の追加クエリパラメータが提供された場合、指定されたpackagePath
のコミットをスキャンし、このビルダーとgoHash
の組み合わせに対するResult
を持たない最初のコミットを返します。
-
resultHandler(w http.ResponseWriter, r *http.Request)
:- ビルド結果を記録します。
- リクエストボディからJSONエンコードされた
Result
値を読み取り、新しいResult
エンティティをDatastoreに作成し、関連するCommit
エンティティを更新します。 Log
フィールドが空でない場合、resultHandler
は新しいLog
エンティティを作成し、Commit
エンティティを保存する前にLogHash
フィールドを更新します。
-
AuthHandler(http.HandlerFunc) http.HandlerFunc
:http.HandlerFunc
をラップするハンドラで、提供されたキーとビルダーのクエリパラメータを検証します。これにより、APIエンドポイントへのアクセスが認証されたクライアントに限定されます。
init()
関数
func init() {
http.HandleFunc("/commit", AuthHandler(commitHandler))
http.HandleFunc("/result", AuthHandler(commitHandler))
http.HandleFunc("/tag", AuthHandler(tagHandler))
http.HandleFunc("/todo", AuthHandler(todoHandler))
}
Goのinit()
関数は、パッケージがインポートされたときに自動的に実行されます。ここでは、各URLパスと対応するHTTPハンドラを登録しています。AuthHandler
で各ハンドラをラップすることで、認証ロジックが適用されます。
コアとなるコードの変更箇所
このコミットでは、以下の2つのファイルが新規に追加されています。
-
misc/dashboard/app/app.yaml
:- Google App Engineアプリケーションの構成ファイル。
- アプリケーションID、ランタイム(Go)、APIバージョン、およびHTTPリクエストのルーティングルールが定義されています。
-
misc/dashboard/app/build/build.go
:- Goダッシュボードのデータ構造(
Package
,Commit
,Result
,Log
,Tag
)が定義されています。 - これらのデータ構造をDatastoreに保存・取得するための
Key()
メソッドが実装されています。 - ダッシュボードのAPIエンドポイントとして機能するHTTPハンドラ(
commitHandler
,tagHandler
,todoHandler
,resultHandler
)が定義されています。 - ハンドラを認証でラップする
AuthHandler
と、ハンドラを登録するinit()
関数が含まれています。
- Goダッシュボードのデータ構造(
コアとなるコードの解説
app.yaml
の役割
app.yaml
は、GoアプリケーションがGoogle App Engine上でどのようにデプロイされ、動作するかをGAEに指示するものです。このコミットで追加されたapp.yaml
は、godashboard
というアプリケーションIDを持ち、Goランタイムを使用し、/commit
, /tag
, /todo
, /result
といった特定のURLパスへのリクエストをGoアプリケーションのメインエントリーポイント(_go_app
)にルーティングするように設定しています。これにより、外部からのAPI呼び出しがGoアプリケーション内の適切なハンドラに到達できるようになります。
build.go
におけるデータ構造とDatastoreの設計
build.go
の核心は、GoダッシュボードのデータをDatastoreに効率的かつ整合性を持って保存するためのデータモデルです。
Package
: 最上位のエンティティとして機能します。Goのメインツリーや個別のGoパッケージを表します。Commit
とエンティティグループ:Commit
エンティティは、その親であるPackage
エンティティの子孫として設計されています。
この親子関係は、Datastoreのエンティティグループの概念を利用しています。同じエンティティグループに属するエンティティは、強い整合性(Strong Consistency)が保証されます。つまり、// Each Commit entity is a descendant of its associated Package entity. // In other words, all Commits with the same PackagePath belong to the same // datastore entity group. type Commit struct { // ... }
Package
とCommit
に対する更新はアトミックに処理され、常に最新のデータが読み取られることが保証されます。これは、CIシステムにおいてコミットとそれに関連する情報の一貫性が非常に重要であるため、理にかなった設計です。Result
とエンティティグループ: 同様に、Result
エンティティは、その親であるCommit
エンティティの子孫として設計されています。
これにより、特定のコミットに対するビルド結果の強い整合性が保証されます。// Each Result entity is a descendant of its associated Commit entity. type Result struct { // ... }
datastore:",noindex"
タグ:Commit
構造体のDesc
フィールドとResult
フィールド、およびResult
構造体のLogHash
フィールドにはdatastore:",noindex"
タグが付与されています。
このタグは、Datastoreがこれらのフィールドにインデックスを作成しないように指示します。インデックスを作成しないことで、書き込み操作のパフォーマンスが向上し、ストレージコストが削減されます。しかし、これらのフィールドを直接クエリのフィルタ条件として使用することはできなくなります。Desc string `datastore:",noindex"` Result []string `datastore:",noindex"` LogHash string `datastore:",noindex"`
Desc
(コミットメッセージ)やResult
(デノーマライズされたビルド結果のリスト)は、通常、全文検索の対象ではなく、特定のコミットに関連付けられた情報として取得されることが多いため、この設計は適切です。LogHash
もログ本体への参照であり、直接クエリされることは稀です。- デノーマライズ化:
Commit
構造体のResult []string
フィールドは、関連するResult
エンティティのData()
文字列を直接格納することで、データをデノーマライズしています。
これにより、特定のコミットのビルド結果の概要を、追加のDatastoreクエリなしで// Result is the Data string of each build Result for this Commit. // For non-Go commits, only the Results for the current Go tip, weekly, // and release Tags are stored here. This is purely de-normalized data. // The complete data set is stored in Result entities. Result []string `datastore:",noindex"`
Commit
エンティティから直接取得できるようになり、読み込みパフォーマンスが向上します。完全な詳細が必要な場合は、個別のResult
エンティティをクエリします。 Log
構造体: ビルドログは、そのサイズが大きくなる可能性があるため、CompressedLog []byte
として別途Log
エンティティに保存されます。これにより、ログの保存と取得が効率的に行われ、メインのResult
エンティティのサイズを小さく保つことができます。
HTTPハンドラの機能
定義されたHTTPハンドラは、GoダッシュボードのCIシステムとのインタラクションを可能にします。
commitHandler
とresultHandler
は、gobuilder
プロセスからの新しいコミット情報とビルド結果を受け取り、Datastoreに永続化します。tagHandler
は、Goのバージョンタグ(weekly, release, tip)の更新を処理します。todoHandler
は、特定のビルダーが次にビルドすべきコミットを効率的に特定するためのAPIを提供します。これは、CIシステムがビルドキューを管理する上で不可欠な機能です。AuthHandler
は、これらのAPIエンドポイントへのアクセスを保護し、認証されたgobuilder
プロセスのみがデータを送信できるようにします。
これらのデータ構造とハンドラの組み合わせにより、Goダッシュボードは、Goプロジェクトの継続的な開発とテストのプロセスから生成される大量のデータを、スケーラブルかつ整合性を持って管理できるようになります。
関連リンク
- Go Code Review: https://golang.org/cl/5416056
参考にした情報源リンク
- Google App Engine Documentation (Go): (当時のドキュメントは直接リンクできませんが、App EngineのGoランタイムとDatastoreに関する公式ドキュメントが参考になります)
- Google Cloud Datastore Documentation: (当時のDatastoreの概念に関する公式ドキュメントが参考になります)
- Go言語公式ドキュメント: https://go.dev/
- 継続的インテグレーション (CI) の概念: (一般的なCIに関する情報源)