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

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

コミット

commit 634f0edabce65774a5e9dbd963b7b67a07d4bd62
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Dec 20 15:30:11 2011 +1100

    dashboard: todo sends full Commit with Kind field
    
    Permits us to implement other Kinds of todo instruction in the future.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5495087

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

https://github.com/golang/go/commit/634f0edabce65774a5e9dbd963b7b67a07d4bd62

元コミット内容

このコミットは、Goプロジェクトのダッシュボードシステムのtodoハンドラーを大幅に改善し、拡張可能なアーキテクチャを導入しました。変更対象となったファイルは以下の通りです:

  • misc/dashboard/app/build/build.go (56行変更、主要な実装)
  • misc/dashboard/app/build/test.go (66行変更、テストケースの更新)
  • misc/dashboard/builder/http.go (21行変更、HTTPクライアント実装)
  • misc/dashboard/builder/main.go (4行変更、メイン処理の更新)

変更の背景

2011年のGoプロジェクトの継続的インテグレーション(CI)システムでは、ビルドジョブの管理を行うためのダッシュボードシステムが運用されていました。このシステムは、複数のプラットフォーム(linux-386、linux-amd64など)でのビルドを調整し、次にビルドすべきコミットを特定する重要な役割を担っていました。

従来のシステムでは、todoハンドラーは単純にコミットハッシュの文字列を返すだけでした。しかし、プロジェクトの規模拡大に伴い、以下の課題が顕在化していました:

  1. ビルドタイプの多様化: Go本体のビルドだけでなく、外部パッケージのビルドも必要となっていた
  2. 拡張性の欠如: 新しいタイプのビルドジョブを追加する際の柔軟性が不足
  3. 情報の不十分さ: ビルダーが必要とする詳細な情報を提供できていない

これらの問題を解決するため、Andrew Gerrandは構造化されたレスポンス形式を導入することを決定しました。

前提知識の解説

Google App Engine アーキテクチャ(2011年)

このシステムはGoogle App Engine上で動作しており、2011年当時のApp Engineの特徴を理解することが重要です:

  • App Engineコンテキスト: appengine.NewContext(r)を使用してリクエストごとのコンテキストを作成
  • Datastoreアクセス: App Engineのデータストアを使用してCommitとResultデータを永続化
  • HTTPハンドラー: 標準のHTTPハンドラーパターンを使用
  • JSONレスポンス: レスポンスはJSONシリアライゼーションを通じて送信

Go初期のビルドシステム

2011年のGoプロジェクトでは、以下のような分散ビルドシステムが構築されていました:

  1. 中央ダッシュボード: App Engine上でホストされ、ビルドジョブを調整
  2. 分散ビルダー: 各プラットフォームで動作するビルダーが中央ダッシュボードにタスクを問い合わせ
  3. 結果レポート: ビルド結果を中央ダッシュボードに送信

従来のワークフロー

従来のシステムでは、ビルダーは以下のような単純なワークフローを実行していました:

1. ビルダーが /todo エンドポイントにリクエスト
2. ダッシュボードがビルドすべきコミットハッシュを返却
3. ビルダーがそのコミットをビルド
4. ビルダーが結果を /result エンドポイントに送信

技術的詳細

新しいTodo構造体の導入

最も重要な変更は、新しいTodo構造体の導入です:

type Todo struct {
    Kind string // "build-go-commit" or "build-package"
    Data interface{}
}

この構造体により、以下の利点が得られます:

  1. 型安全性: 明確な構造によりデータの型が保証される
  2. 拡張性: 新しいKindを追加することで、異なるタイプのタスクを処理可能
  3. 情報の豊富さ: Dataフィールドに完全なCommitオブジェクトを格納可能

todoHandlerの完全な刷新

従来のtodoHandlerは以下のような単純な実装でした:

func todoHandler(r *http.Request) (interface{}, os.Error) {
    builder := r.FormValue("builder")
    // ... 処理 ...
    return com.Hash, nil  // 単純にハッシュを返却
}

新しい実装では、複数のKindを処理できる柔軟な仕組みが導入されました:

func todoHandler(r *http.Request) (todo interface{}, err os.Error) {
    c := appengine.NewContext(r)
    builder := r.FormValue("builder")
    for _, kind := range r.Form["kind"] {
        // 各Kindに対して適切な処理を実行
        switch kind {
        case "build-go-commit":
            data, err = buildTodo(c, builder, "", "")
        case "build-package":
            data, err = buildTodo(c, builder,
                r.FormValue("packagePath"),
                r.FormValue("goHash"))
        }
        if data != nil || err != nil {
            return &Todo{Kind: kind, Data: data}, err
        }
    }
    return nil, nil
}

buildTodo関数の分離

ビルドロジックは新しいbuildTodo関数に分離されました:

func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interface{}, os.Error) {
    // 共通のビルドロジックを実装
    // 戻り値が変更: com.Hash → com(完全なCommitオブジェクト)
    return com, nil
}

この変更により、コードの再利用性が向上し、保守性が大幅に改善されました。

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

1. build.go: 40-70行目 - 新しいTodo構造体とhandler

// Todo is a todoHandler response.
type Todo struct {
    Kind string // "build-go-commit" or "build-package"
    Data interface{}
}

// todoHandler returns the next action to be performed by a builder.
func todoHandler(r *http.Request) (todo interface{}, err os.Error) {
    c := appengine.NewContext(r)
    builder := r.FormValue("builder")
    for _, kind := range r.Form["kind"] {
        var data interface{}
        switch kind {
        case "build-go-commit":
            data, err = buildTodo(c, builder, "", "")
        case "build-package":
            data, err = buildTodo(c, builder,
                r.FormValue("packagePath"),
                r.FormValue("goHash"))
        }
        if data != nil || err != nil {
            return &Todo{Kind: kind, Data: data}, err
        }
    }
    return nil, nil
}

2. build.go: 90行目 - 戻り値の変更

// 従来: return com.Hash, nil
// 新版: return com, nil
return com, nil

3. http.go: 224-251行目 - クライアント側の対応

func (b *Builder) todo(kind, pkg, goHash string) (rev string, err error) {
    args := url.Values{
        "kind":        {kind},
        "builder":     {b.name},
        "packagePath": {pkg},
        "goHash":      {goHash},
    }
    var resp *struct {
        Kind string
        Data struct {
            Hash string
        }
    }
    // ... エラーハンドリング ...
    return resp.Data.Hash, nil
}

4. main.go: 264, 273行目 - 呼び出し側の更新

// 従来: hash, err := b.todo("", "")
// 新版: hash, err := b.todo("build-go-commit", "", "")

// 従来: hash, err := b.todo(pkg, goHash)
// 新版: hash, err := b.todo("build-package", pkg, goHash)

コアとなるコードの解説

アーキテクチャパターンの採用

このコミットでは、以下のソフトウェアアーキテクチャパターンが採用されています:

  1. Strategy Pattern: Kindフィールドにより異なるビルドストラテジーを選択
  2. Template Method Pattern: buildTodo関数が共通の処理フローを定義
  3. Data Transfer Object Pattern: Todo構造体がデータ転送オブジェクトとして機能

後方互換性の考慮

APIの変更は破壊的変更でしたが、以下の理由により正当化されます:

  1. 内部システム: これは内部の継続的インテグレーションシステムであり、外部APIではない
  2. 同期デプロイ: サーバーとクライアントの両方を同時に更新可能
  3. テストカバレッジ: 包括的なテストが変更の安全性を保証

エラーハンドリングの改善

新しい実装では、エラーハンドリングがより堅牢になりました:

if kind != resp.Kind {
    return "", fmt.Errorf("expecting Kind %q, got %q", kind, resp.Kind)
}

このチェックにより、クライアントとサーバー間の契約が確実に守られます。

テストの包括性

test.goファイルの変更は、新しいAPIに対する包括的なテストカバレッジを提供しています:

// 従来のテスト
{"/todo", url.Values{"builder": {"linux-386"}}, nil, "0003"},

// 新しいテスト
{"/todo", url.Values{"kind": {"build-go-commit"}, "builder": {"linux-386"}}, 
 nil, &Todo{Kind: "build-go-commit", Data: &Commit{Hash: "0003"}}},

関連リンク

参考にした情報源リンク