Skip to content

Commit

Permalink
devapp: put appengine code behind build flag
Browse files Browse the repository at this point in the history
Per discussion on the mailing list we want to make this app run
on more environments than App Engine and hopefully with a backing
datastore that is just in-memory, or at the very least is not App
Engine specific.

Puts the datastore implementation detail behind an appengine build flag.
Adds an in-memory datastore if you are not running on App Engine; it's slow
since you have to fetch issues every time, but you can get all of the issues
and browse them. Adds a single test so we have the most basic of protections
against a regression.

Add a cmd/devapp main package so we can run the server outside of an
App Engine context.

Rename gg.Percentile to gg.Quantile to match the change in the latest
version of the downstream library.

Change-Id: Icbdef29676ecbf7078b0fb8c3920f61df60a5e2e
Reviewed-on: https://go-review.googlesource.com/34928
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
kevinburke authored and bradfitz committed Jan 31, 2017
1 parent d559b30 commit 310c021
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 100 deletions.
48 changes: 48 additions & 0 deletions cmd/devapp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2017 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.

// Devapp generates the dashboard that powers dev.golang.org.
//
// Usage:
//
// devapp --port=8081
//
// By default devapp listens on port 8081.
//
// Github issues and Gerrit CL's are stored in memory in the running process.
// To trigger an initial download, visit http://localhost:8081/update or
// http://localhost:8081/update/stats in your browser.
package main

import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"

_ "golang.org/x/build/devapp"
)

func init() {
flag.Usage = func() {
os.Stderr.WriteString(`usage: devapp [-port=port]
Devapp generates the dashboard that powers dev.golang.org.
`)
os.Exit(2)
}
}

func main() {
port := flag.Uint("port", 8081, "Port to listen on")
flag.Parse()
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
fmt.Fprintf(os.Stderr, "Listening on port %d\n", *port)
log.Fatal(http.Serve(ln, nil))
}
142 changes: 142 additions & 0 deletions devapp/appengine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2015 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.

// +build appengine

package devapp

import (
"net/http"
"os"

"appengine"

"golang.org/x/build/godash"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
applog "google.golang.org/appengine/log"
"google.golang.org/appengine/urlfetch"
"google.golang.org/appengine/user"
)

func init() {
onAppengine = !appengine.IsDevAppServer()
log = &appengineLogger{}

http.HandleFunc("/setToken", setTokenHandler)
}

type appengineLogger struct{}

func (a *appengineLogger) Infof(ctx context.Context, format string, args ...interface{}) {
applog.Infof(ctx, format, args...)
}

func (a *appengineLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
applog.Errorf(ctx, format, args...)
}

func (a *appengineLogger) Criticalf(ctx context.Context, format string, args ...interface{}) {
applog.Criticalf(ctx, format, args...)
}

func newTransport(ctx context.Context) http.RoundTripper {
return &urlfetch.Transport{Context: ctx}
}

func currentUserEmail(ctx context.Context) string {
u := user.Current(ctx)
if u == nil {
return ""
}
return u.Email
}

// loginURL returns a URL that, when visited, prompts the user to sign in,
// then redirects the user to the URL specified by dest.
func loginURL(ctx context.Context, path string) (string, error) {
return user.LoginURL(ctx, path)
}

func logoutURL(ctx context.Context, path string) (string, error) {
return user.LogoutURL(ctx, path)
}

func loadData(ctx context.Context) (*godash.Data, error) {
os.Stderr.WriteString("appengine load data")
cache, err := getCache(ctx, "gzdata")
if err != nil {
return nil, err
}
return parseData(cache)
}

func newHTTPClient(ctx context.Context) *http.Client {
return urlfetch.Client(ctx)
}

func getCache(ctx context.Context, name string) (*Cache, error) {
var cache Cache
if err := datastore.Get(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &cache); err != nil {
return &cache, err
}
return &cache, nil
}

func getCaches(ctx context.Context, names ...string) map[string]*Cache {
out := make(map[string]*Cache)
var keys []*datastore.Key
var ptrs []*Cache
for _, name := range names {
keys = append(keys, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil))
out[name] = &Cache{}
ptrs = append(ptrs, out[name])
}
datastore.GetMulti(ctx, keys, ptrs) // Ignore errors since they might not exist.
return out
}

func getPage(ctx context.Context, page string) (*Page, error) {
entity := new(Page)
err := datastore.Get(ctx, datastore.NewKey(ctx, entityPrefix+"Page", page, 0, nil), entity)
return entity, err
}

func writePage(ctx context.Context, page string, content []byte) error {
entity := &Page{
Content: content,
}
_, err := datastore.Put(ctx, datastore.NewKey(ctx, entityPrefix+"Page", page, 0, nil), entity)
return err
}

func putCache(ctx context.Context, name string, c *Cache) error {
_, err := datastore.Put(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &c)
return err
}

func getToken(ctx context.Context) (string, error) {
cache, err := getCache(ctx, "github-token")
if err != nil {
return "", err
}
return cache.Value, nil
}

// Store a token in the database
func setTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
r.ParseForm()
if value := r.Form.Get("value"); value != "" {
var token Cache
token.Value = []byte(value)
if err := putCache(ctx, "github-token", &token); err != nil {
http.Error(w, err.Error(), 500)
}
}
}

func getContext(r *http.Request) context.Context {
return appengine.NewContext(r)
}
28 changes: 1 addition & 27 deletions devapp/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"encoding/gob"

"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
)

// Cache is a datastore entity type that contains serialized data for dashboards.
Expand All @@ -22,27 +20,6 @@ type Cache struct {
Value []byte
}

func getCaches(ctx context.Context, names ...string) map[string]*Cache {
out := make(map[string]*Cache)
var keys []*datastore.Key
var ptrs []*Cache
for _, name := range names {
keys = append(keys, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil))
out[name] = &Cache{}
ptrs = append(ptrs, out[name])
}
datastore.GetMulti(ctx, keys, ptrs) // Ignore errors since they might not exist.
return out
}

func getCache(ctx context.Context, name string) (*Cache, error) {
var cache Cache
if err := datastore.Get(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &cache); err != nil {
return nil, err
}
return &cache, nil
}

func unpackCache(cache *Cache, data interface{}) error {
if len(cache.Value) > 0 {
gzr, err := gzip.NewReader(bytes.NewReader(cache.Value))
Expand Down Expand Up @@ -78,8 +55,5 @@ func writeCache(ctx context.Context, name string, data interface{}) error {
}
cache.Value = cacheout.Bytes()
log.Infof(ctx, "Cache %q update finished; writing %d bytes", name, cacheout.Len())
if _, err := datastore.Put(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &cache); err != nil {
return err
}
return nil
return putCache(ctx, name, &cache)
}
41 changes: 24 additions & 17 deletions devapp/dash.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ import (
"github.com/google/go-github/github"
"golang.org/x/build/godash"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
"google.golang.org/appengine/user"
)

var onAppengine = false

type logger interface {
Infof(context.Context, string, ...interface{})
Errorf(context.Context, string, ...interface{})
Criticalf(context.Context, string, ...interface{})
}

var log logger

func findEmail(ctx context.Context, data *godash.Data) string {
u := user.Current(ctx)
email := currentUserEmail(ctx)

if u != nil {
return data.Reviewers.Preferred(u.Email)
if email != "" {
return data.Reviewers.Preferred(email)
}
return ""
}
Expand Down Expand Up @@ -67,6 +74,14 @@ func (x byDate) Less(i, j int) bool {
return a.Before(*b)
}

func loadData(ctx context.Context) (*godash.Data, error) {
cache, err := getCache(ctx, "gzdata")
if err != nil {
return nil, err
}
return parseData(cache)
}

func datedMilestones(milestones []*github.Milestone) []string {
milestones = append([]*github.Milestone{}, milestones...)
sort.Stable(byDate(milestones))
Expand All @@ -79,21 +94,13 @@ func datedMilestones(milestones []*github.Milestone) []string {
return names
}

func loadData(ctx context.Context) (*godash.Data, error) {
cache, err := getCache(ctx, "gzdata")
if err != nil {
return nil, err
}
return parseData(cache)
}

func parseData(cache *Cache) (*godash.Data, error) {
data := &godash.Data{Reviewers: &godash.Reviewers{}}
return data, unpackCache(cache, &data)
}

func showDash(w http.ResponseWriter, req *http.Request) {
ctx := appengine.NewContext(req)
ctx := getContext(req)
req.ParseForm()

data, err := loadData(ctx)
Expand Down Expand Up @@ -143,11 +150,11 @@ func showDash(w http.ResponseWriter, req *http.Request) {
filtered = append(filtered, group)
}

login, err := user.LoginURL(ctx, "/dash")
login, err := loginURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
}
logout, err := user.LogoutURL(ctx, "/dash")
logout, err := logoutURL(ctx, "/dash")
if err != nil {
http.Error(w, err.Error(), 500)
}
Expand Down
Loading

0 comments on commit 310c021

Please sign in to comment.