Commit 5a65cbac authored by Andrew Gerrand's avatar Andrew Gerrand

dashboard: cache packages, introduce caching helpers

R=rsc, gary.burd, adg
CC=golang-dev
https://golang.org/cl/5498067
parent 9c2e0b75
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
package build package build
import ( import (
"appengine"
"appengine/datastore"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha1" "crypto/sha1"
...@@ -15,6 +13,9 @@ import ( ...@@ -15,6 +13,9 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"appengine"
"appengine/datastore"
) )
const maxDatastoreStringLen = 500 const maxDatastoreStringLen = 500
......
// 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/memcache"
"json"
"os"
)
const (
todoCacheKey = "build-todo"
todoCacheExpiry = 3600 // 1 hour in seconds
uiCacheKey = "build-ui"
uiCacheExpiry = 10 * 60 // 10 minutes in seconds
)
// invalidateCache deletes the build cache records from memcache.
// This function should be called whenever the datastore changes.
func invalidateCache(c appengine.Context) {
keys := []string{uiCacheKey, todoCacheKey}
errs := memcache.DeleteMulti(c, keys)
for i, err := range errs {
if err != nil && err != memcache.ErrCacheMiss {
c.Errorf("memcache.Delete(%q): %v", keys[i], err)
}
}
}
// cachedTodo gets the specified todo cache entry (if it exists) from the
// shared todo cache.
func cachedTodo(c appengine.Context, todoKey string) (todo *Todo, ok bool) {
t := todoCache(c)
if t == nil {
return nil, false
}
todos := unmarshalTodo(c, t)
if todos == nil {
return nil, false
}
todo, ok = todos[todoKey]
return
}
// cacheTodo puts the provided todo cache entry into the shared todo cache.
// The todo cache is a JSON-encoded map[string]*Todo, where the key is todoKey.
func cacheTodo(c appengine.Context, todoKey string, todo *Todo) {
// Get the todo cache record (or create a new one).
newItem := false
t := todoCache(c)
if t == nil {
newItem = true
t = &memcache.Item{
Key: todoCacheKey,
Value: []byte("{}"), // default is an empty JSON object
}
}
// Unmarshal the JSON value.
todos := unmarshalTodo(c, t)
if todos == nil {
return
}
// Update the map.
todos[todoKey] = todo
// Marshal the updated JSON value.
var err os.Error
t.Value, err = json.Marshal(todos)
if err != nil {
// This shouldn't happen.
c.Criticalf("marshal todo cache: %v", err)
return
}
// Set a new expiry.
t.Expiration = todoCacheExpiry
// Update the cache record (or Set it, if new).
if newItem {
err = memcache.Set(c, t)
} else {
err = memcache.CompareAndSwap(c, t)
}
if err == memcache.ErrCASConflict || err == memcache.ErrNotStored {
// No big deal if it didn't work; it should next time.
c.Warningf("didn't update todo cache: %v", err)
} else if err != nil {
c.Errorf("update todo cache: %v", err)
}
}
// todoCache gets the todo cache record from memcache (if it exists).
func todoCache(c appengine.Context) *memcache.Item {
t, err := memcache.Get(c, todoCacheKey)
if err != nil {
if err != memcache.ErrCacheMiss {
c.Errorf("get todo cache: %v", err)
}
return nil
}
return t
}
// unmarshalTodo decodes the given item's memcache value into a map.
func unmarshalTodo(c appengine.Context, t *memcache.Item) map[string]*Todo {
todos := make(map[string]*Todo)
if err := json.Unmarshal(t.Value, &todos); err != nil {
// This shouldn't happen.
c.Criticalf("unmarshal todo cache: %v", err)
// Kill the bad record.
if err := memcache.Delete(c, todoCacheKey); err != nil {
c.Errorf("delete todo cache: %v", err)
}
return nil
}
return todos
}
...@@ -5,13 +5,15 @@ ...@@ -5,13 +5,15 @@
package build package build
import ( import (
"appengine"
"appengine/datastore"
"crypto/hmac" "crypto/hmac"
"fmt" "fmt"
"http" "http"
"json" "json"
"os" "os"
"appengine"
"appengine/datastore"
"cache"
) )
const commitsPerPage = 30 const commitsPerPage = 30
...@@ -58,7 +60,7 @@ func commitHandler(r *http.Request) (interface{}, os.Error) { ...@@ -58,7 +60,7 @@ func commitHandler(r *http.Request) (interface{}, os.Error) {
if err := com.Valid(); err != nil { if err := com.Valid(); err != nil {
return nil, fmt.Errorf("validating Commit: %v", err) return nil, fmt.Errorf("validating Commit: %v", err)
} }
defer invalidateCache(c) defer cache.Tick(c)
tx := func(c appengine.Context) os.Error { tx := func(c appengine.Context) os.Error {
return addCommit(c, com) return addCommit(c, com)
} }
...@@ -132,7 +134,7 @@ func tagHandler(r *http.Request) (interface{}, os.Error) { ...@@ -132,7 +134,7 @@ func tagHandler(r *http.Request) (interface{}, os.Error) {
return nil, err return nil, err
} }
c := appengine.NewContext(r) c := appengine.NewContext(r)
defer invalidateCache(c) defer cache.Tick(c)
_, err := datastore.Put(c, t.Key(c), t) _, err := datastore.Put(c, t.Key(c), t)
return nil, err return nil, err
} }
...@@ -148,14 +150,12 @@ type Todo struct { ...@@ -148,14 +150,12 @@ type Todo struct {
// Multiple "kind" parameters may be specified. // Multiple "kind" parameters may be specified.
func todoHandler(r *http.Request) (interface{}, os.Error) { func todoHandler(r *http.Request) (interface{}, os.Error) {
c := appengine.NewContext(r) c := appengine.NewContext(r)
now := cache.Now(c)
todoKey := r.Form.Encode() key := "build-todo-" + r.Form.Encode()
if t, ok := cachedTodo(c, todoKey); ok { cachedTodo := new(Todo)
c.Debugf("cache hit") if cache.Get(r, now, key, cachedTodo) {
return t, nil return cachedTodo, nil
} }
c.Debugf("cache miss")
var todo *Todo var todo *Todo
var err os.Error var err os.Error
builder := r.FormValue("builder") builder := r.FormValue("builder")
...@@ -175,7 +175,7 @@ func todoHandler(r *http.Request) (interface{}, os.Error) { ...@@ -175,7 +175,7 @@ func todoHandler(r *http.Request) (interface{}, os.Error) {
} }
} }
if err == nil { if err == nil {
cacheTodo(c, todoKey, todo) cache.Set(r, now, key, todo)
} }
return todo, err return todo, err
} }
...@@ -218,7 +218,19 @@ func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interf ...@@ -218,7 +218,19 @@ func buildTodo(c appengine.Context, builder, packagePath, goHash string) (interf
// packagesHandler returns a list of the non-Go Packages monitored // packagesHandler returns a list of the non-Go Packages monitored
// by the dashboard. // by the dashboard.
func packagesHandler(r *http.Request) (interface{}, os.Error) { func packagesHandler(r *http.Request) (interface{}, os.Error) {
return Packages(appengine.NewContext(r)) c := appengine.NewContext(r)
now := cache.Now(c)
const key = "build-packages"
var p []*Package
if cache.Get(r, now, key, &p) {
return p, nil
}
p, err := Packages(c)
if err != nil {
return nil, err
}
cache.Set(r, now, key, p)
return p, nil
} }
// resultHandler records a build result. // resultHandler records a build result.
...@@ -240,7 +252,7 @@ func resultHandler(r *http.Request) (interface{}, os.Error) { ...@@ -240,7 +252,7 @@ func resultHandler(r *http.Request) (interface{}, os.Error) {
if err := res.Valid(); err != nil { if err := res.Valid(); err != nil {
return nil, fmt.Errorf("validating Result: %v", err) return nil, fmt.Errorf("validating Result: %v", err)
} }
defer invalidateCache(c) defer cache.Tick(c)
// store the Log text if supplied // store the Log text if supplied
if len(res.Log) > 0 { if len(res.Log) > 0 {
hash, err := PutLog(c, res.Log) hash, err := PutLog(c, res.Log)
...@@ -347,6 +359,7 @@ func AuthHandler(h dashHandler) http.HandlerFunc { ...@@ -347,6 +359,7 @@ func AuthHandler(h dashHandler) http.HandlerFunc {
func initHandler(w http.ResponseWriter, r *http.Request) { func initHandler(w http.ResponseWriter, r *http.Request) {
// TODO(adg): devise a better way of bootstrapping new packages // TODO(adg): devise a better way of bootstrapping new packages
c := appengine.NewContext(r) c := appengine.NewContext(r)
defer cache.Tick(c)
for _, p := range defaultPackages { for _, p := range defaultPackages {
if err := datastore.Get(c, p.Key(c), new(Package)); err == nil { if err := datastore.Get(c, p.Key(c), new(Package)); err == nil {
continue continue
......
...@@ -8,9 +8,6 @@ ...@@ -8,9 +8,6 @@
package build package build
import ( import (
"appengine"
"appengine/datastore"
"appengine/memcache"
"bytes" "bytes"
"exp/template/html" "exp/template/html"
"http" "http"
...@@ -20,6 +17,10 @@ import ( ...@@ -20,6 +17,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"template" "template"
"appengine"
"appengine/datastore"
"cache"
) )
func init() { func init() {
...@@ -30,6 +31,8 @@ func init() { ...@@ -30,6 +31,8 @@ func init() {
// uiHandler draws the build status page. // uiHandler draws the build status page.
func uiHandler(w http.ResponseWriter, r *http.Request) { func uiHandler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r) c := appengine.NewContext(r)
now := cache.Now(c)
const key = "build-ui"
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
if page < 0 { if page < 0 {
...@@ -37,15 +40,12 @@ func uiHandler(w http.ResponseWriter, r *http.Request) { ...@@ -37,15 +40,12 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
} }
// Used cached version of front page, if available. // Used cached version of front page, if available.
if page == 0 && r.Host == "build.golang.org" { if page == 0 {
t, err := memcache.Get(c, uiCacheKey) var b []byte
if err == nil { if cache.Get(r, now, key, &b) {
w.Write(t.Value) w.Write(b)
return return
} }
if err != memcache.ErrCacheMiss {
c.Errorf("get ui cache: %v", err)
}
} }
commits, err := goCommits(c, page) commits, err := goCommits(c, page)
...@@ -78,15 +78,8 @@ func uiHandler(w http.ResponseWriter, r *http.Request) { ...@@ -78,15 +78,8 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
} }
// Cache the front page. // Cache the front page.
if page == 0 && r.Host == "build.golang.org" { if page == 0 {
t := &memcache.Item{ cache.Set(r, now, key, buf.Bytes())
Key: uiCacheKey,
Value: buf.Bytes(),
Expiration: uiCacheExpiry,
}
if err := memcache.Set(c, t); err != nil {
c.Errorf("set ui cache: %v", err)
}
} }
buf.WriteTo(w) buf.WriteTo(w)
......
// 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 cache
import (
"fmt"
"http"
"time"
"appengine"
"appengine/memcache"
)
const (
nocache = "nocache"
timeKey = "cachetime"
expiry = 600 // 10 minutes
)
func newTime() uint64 { return uint64(time.Seconds()) << 32 }
// Now returns the current logical datastore time to use for cache lookups.
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
}
// Tick sets the current logical datastore time to a never-before-used time
// and returns that time. It should be called to invalidate the cache.
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
}
// Get fetches data for name at time now from memcache and unmarshals it into
// value. It reports whether it found the cache record and logs any errors to
// the admin console.
func Get(r *http.Request, now uint64, name string, value interface{}) bool {
if now == 0 || r.FormValue(nocache) != "" {
return false
}
c := appengine.NewContext(r)
key := fmt.Sprintf("%s.%d", name, now)
_, err := memcache.JSON.Get(c, key, value)
if err == nil {
c.Debugf("cache hit %q", key)
return true
}
c.Debugf("cache miss %q", key)
if err != memcache.ErrCacheMiss {
c.Errorf("get cache %q: %v", key, err)
}
return false
}
// Set puts value into memcache under name at time now.
// It logs any errors to the admin console.
func Set(r *http.Request, now uint64, name string, value interface{}) {
if now == 0 || r.FormValue(nocache) != "" {
return
}
c := appengine.NewContext(r)
key := fmt.Sprintf("%s.%d", name, now)
err := memcache.JSON.Set(c, &memcache.Item{
Key: key,
Object: value,
Expiration: expiry,
})
if err != nil {
c.Errorf("set cache %q: %v", key, err)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment