[インデックス 10998] ファイルの概要
このコミットは、Goダッシュボードアプリケーションにおけるキャッシュメカニズムの大幅なリファクタリングと改善を目的としています。具体的には、既存のbuild
パッケージ内に散在していたキャッシュ関連のロジックをcache
という新しい独立したパッケージに集約し、より汎用的で再利用可能なキャッシュヘルパーを導入しています。これにより、キャッシュの管理が中央集権化され、コードの可読性と保守性が向上しています。また、新しいキャッシュ戦略として「論理時刻」に基づいたキャッシュ無効化メカニズムが導入されており、キャッシュの一貫性と効率性が高められています。
コミット
commit 5a65cbacd3112f017d224196027e1ac1b358fa7a
Author: Andrew Gerrand <adg@golang.org>
Date: Fri Dec 23 14:44:56 2011 +1100
dashboard: cache packages, introduce caching helpers
R=rsc, gary.burd, adg
CC=golang-dev
https://golang.org/cl/5498067
---
misc/dashboard/app/build/build.go | 5 +-
misc/dashboard/app/build/cache.go | 122 ------------------------------------\n misc/dashboard/app/build/handler.go | 41 +++++++-----\n misc/dashboard/app/build/ui.go | 31 ++++-----\n misc/dashboard/app/cache/cache.go | 82 ++++++++++++++++++++++++\n 5 files changed, 124 insertions(+), 157 deletions(-)
diff --git a/misc/dashboard/app/build/build.go b/misc/dashboard/app/build/build.go
index e7edd7831e..175812a378 100644
--- a/misc/dashboard/app/build/build.go
+++ b/misc/dashboard/app/build/build.go
@@ -5,8 +5,6 @@
package build
import (
- "appengine"
- "appengine/datastore"
"bytes"
"compress/gzip"
"crypto/sha1"
@@ -15,6 +13,9 @@ import (
"io/ioutil"
"os"
"strings"
+
+ "appengine"
+ "appengine/datastore"
)
const maxDatastoreStringLen = 500
diff --git a/misc/dashboard/app/build/cache.go b/misc/dashboard/app/build/cache.go
deleted file mode 100644
index 799a9c11ae..0000000000
--- a/misc/dashboard/app/build/cache.go
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright 2011 The Go Authors. All rights reserved.\n-// Use of this source code is governed by a BSD-style\n-// license that can be found in the LICENSE file.\n-\n-package build\n-\n-import (\n-\t"appengine"\n-\t"appengine/memcache"\n-\t"json"\n-\t"os"\n-)\n-\n-const (\n-\ttodoCacheKey = "build-todo"\n-\ttodoCacheExpiry = 3600 // 1 hour in seconds\n-\tuiCacheKey = "build-ui"\n-\tuiCacheExpiry = 10 * 60 // 10 minutes in seconds\n-)\n-\n-// invalidateCache deletes the build cache records from memcache.\n-// This function should be called whenever the datastore changes.\n-func invalidateCache(c appengine.Context) {\n-\tkeys := []string{uiCacheKey, todoCacheKey}\n-\terrs := memcache.DeleteMulti(c, keys)\n-\tfor i, err := range errs {\n-\t\tif err != nil && err != memcache.ErrCacheMiss {\n-\t\t\tc.Errorf("memcache.Delete(%q): %v", keys[i], err)\n-\t\t}\n-\t}\n-}\n-\n-// cachedTodo gets the specified todo cache entry (if it exists) from the\n-// shared todo cache.\n-func cachedTodo(c appengine.Context, todoKey string) (todo *Todo, ok bool) {\n-\tt := todoCache(c)\n-\tif t == nil {\n-\t\treturn nil, false\n-\t}\n-\ttodos := unmarshalTodo(c, t)\n-\tif todos == nil {\n-\t\treturn nil, false\n-\t}\n-\ttodo, ok = todos[todoKey]\n-\treturn\n-}\n-\n-// cacheTodo puts the provided todo cache entry into the shared todo cache.\n-// The todo cache is a JSON-encoded map[string]*Todo, where the key is todoKey.\n-func cacheTodo(c appengine.Context, todoKey string, todo *Todo) {\n-\t// Get the todo cache record (or create a new one).\n-\tnewItem := false\n-\tt := todoCache(c)\n-\tif t == nil {\n-\t\tnewItem = true\n-\t\tt = &memcache.Item{\n-\t\t\tKey: todoCacheKey,\n-\t\t\tValue: []byte("{}"), // default is an empty JSON object\n-\t\t}\n-\t}\n-\n-\t// Unmarshal the JSON value.\n-\ttodos := unmarshalTodo(c, t)\n-\tif todos == nil {\n-\t\treturn\n-\t}\n-\n-\t// Update the map.\n-\ttodos[todoKey] = todo\n-\n-\t// Marshal the updated JSON value.\n-\tvar err os.Error\n-\tt.Value, err = json.Marshal(todos)\n-\tif err != nil {\n-\t\t// This shouldn't happen.\n-\t\tc.Criticalf("marshal todo cache: %v", err)\n-\t\treturn\n-\t}\n-\n-\t// Set a new expiry.\n-\tt.Expiration = todoCacheExpiry\n-\n-\t// Update the cache record (or Set it, if new).\n-\tif newItem {\n-\t\terr = memcache.Set(c, t)\n-\t} else {\n-\t\terr = memcache.CompareAndSwap(c, t)\n-\t}\n-\tif err == memcache.ErrCASConflict || err == memcache.ErrNotStored {\n-\t\t// No big deal if it didn't work; it should next time.\n-\t\tc.Warningf("didn't update todo cache: %v", err)\n-\t} else if err != nil {\n-\t\tc.Errorf("update todo cache: %v", err)\n-\t}\n-}\n-\n-// todoCache gets the todo cache record from memcache (if it exists).\n-func todoCache(c appengine.Context) *memcache.Item {\n-\tt, err := memcache.Get(c, todoCacheKey)\n-\tif err != nil {\n-\t\tif err != memcache.ErrCacheMiss {\n-\t\t\tc.Errorf("get todo cache: %v", err)\n-\t\t}\n-\t\treturn nil\n-\t}\n-\treturn t\n-}\n-\n-// unmarshalTodo decodes the given item's memcache value into a map.\n-func unmarshalTodo(c appengine.Context, t *memcache.Item) map[string]*Todo {\n-\ttodos := make(map[string]*Todo)\n-\tif err := json.Unmarshal(t.Value, &todos); err != nil {\n-\t\t// This shouldn't happen.\n-\t\tc.Criticalf("unmarshal todo cache: %v", err)\n-\t\t// Kill the bad record.\n-\t\tif err := memcache.Delete(c, todoCacheKey); err != nil {\n-\t\t\tc.Errorf("delete todo cache: %v", err)\n-\t\t}\n-\t\treturn nil\n-\t}\n-\treturn todos\n-}\ndiff --git a/misc/dashboard/app/build/handler.go b/misc/dashboard/app/build/handler.go
index eba8d0eaf6..b44e800453 100644
--- a/misc/dashboard/app/build/handler.go
+++ b/misc/dashboard/app/build/handler.go
@@ -5,13 +5,15 @@
package build
import (
- "appengine"
- "appengine/datastore"
"crypto/hmac"
"fmt"
"http"
"json"
"os"
+
+ "appengine"
+ "appengine/datastore"
+ "cache"
)
const commitsPerPage = 30
@@ -58,7 +60,7 @@ func commitHandler(r *http.Request) (interface{}, os.Error) {
if err := com.Valid(); err != nil {
return nil, fmt.Errorf("validating Commit: %v", err)
}\n- defer invalidateCache(c)\n+ defer cache.Tick(c)\n tx := func(c appengine.Context) os.Error {
return addCommit(c, com)
}\n@@ -132,7 +134,7 @@ func tagHandler(r *http.Request) (interface{}, os.Error) {
return nil, err
}\n c := appengine.NewContext(r)\n- defer invalidateCache(c)\n+ defer cache.Tick(c)\n _, err := datastore.Put(c, t.Key(c), t)\n return nil, err
}\n@@ -148,14 +150,12 @@ type Todo struct {
// Multiple "kind" parameters may be specified.\n func todoHandler(r *http.Request) (interface{}, os.Error) {
c := appengine.NewContext(r)\n-\n-\ttodoKey := r.Form.Encode()\n-\tif t, ok := cachedTodo(c, todoKey); ok {\n-\t\tc.Debugf("cache hit")\n-\t\treturn t, nil\n+\tnow := cache.Now(c)\n+\tkey := "build-todo-" + r.Form.Encode()\n+\tcachedTodo := new(Todo)\n+\tif cache.Get(r, now, key, cachedTodo) {\n+\t\treturn cachedTodo, nil\n \t}\n-\tc.Debugf("cache miss")\n-\n \tvar todo *Todo\n \tvar err os.Error\n \tbuilder := r.FormValue("builder")\n@@ -175,7 +175,7 @@ func todoHandler(r *http.Request) (interface{}, os.Error) {\n \t\t}\n \t}\n \tif err == nil {\n-\t\tcacheTodo(c, todoKey, todo)\n+\t\tcache.Set(r, now, key, todo)\n \t}\n \treturn todo, err\n }\n@@ -218,7 +218,19 @@ func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interf\n // packagesHandler returns a list of the non-Go Packages monitored\n // by the dashboard.\n func packagesHandler(r *http.Request) (interface{}, os.Error) {\n-\treturn Packages(appengine.NewContext(r))\n+\tc := appengine.NewContext(r)\n+\tnow := cache.Now(c)\n+\tconst key = "build-packages"\n+\tvar p []*Package\n+\tif cache.Get(r, now, key, &p) {\n+\t\treturn p, nil\n+\t}\n+\tp, err := Packages(c)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\tcache.Set(r, now, key, p)\n+\treturn p, nil\n }\n \n // resultHandler records a build result.\n@@ -240,7 +252,7 @@ func resultHandler(r *http.Request) (interface{}, os.Error) {\n \tif err := res.Valid(); err != nil {\n \t\treturn nil, fmt.Errorf("validating Result: %v", err)\n \t}\n-\tdefer invalidateCache(c)\n+\tdefer cache.Tick(c)\n \t// store the Log text if supplied\n \tif len(res.Log) > 0 {\n \t\thash, err := PutLog(c, res.Log)\n@@ -347,6 +359,7 @@ func AuthHandler(h dashHandler) http.HandlerFunc {\n func initHandler(w http.ResponseWriter, r *http.Request) {\n \t// TODO(adg): devise a better way of bootstrapping new packages\n \tc := appengine.NewContext(r)\n+\tdefer cache.Tick(c)\n \tfor _, p := range defaultPackages {\n \t\tif err := datastore.Get(c, p.Key(c), new(Package)); err == nil {\n \t\t\tcontinue\ndiff --git a/misc/dashboard/app/build/ui.go b/misc/dashboard/app/build/ui.go
index 0b55aa2396..032fdbd84e 100644
--- a/misc/dashboard/app/build/ui.go
+++ b/misc/dashboard/app/build/ui.go
@@ -8,9 +8,6 @@
package build
import (
-\t"appengine"
-\t"appengine/datastore"
-\t"appengine/memcache"
"bytes"
"exp/template/html"
"http"
@@ -20,6 +17,10 @@ import (
"strconv"
"strings"
"template"
+\n+\t"appengine"\n+\t"appengine/datastore"\n+\t"cache"\n )
func init() {
@@ -30,6 +31,8 @@ func init() {
// uiHandler draws the build status page.\n func uiHandler(w http.ResponseWriter, r *http.Request) {\n \tc := appengine.NewContext(r)\n+\tnow := cache.Now(c)\n+\tconst key = "build-ui"\n \n \tpage, _ := strconv.Atoi(r.FormValue("page"))\n \tif page < 0 {\n@@ -37,15 +40,12 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {\n \t}\n \n \t// Used cached version of front page, if available.\n-\tif page == 0 && r.Host == "build.golang.org" {\n-\t\tt, err := memcache.Get(c, uiCacheKey)\n-\t\tif err == nil {\n-\t\t\tw.Write(t.Value)\n+\tif page == 0 {\n+\t\tvar b []byte\n+\t\tif cache.Get(r, now, key, &b) {\n+\t\t\tw.Write(b)\n \t\t\treturn\n \t\t}\n-\t\tif err != memcache.ErrCacheMiss {\n-\t\t\tc.Errorf("get ui cache: %v", err)\n-\t\t}\n \t}\n \n \tcommits, err := goCommits(c, page)\n@@ -78,15 +78,8 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {\n \t}\n \n \t// Cache the front page.\n-\tif page == 0 && r.Host == "build.golang.org" {\n-\t\tt := &memcache.Item{\n-\t\t\tKey: uiCacheKey,\n-\t\t\tValue: buf.Bytes(),\n-\t\t\tExpiration: uiCacheExpiry,\n-\t\t}\n-\t\tif err := memcache.Set(c, t); err != nil {\n-\t\t\tc.Errorf("set ui cache: %v", err)\n-\t\t}\n+\tif page == 0 {\n+\t\tcache.Set(r, now, key, buf.Bytes())\n \t}\n \n \tbuf.WriteTo(w)\ndiff --git a/misc/dashboard/app/cache/cache.go b/misc/dashboard/app/cache/cache.go
new file mode 100644
index 0000000000..d290ed416c
--- /dev/null
+++ b/misc/dashboard/app/cache/cache.go
@@ -0,0 +1,82 @@
+// Copyright 2011 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+package cache\n+\n+import (\n+\t"fmt"\n+\t"http"\n+\t"time"\n+\n+\t"appengine"\n+\t"appengine/memcache"\n+)\n+\n+const (\n+\tnocache = "nocache"\n+\ttimeKey = "cachetime"\n+\texpiry = 600 // 10 minutes\n+)\n+\n+func newTime() uint64 { return uint64(time.Seconds()) << 32 }\n+\n+// Now returns the current logical datastore time to use for cache lookups.\n+func Now(c appengine.Context) uint64 {\n+\tt, err := memcache.Increment(c, timeKey, 0, newTime())\n+\tif err != nil {\n+\t\tc.Errorf("cache.Now: %v", err)\n+\t\treturn 0\n+\t}\n+\treturn t\n+}\n+\n+// Tick sets the current logical datastore time to a never-before-used time\n+// and returns that time. It should be called to invalidate the cache.\n+func Tick(c appengine.Context) uint64 {\n+\tt, err := memcache.Increment(c, timeKey, 1, newTime())\n+\tif err != nil {\n+\t\tc.Errorf("cache.Tick: %v", err)\n+\t\treturn 0\n+\t}\n+\treturn t\n+}\n+\n+// Get fetches data for name at time now from memcache and unmarshals it into\n+// value. It reports whether it found the cache record and logs any errors to\n+// the admin console.\n+func Get(r *http.Request, now uint64, name string, value interface{}) bool {\n+\tif now == 0 || r.FormValue(nocache) != "" {\n+\t\treturn false\n+\t}\n+\tc := appengine.NewContext(r)\n+\tkey := fmt.Sprintf("%s.%d", name, now)\n+\t_, err := memcache.JSON.Get(c, key, value)\n+\tif err == nil {\n+\t\tc.Debugf("cache hit %q", key)\n+\t\treturn true\n+\t}\n+\tc.Debugf("cache miss %q", key)\n+\tif err != memcache.ErrCacheMiss {\n+\t\tc.Errorf("get cache %q: %v", key, err)\n+\t}\n+\treturn false\n+}\n+\n+// Set puts value into memcache under name at time now.\n+// It logs any errors to the admin console.\n+func Set(r *http.Request, now uint64, name string, value interface{}) {\n+\tif now == 0 || r.FormValue(nocache) != "" {\n+\t\treturn\n+\t}\n+\tc := appengine.NewContext(r)\n+\tkey := fmt.Sprintf("%s.%d", name, now)\n+\terr := memcache.JSON.Set(c, &memcache.Item{\n+\t\tKey: key,\n+\t\tObject: value,\n+\t\tExpiration: expiry,\n+\t})\n+\tif err != nil {\n+\t\tc.Errorf("set cache %q: %v", key, err)\n+\t}\n+}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/5a65cbacd3112f017d224196027e1ac1b358fa7a](https://github.com/golang/go/commit/5a65cbacd3112f017d224196027e1ac1b358fa7a)
## 元コミット内容
dashboard: cache packages, introduce caching helpers
R=rsc, gary.burd, adg CC=golang-dev https://golang.org/cl/5498067
## 変更の背景
このコミットの主な背景は、Goダッシュボードアプリケーションにおけるキャッシュ管理の改善と効率化です。以前は、`misc/dashboard/app/build/cache.go`ファイルにキャッシュ関連のロジックが直接実装されており、特定のデータ型(`Todo`やUIデータ)に特化した形でキャッシュの読み書きや無効化が行われていました。
しかし、このような実装は以下のような課題を抱えていました。
1. **コードの重複と保守性の低下**: 複数の場所で同様のキャッシュロジックが記述される可能性があり、コードの重複や保守性の低下を招きます。
2. **汎用性の欠如**: 特定のデータ型に依存したキャッシュ実装では、新しい種類のデータをキャッシュする際に同様のロジックを再度記述する必要があり、拡張性に欠けます。
3. **キャッシュ無効化の複雑さ**: データストアの変更時にキャッシュを無効化する`invalidateCache`関数は、キャッシュキーを直接指定する必要があり、管理が煩雑になる可能性があります。
これらの課題を解決するため、このコミットではキャッシュ機能を独立した`cache`パッケージとして切り出し、より汎用的で再利用可能なキャッシュヘルパーを提供することで、ダッシュボード全体のキャッシュ戦略を改善しています。これにより、アプリケーションのパフォーマンス向上だけでなく、コードベースの健全性と将来的な拡張性が確保されます。
## 前提知識の解説
このコミットを理解するためには、以下の技術的知識が役立ちます。
### 1. Go App Engine (GAE)
Google App Engineは、Googleが提供するPaaS(Platform as a Service)であり、ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。Go言語はApp Engineでサポートされている言語の一つです。App Engine環境では、アプリケーションはGoogleのインフラ上で動作し、スケーラビリティや信頼性が提供されます。
### 2. Google App Engine Datastore
Datastoreは、Google App Engineで利用できるNoSQLドキュメントデータベースです。半構造化データを格納するために設計されており、高いスケーラビリティと可用性を提供します。アプリケーションはDatastoreにデータを永続的に保存し、必要に応じて取得します。
### 3. Google App Engine Memcache
Memcacheは、Google App Engineで利用できる分散型インメモリキャッシュサービスです。頻繁にアクセスされるデータを一時的にメモリに保存することで、Datastoreなどの永続ストレージへのアクセス回数を減らし、アプリケーションの応答速度を向上させます。Memcacheはキーと値のペアを保存し、高速な読み書きが可能です。ただし、Memcacheに保存されたデータは一時的なものであり、メモリ不足やサーバーの再起動などによりいつでも削除される可能性があるため、Memcacheは「真の情報源(source of truth)」としてではなく、あくまでパフォーマンス向上のためのキャッシュとして利用すべきです。
### 4. キャッシュの無効化戦略
キャッシュはアプリケーションのパフォーマンスを向上させますが、キャッシュされたデータが古くなる(Stale Cache)と問題が発生します。そのため、データが更新された際にはキャッシュを適切に無効化(Invalidation)し、最新のデータが取得されるようにする必要があります。一般的なキャッシュ無効化戦略には以下のようなものがあります。
* **Time-based Invalidation (TTL)**: キャッシュエントリに有効期限(Time To Live, TTL)を設定し、期限が切れたら自動的に無効化する。
* **Event-based Invalidation**: データが更新された際に、明示的にキャッシュエントリを削除または更新する。
* **Version-based / Logical Time Invalidation**: データにバージョン番号やタイムスタンプを付与し、キャッシュされたデータのバージョンと最新のデータのバージョンを比較して、キャッシュが古いかどうかを判断する。このコミットで採用されているのは、この「論理時刻」に基づいた無効化戦略の一種です。
### 5. `memcache.Increment`
Go App Engineの`memcache`パッケージには、キーに関連付けられた数値をアトミックにインクリメント(増加)させる`Increment`関数があります。この関数は、分散環境下で競合状態を避けてカウンタを更新するのに非常に有用です。このコミットでは、この`Increment`関数を応用して、キャッシュの「論理時刻」を管理しています。
### 6. `fmt.Sprintf`
Go言語の`fmt`パッケージに含まれる`Sprintf`関数は、フォーマット指定子(例: `%s`, `%d`)を使用して文字列を整形し、その結果を文字列として返します。このコミットでは、キャッシュキーを生成する際に、キャッシュ名と論理時刻を組み合わせて一意のキーを作成するために使用されています。
### 7. ビットシフト演算子 (`<<`)
`<<`は左ビットシフト演算子です。`A << B`は、`A`のビットを`B`だけ左にシフトすることを意味します。これは`A * 2^B`と同じ効果を持ちます。このコミットでは、`newTime()`関数で`uint64(time.Seconds()) << 32`という形で使用されており、これは秒単位のタイムスタンプを上位32ビットに配置し、下位32ビットを空けることで、将来的に追加の情報を格納できるような構造を意図している可能性があります。
## 技術的詳細
このコミットの最も重要な技術的変更は、新しい`misc/dashboard/app/cache/cache.go`ファイルで導入された、**論理時刻に基づいたキャッシュ無効化ヘルパー**です。
### 論理時刻キャッシュ戦略の概要
従来のキャッシュ無効化は、データストアの変更時に特定のキャッシュキーを直接削除する`invalidateCache`関数に依存していました。しかし、新しいアプローチでは、`memcache`に保存された単一のカウンタ(`timeKey`)を「論理時刻」として利用します。
1. **`Now()`関数**: 現在の論理時刻を取得します。これは`memcache.Increment(c, timeKey, 0, newTime())`を呼び出すことで実現されます。`Increment`の第2引数が`0`であるため、カウンタの値は変更されず、現在の値が返されます。もし`timeKey`が存在しない場合は、`newTime()`で生成された初期値が設定されます。
2. **`Tick()`関数**: キャッシュを無効化するために論理時刻を進めます。これは`memcache.Increment(c, timeKey, 1, newTime())`を呼び出すことで実現されます。`Increment`の第2引数が`1`であるため、`timeKey`のカウンタが1増加します。このカウンタの増加が、すべてのキャッシュエントリを「古く」します。
3. **キャッシュキーの生成**: `Get`および`Set`関数では、実際のキャッシュキーとして`fmt.Sprintf("%s.%d", name, now)`という形式を使用します。ここで`name`はキャッシュされるデータの種類(例: "build-todo")、`now`は`Now()`関数で取得した現在の論理時刻です。これにより、キャッシュエントリは特定のデータ名と、そのデータがキャッシュされた時点の論理時刻の組み合わせで一意に識別されます。
### キャッシュの読み書き (`Get`と`Set`)
* **`Get(r *http.Request, now uint64, name string, value interface{}) bool`**:
* `now`が`0`の場合、またはHTTPリクエストのフォーム値に`nocache`パラメータが含まれる場合(デバッグ目的など)、キャッシュは利用されず`false`を返します。
* `fmt.Sprintf("%s.%d", name, now)`で生成されたキーを使用して`memcache.JSON.Get`を呼び出し、キャッシュされたデータを取得します。
* キャッシュヒットした場合、`true`を返します。キャッシュミスまたはエラーの場合、`false`を返します。
* **`Set(r *http.Request, now uint64, name string, value interface{})`**:
* `now`が`0`の場合、またはHTTPリクエストのフォーム値に`nocache`パラメータが含まれる場合、キャッシュは行われません。
* `fmt.Sprintf("%s.%d", name, now)`で生成されたキーを使用して`memcache.JSON.Set`を呼び出し、データをキャッシュに保存します。
* キャッシュエントリには`expiry`(10分)が設定されます。
### 論理時刻による無効化の仕組み
この戦略の鍵は、データが更新されるたびに`Tick()`が呼び出され、`timeKey`のカウンタがインクリメントされる点です。これにより、`Now()`が返す論理時刻が新しい値になります。
例えば、あるデータが論理時刻`T1`でキャッシュされたとします。その後、そのデータが更新され、`Tick()`が呼び出されると、論理時刻は`T2`(`T1`より大きい)になります。次にそのデータを取得しようとすると、`Get`関数は新しい論理時刻`T2`を使用してキャッシュキー(例: "build-todo.T2")を検索します。しかし、キャッシュには"build-todo.T1"しか存在しないため、キャッシュミスとなり、最新のデータがデータストアから取得され、新しい論理時刻`T2`でキャッシュされます。
このアプローチは、特定のキャッシュエントリを明示的に削除することなく、データストアの変更と同期してキャッシュを「無効化」できるため、非常に効率的です。また、`memcache.Increment`がアトミックな操作であるため、複数のインスタンスからの同時更新にも対応できます。
## コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
1. **`misc/dashboard/app/build/cache.go` (削除)**:
* 以前のキャッシュロジックが記述されていたファイルが完全に削除されました。これには、`invalidateCache`, `cachedTodo`, `cacheTodo`などの関数が含まれていました。
2. **`misc/dashboard/app/cache/cache.go` (新規追加)**:
* 新しい汎用的なキャッシュヘルパーがこのファイルに実装されました。
* `Now()`, `Tick()`, `Get()`, `Set()`といった関数が定義され、論理時刻に基づいたキャッシュ戦略が導入されています。
3. **`misc/dashboard/app/build/handler.go`**:
* `cache`パッケージがインポートされました。
* `commitHandler`, `tagHandler`, `resultHandler`, `initHandler`などのデータストアを更新するハンドラ関数内で、`defer invalidateCache(c)`が`defer cache.Tick(c)`に置き換えられました。これにより、データ更新時に新しい汎用キャッシュ無効化メカニズムがトリガーされるようになりました。
* `todoHandler`では、以前の`cachedTodo`と`cacheTodo`の呼び出しが、新しい`cache.Get`と`cache.Set`に置き換えられました。キャッシュキーの生成も新しい形式(`"build-todo-" + r.Form.Encode()`と`now`の組み合わせ)に変更されています。
* `packagesHandler`にも同様に`cache.Get`と`cache.Set`を使用したキャッシュロジックが追加されました。
4. **`misc/dashboard/app/build/ui.go`**:
* `cache`パッケージがインポートされました。
* `uiHandler`内で、以前の`memcache.Get`と`memcache.Set`によるUIキャッシュの処理が、新しい`cache.Get`と`cache.Set`に置き換えられました。キャッシュキーの生成も新しい形式(`"build-ui"`と`now`の組み合わせ)に変更されています。
5. **`misc/dashboard/app/build/build.go`**:
* インポート文の順序が変更されましたが、機能的な変更はありません。
## コアとなるコードの解説
新しく追加された`misc/dashboard/app/cache/cache.go`ファイルに実装された主要な関数について詳しく解説します。
### `newTime() uint64`
```go
func newTime() uint64 { return uint64(time.Seconds()) << 32 }
この関数は、現在のUnixタイムスタンプ(秒単位)を取得し、それをuint64
型にキャストした後、32ビット左にシフトします。これにより、タイムスタンプがuint64
値の上位32ビットに配置され、下位32ビットはゼロで埋められます。この設計は、将来的に下位32ビットに追加の情報を格納するための余地を残している可能性があります。
Now(c appengine.Context) uint64
func Now(c appengine.Context) uint64 {
t, err := memcache.Increment(c, timeKey, 0, newTime())
if err != nil {
c.Errorf("cache.Now: %v", err)
return 0
}
return t
}
この関数は、現在の「論理時刻」を取得します。
memcache.Increment(c, timeKey, 0, newTime())
を呼び出します。timeKey
は、Memcacheに保存されている論理時刻のカウンタのキーです。- インクリメント値が
0
であるため、timeKey
の値は変更されません。 newTime()
は、timeKey
がMemcacheに存在しない場合に設定される初期値です。
- この関数は、
timeKey
の現在の値(つまり論理時刻)を返します。エラーが発生した場合はログに記録し、0
を返します。
Tick(c appengine.Context) uint64
func Tick(c appengine.Context) uint64 {
t, err := memcache.Increment(c, timeKey, 1, newTime())
if err != nil {
c.Errorf("cache.Tick: %v", err)
return 0
}
return t
}
この関数は、キャッシュを無効化するために「論理時刻」を進めます。
memcache.Increment(c, timeKey, 1, newTime())
を呼び出します。- インクリメント値が
1
であるため、timeKey
のカウンタが1増加します。
- インクリメント値が
- このカウンタの増加により、`Now