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

[インデックス 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ハンドラを定義しています。

データ構造の定義

  1. Package struct:

    • Goのパッケージを表します。
    • Name: パッケージ名。
    • Path: パッケージのパス(メインのGoツリーの場合は空)。
    • Key(c appengine.Context) *datastore.Key: PackageエンティティのDatastoreキーを生成するメソッド。Pathが空の場合は"go"をキー名として使用します。
  2. 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エンティティグループに属することを意味します。これにより、PackageCommit間の強い整合性が保証されます。
  3. 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エンティティの子孫として定義されます。これにより、CommitResult間の強い整合性が保証されます。
  4. Log struct:

    • gzip圧縮されたビルドログファイルを格納します。
    • CompressedLog []byte: 圧縮されたログデータ。ログは非圧縮ログテキストのSHA1ハッシュをキーとして保存されます。
  5. 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を持たない最初のコミットを返します。
    • packagePathgoHashの追加クエリパラメータが提供された場合、指定された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つのファイルが新規に追加されています。

  1. misc/dashboard/app/app.yaml:

    • Google App Engineアプリケーションの構成ファイル。
    • アプリケーションID、ランタイム(Go)、APIバージョン、およびHTTPリクエストのルーティングルールが定義されています。
  2. misc/dashboard/app/build/build.go:

    • Goダッシュボードのデータ構造(Package, Commit, Result, Log, Tag)が定義されています。
    • これらのデータ構造をDatastoreに保存・取得するためのKey()メソッドが実装されています。
    • ダッシュボードのAPIエンドポイントとして機能するHTTPハンドラ(commitHandler, tagHandler, todoHandler, resultHandler)が定義されています。
    • ハンドラを認証でラップするAuthHandlerと、ハンドラを登録するinit()関数が含まれています。

コアとなるコードの解説

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エンティティの子孫として設計されています。
    // 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 {
        // ...
    }
    
    この親子関係は、Datastoreのエンティティグループの概念を利用しています。同じエンティティグループに属するエンティティは、強い整合性(Strong Consistency)が保証されます。つまり、PackageCommitに対する更新はアトミックに処理され、常に最新のデータが読み取られることが保証されます。これは、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"タグが付与されています。
    Desc string `datastore:",noindex"`
    Result []string `datastore:",noindex"`
    LogHash string `datastore:",noindex"`
    
    このタグは、Datastoreがこれらのフィールドにインデックスを作成しないように指示します。インデックスを作成しないことで、書き込み操作のパフォーマンスが向上し、ストレージコストが削減されます。しかし、これらのフィールドを直接クエリのフィルタ条件として使用することはできなくなります。Desc(コミットメッセージ)やResult(デノーマライズされたビルド結果のリスト)は、通常、全文検索の対象ではなく、特定のコミットに関連付けられた情報として取得されることが多いため、この設計は適切です。LogHashもログ本体への参照であり、直接クエリされることは稀です。
  • デノーマライズ化: Commit構造体のResult []stringフィールドは、関連するResultエンティティのData()文字列を直接格納することで、データをデノーマライズしています。
    // 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"`
    
    これにより、特定のコミットのビルド結果の概要を、追加のDatastoreクエリなしでCommitエンティティから直接取得できるようになり、読み込みパフォーマンスが向上します。完全な詳細が必要な場合は、個別のResultエンティティをクエリします。
  • Log構造体: ビルドログは、そのサイズが大きくなる可能性があるため、CompressedLog []byteとして別途Logエンティティに保存されます。これにより、ログの保存と取得が効率的に行われ、メインのResultエンティティのサイズを小さく保つことができます。

HTTPハンドラの機能

定義されたHTTPハンドラは、GoダッシュボードのCIシステムとのインタラクションを可能にします。

  • commitHandlerresultHandlerは、gobuilderプロセスからの新しいコミット情報とビルド結果を受け取り、Datastoreに永続化します。
  • tagHandlerは、Goのバージョンタグ(weekly, release, tip)の更新を処理します。
  • todoHandlerは、特定のビルダーが次にビルドすべきコミットを効率的に特定するためのAPIを提供します。これは、CIシステムがビルドキューを管理する上で不可欠な機能です。
  • AuthHandlerは、これらのAPIエンドポイントへのアクセスを保護し、認証されたgobuilderプロセスのみがデータを送信できるようにします。

これらのデータ構造とハンドラの組み合わせにより、Goダッシュボードは、Goプロジェクトの継続的な開発とテストのプロセスから生成される大量のデータを、スケーラブルかつ整合性を持って管理できるようになります。

関連リンク

参考にした情報源リンク

  • 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ハンドラを定義しています。

データ構造の定義

  1. Package struct:

    • Goのパッケージを表します。
    • Name: パッケージ名。
    • Path: パッケージのパス(メインのGoツリーの場合は空)。
    • Key(c appengine.Context) *datastore.Key: PackageエンティティのDatastoreキーを生成するメソッド。Pathが空の場合は"go"をキー名として使用します。
  2. 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エンティティグループに属することを意味します。これにより、PackageCommit間の強い整合性が保証されます。
  3. 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エンティティの子孫として定義されます。これにより、CommitResult間の強い整合性が保証されます。
  4. Log struct:

    • gzip圧縮されたビルドログファイルを格納します。
    • CompressedLog []byte: 圧縮されたログデータ。ログは非圧縮ログテキストのSHA1ハッシュをキーとして保存されます。
  5. 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を持たない最初のコミットを返します。
    • packagePathgoHashの追加クエリパラメータが提供された場合、指定された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つのファイルが新規に追加されています。

  1. misc/dashboard/app/app.yaml:

    • Google App Engineアプリケーションの構成ファイル。
    • アプリケーションID、ランタイム(Go)、APIバージョン、およびHTTPリクエストのルーティングルールが定義されています。
  2. misc/dashboard/app/build/build.go:

    • Goダッシュボードのデータ構造(Package, Commit, Result, Log, Tag)が定義されています。
    • これらのデータ構造をDatastoreに保存・取得するためのKey()メソッドが実装されています。
    • ダッシュボードのAPIエンドポイントとして機能するHTTPハンドラ(commitHandler, tagHandler, todoHandler, resultHandler)が定義されています。
    • ハンドラを認証でラップするAuthHandlerと、ハンドラを登録するinit()関数が含まれています。

コアとなるコードの解説

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エンティティの子孫として設計されています。
    // 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 {
        // ...
    }
    
    この親子関係は、Datastoreのエンティティグループの概念を利用しています。同じエンティティグループに属するエンティティは、強い整合性(Strong Consistency)が保証されます。つまり、PackageCommitに対する更新はアトミックに処理され、常に最新のデータが読み取られることが保証されます。これは、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"タグが付与されています。
    Desc string `datastore:",noindex"`
    Result []string `datastore:",noindex"`
    LogHash string `datastore:",noindex"`
    
    このタグは、Datastoreがこれらのフィールドにインデックスを作成しないように指示します。インデックスを作成しないことで、書き込み操作のパフォーマンスが向上し、ストレージコストが削減されます。しかし、これらのフィールドを直接クエリのフィルタ条件として使用することはできなくなります。Desc(コミットメッセージ)やResult(デノーマライズされたビルド結果のリスト)は、通常、全文検索の対象ではなく、特定のコミットに関連付けられた情報として取得されることが多いため、この設計は適切です。LogHashもログ本体への参照であり、直接クエリされることは稀です。
  • デノーマライズ化: Commit構造体のResult []stringフィールドは、関連するResultエンティティのData()文字列を直接格納することで、データをデノーマライズしています。
    // 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"`
    
    これにより、特定のコミットのビルド結果の概要を、追加のDatastoreクエリなしでCommitエンティティから直接取得できるようになり、読み込みパフォーマンスが向上します。完全な詳細が必要な場合は、個別のResultエンティティをクエリします。
  • Log構造体: ビルドログは、そのサイズが大きくなる可能性があるため、CompressedLog []byteとして別途Logエンティティに保存されます。これにより、ログの保存と取得が効率的に行われ、メインのResultエンティティのサイズを小さく保つことができます。

HTTPハンドラの機能

定義されたHTTPハンドラは、GoダッシュボードのCIシステムとのインタラクションを可能にします。

  • commitHandlerresultHandlerは、gobuilderプロセスからの新しいコミット情報とビルド結果を受け取り、Datastoreに永続化します。
  • tagHandlerは、Goのバージョンタグ(weekly, release, tip)の更新を処理します。
  • todoHandlerは、特定のビルダーが次にビルドすべきコミットを効率的に特定するためのAPIを提供します。これは、CIシステムがビルドキューを管理する上で不可欠な機能です。
  • AuthHandlerは、これらのAPIエンドポイントへのアクセスを保護し、認証されたgobuilderプロセスのみがデータを送信できるようにします。

これらのデータ構造とハンドラの組み合わせにより、Goダッシュボードは、Goプロジェクトの継続的な開発とテストのプロセスから生成される大量のデータを、スケーラブルかつ整合性を持って管理できるようになります。

関連リンク

参考にした情報源リンク

  • Google App Engine Documentation (Go): (当時のドキュメントは直接リンクできませんが、App EngineのGoランタイムとDatastoreに関する公式ドキュメントが参考になります)
  • Google Cloud Datastore Documentation: (当時のDatastoreの概念に関する公式ドキュメントが参考になります)
  • Go言語公式ドキュメント: https://go.dev/
  • 継続的インテグレーション (CI) の概念: (一般的なCIに関する情報源)