playground: add program output caching
Change-Id: Ia1b64b4eec7d40a2cdade0ba2e67b82125f474b3
Reviewed-on: https://go-review.googlesource.com/85575
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/Dockerfile b/Dockerfile
index 5697930..4349d5e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -59,63 +59,78 @@
# BEGIN deps (run `make update-deps` to update)
-# Repo cloud.google.com/go at 558b56d (2017-07-03)
-ENV REV=558b56dfa3c56acc26fef35cb07f97df0bb18b39
-RUN go get -d cloud.google.com/go/compute/metadata `#and 5 other pkgs` &&\
+# Repo cloud.google.com/go at 3051b91 (2017-12-06)
+ENV REV=3051b919da3b8d62bc3a57ab4b353ca1c72402d5
+RUN go get -d cloud.google.com/go/compute/metadata `#and 6 other pkgs` &&\
(cd /go/src/cloud.google.com/go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo github.com/golang/protobuf at 748d386 (2017-07-26)
-ENV REV=748d386b5c1ea99658fd69fe9f03991ce86a90c1
-RUN go get -d github.com/golang/protobuf/proto `#and 6 other pkgs` &&\
+# Repo github.com/bradfitz/gomemcache at 1952afa (2017-02-08)
+ENV REV=1952afaa557dc08e8e0d89eafab110fb501c1a2b
+RUN go get -d github.com/bradfitz/gomemcache/memcache &&\
+ (cd /go/src/github.com/bradfitz/gomemcache && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
+
+# Repo github.com/golang/protobuf at 1e59b77 (2017-11-13)
+ENV REV=1e59b77b52bf8e4b449a57e6f79f21226d571845
+RUN go get -d github.com/golang/protobuf/proto `#and 8 other pkgs` &&\
(cd /go/src/github.com/golang/protobuf && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo golang.org/x/net at 66aacef (2017-08-28)
-ENV REV=66aacef3dd8a676686c7ae3716979581e8b03c47
+# Repo github.com/googleapis/gax-go at 317e000 (2017-09-15)
+ENV REV=317e0006254c44a0ac427cc52a0e083ff0b9622f
+RUN go get -d github.com/googleapis/gax-go &&\
+ (cd /go/src/github.com/googleapis/gax-go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
+
+# Repo golang.org/x/net at faacc1b (2017-12-07)
+ENV REV=faacc1b5e36e3ff02cbec9661c69ac63dd5a83ad
RUN go get -d golang.org/x/net/context `#and 8 other pkgs` &&\
(cd /go/src/golang.org/x/net && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo golang.org/x/oauth2 at cce311a (2017-06-29)
-ENV REV=cce311a261e6fcf29de72ca96827bdb0b7d9c9e6
+# Repo golang.org/x/oauth2 at 6a2004c (2017-12-06)
+ENV REV=6a2004c8907a86949d71c664c81574897a4e55a6
RUN go get -d golang.org/x/oauth2 `#and 5 other pkgs` &&\
(cd /go/src/golang.org/x/oauth2 && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo golang.org/x/text at 2bf8f2a (2017-06-30)
-ENV REV=2bf8f2a19ec09c670e931282edfe6567f6be21c9
+# Repo golang.org/x/text at be25de4 (2017-12-07)
+ENV REV=be25de41fadfae372d6470bda81ca6beb55ef551
RUN go get -d golang.org/x/text/secure/bidirule `#and 4 other pkgs` &&\
(cd /go/src/golang.org/x/text && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo golang.org/x/tools at 89c69fd (2017-09-01)
-ENV REV=89c69fd3045b723bb4d9f75d73b881c50ab481c0
+# Repo golang.org/x/tools at b451b9a (2017-12-26)
+ENV REV=b451b9aaee4dcf75f9f28cddb69b9d0ed17a9752
RUN go get -d golang.org/x/tools/go/ast/astutil `#and 3 other pkgs` &&\
(cd /go/src/golang.org/x/tools && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo google.golang.org/api at e6586c9 (2017-06-27)
-ENV REV=e6586c9293b9d514c7f5d5076731ec977cff1be6
-RUN go get -d google.golang.org/api/googleapi/transport `#and 5 other pkgs` &&\
+# Repo google.golang.org/api at 9a048ca (2017-12-07)
+ENV REV=9a048cac3675aa589c62a35d7d42b25451dd15f1
+RUN go get -d google.golang.org/api/googleapi `#and 6 other pkgs` &&\
(cd /go/src/google.golang.org/api && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo google.golang.org/genproto at aa2eb68 (2017-06-01)
-ENV REV=aa2eb687b4d3e17154372564ad8d6bf11c3cf21f
+# Repo google.golang.org/genproto at 7f0da29 (2017-11-23)
+ENV REV=7f0da29060c682909f650ad8ed4e515bd74fa12a
RUN go get -d google.golang.org/genproto/googleapis/api/annotations `#and 4 other pkgs` &&\
(cd /go/src/google.golang.org/genproto && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
-# Repo google.golang.org/grpc at 3c33c26 (2017-06-27)
-ENV REV=3c33c26290b747350f8650c7d38bcc51b42dc785
-RUN go get -d google.golang.org/grpc `#and 15 other pkgs` &&\
+# Repo google.golang.org/grpc at b8191e5 (2017-12-06)
+ENV REV=b8191e57b23de650278db4d23bf596219e5f3665
+RUN go get -d google.golang.org/grpc `#and 24 other pkgs` &&\
(cd /go/src/google.golang.org/grpc && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV)
# Optimization to speed up iterative development, not necessary for correctness:
RUN go install cloud.google.com/go/compute/metadata \
cloud.google.com/go/datastore \
+ cloud.google.com/go/internal \
cloud.google.com/go/internal/atomiccache \
cloud.google.com/go/internal/fields \
cloud.google.com/go/internal/version \
+ github.com/bradfitz/gomemcache/memcache \
github.com/golang/protobuf/proto \
github.com/golang/protobuf/protoc-gen-go/descriptor \
+ github.com/golang/protobuf/ptypes \
github.com/golang/protobuf/ptypes/any \
+ github.com/golang/protobuf/ptypes/duration \
github.com/golang/protobuf/ptypes/struct \
github.com/golang/protobuf/ptypes/timestamp \
github.com/golang/protobuf/ptypes/wrappers \
+ github.com/googleapis/gax-go \
golang.org/x/net/context \
golang.org/x/net/context/ctxhttp \
golang.org/x/net/http2 \
@@ -136,26 +151,36 @@
golang.org/x/tools/go/ast/astutil \
golang.org/x/tools/godoc/static \
golang.org/x/tools/imports \
- google.golang.org/api/googleapi/transport \
+ google.golang.org/api/googleapi \
+ google.golang.org/api/googleapi/internal/uritemplates \
google.golang.org/api/internal \
google.golang.org/api/iterator \
google.golang.org/api/option \
- google.golang.org/api/transport \
+ google.golang.org/api/transport/grpc \
google.golang.org/genproto/googleapis/api/annotations \
google.golang.org/genproto/googleapis/datastore/v1 \
google.golang.org/genproto/googleapis/rpc/status \
google.golang.org/genproto/googleapis/type/latlng \
google.golang.org/grpc \
+ google.golang.org/grpc/balancer \
+ google.golang.org/grpc/balancer/base \
+ google.golang.org/grpc/balancer/roundrobin \
google.golang.org/grpc/codes \
+ google.golang.org/grpc/connectivity \
google.golang.org/grpc/credentials \
google.golang.org/grpc/credentials/oauth \
- google.golang.org/grpc/grpclb/grpc_lb_v1 \
+ google.golang.org/grpc/encoding \
+ google.golang.org/grpc/grpclb/grpc_lb_v1/messages \
google.golang.org/grpc/grpclog \
google.golang.org/grpc/internal \
google.golang.org/grpc/keepalive \
google.golang.org/grpc/metadata \
google.golang.org/grpc/naming \
google.golang.org/grpc/peer \
+ google.golang.org/grpc/resolver \
+ google.golang.org/grpc/resolver/dns \
+ google.golang.org/grpc/resolver/manual \
+ google.golang.org/grpc/resolver/passthrough \
google.golang.org/grpc/stats \
google.golang.org/grpc/status \
google.golang.org/grpc/tap \
diff --git a/Makefile b/Makefile
index 0854a90..e4c4d87 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
update-deps:
go install golang.org/x/build/cmd/gitlock
- gitlock --update=Dockerfile golang.org/x/playground/frontend
+ gitlock --update=Dockerfile golang.org/x/playground
docker: Dockerfile
docker build -t playground .
diff --git a/cache.go b/cache.go
new file mode 100644
index 0000000..5d725ba
--- /dev/null
+++ b/cache.go
@@ -0,0 +1,45 @@
+// 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.
+
+package main
+
+import (
+ "bytes"
+ "encoding/gob"
+
+ "github.com/bradfitz/gomemcache/memcache"
+)
+
+// gobCache stores and retrieves values using a memcache client using the gob
+// encoding package. It does not currently allow for expiration of items.
+// With a nil gobCache, Set is a no-op and Get will always return memcache.ErrCacheMiss.
+type gobCache struct {
+ client *memcache.Client
+}
+
+func newGobCache(addr string) *gobCache {
+ return &gobCache{memcache.New(addr)}
+}
+
+func (c *gobCache) Set(key string, v interface{}) error {
+ if c == nil {
+ return nil
+ }
+ var buf bytes.Buffer
+ if err := gob.NewEncoder(&buf).Encode(v); err != nil {
+ return err
+ }
+ return c.client.Set(&memcache.Item{Key: key, Value: buf.Bytes()})
+}
+
+func (c *gobCache) Get(key string, v interface{}) error {
+ if c == nil {
+ return memcache.ErrCacheMiss
+ }
+ item, err := c.client.Get(key)
+ if err != nil {
+ return err
+ }
+ return gob.NewDecoder(bytes.NewBuffer(item.Value)).Decode(v)
+}
diff --git a/main.go b/main.go
index acef3f6..ee5bdb6 100644
--- a/main.go
+++ b/main.go
@@ -17,11 +17,6 @@
var log = newStdLogger()
func main() {
- if len(os.Args) > 1 && os.Args[1] == "test" {
- test()
- return
- }
-
s, err := newServer(func(s *server) error {
pid := projectID()
if pid == "" {
@@ -33,12 +28,21 @@
}
s.db = cloudDatastore{client: c}
}
+ if os.Getenv("GAE_INSTANCE") != "" {
+ s.cache = newGobCache("memcached:11211")
+ }
s.log = log
return nil
})
if err != nil {
log.Fatalf("Error creating server: %v", err)
}
+
+ if len(os.Args) > 1 && os.Args[1] == "test" {
+ s.test()
+ return
+ }
+
port := os.Getenv("PORT")
if port == "" {
port = "8080"
@@ -48,10 +52,6 @@
}
func projectID() string {
- id := os.Getenv("DATASTORE_PROJECT_ID")
- if id != "" {
- return id
- }
id, err := metadata.ProjectID()
if err != nil && os.Getenv("GAE_INSTANCE") != "" {
log.Fatalf("Could not determine the project ID: %v", err)
diff --git a/sandbox.go b/sandbox.go
index 97ba0ba..bf0cd72 100644
--- a/sandbox.go
+++ b/sandbox.go
@@ -9,19 +9,25 @@
package main
import (
+ "bytes"
"context"
+ "crypto/sha256"
"encoding/json"
"fmt"
"go/parser"
"go/token"
+ "io"
"io/ioutil"
stdlog "log"
"net/http"
"os"
"os/exec"
"path/filepath"
+ "runtime"
"strings"
"time"
+
+ "github.com/bradfitz/gomemcache/memcache"
)
const maxRunTime = 2 * time.Second
@@ -35,7 +41,7 @@
Events []Event
}
-func handleCompile(w http.ResponseWriter, r *http.Request) {
+func (s *server) handleCompile(w http.ResponseWriter, r *http.Request) {
var req request
// Until programs that depend on golang.org/x/tools/godoc/static/playground.js
// are updated to always send JSON, this check is in place.
@@ -45,18 +51,42 @@
http.Error(w, fmt.Sprintf("error decoding request: %v", err), http.StatusBadRequest)
return
}
- resp, err := compileAndRun(&req)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+
+ resp := &response{}
+ key := cacheKey(req.Body)
+ if err := s.cache.Get(key, resp); err != nil {
+ if err != memcache.ErrCacheMiss {
+ s.log.Errorf("s.cache.Get(%q, &response): %v", key, err)
+ }
+ var err error
+ resp, err = s.compileAndRun(&req)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := s.cache.Set(key, resp); err != nil {
+ s.log.Errorf("cache.Set(%q, %+v): %v", key, resp, err)
+ }
+ }
+
+ var buf bytes.Buffer
+ if err := json.NewEncoder(&buf).Encode(resp); err != nil {
+ http.Error(w, fmt.Sprintf("error encoding response: %v", err), http.StatusInternalServerError)
return
}
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- http.Error(w, fmt.Sprintf("error encoding response: %v", err), http.StatusInternalServerError)
+ if _, err := io.Copy(w, &buf); err != nil {
+ s.log.Errorf("io.Copy(w, %+v): %v", buf, err)
return
}
}
-func compileAndRun(req *request) (*response, error) {
+func cacheKey(body string) string {
+ h := sha256.New()
+ io.WriteString(h, body)
+ return fmt.Sprintf("prog-%s-%x", runtime.Version(), h.Sum(nil))
+}
+
+func (s *server) compileAndRun(req *request) (*response, error) {
// TODO(andybons): Add semaphore to limit number of running programs at once.
tmpDir, err := ioutil.TempDir("", "sandbox")
if err != nil {
@@ -116,8 +146,8 @@
return &response{Events: events}, nil
}
-func healthCheck() error {
- resp, err := compileAndRun(&request{Body: healthProg})
+func (s *server) healthCheck() error {
+ resp, err := s.compileAndRun(&request{Body: healthProg})
if err != nil {
return err
}
@@ -138,12 +168,12 @@
func main() { fmt.Print("ok") }
`
-func test() {
- if err := healthCheck(); err != nil {
+func (s *server) test() {
+ if err := s.healthCheck(); err != nil {
stdlog.Fatal(err)
}
for _, t := range tests {
- resp, err := compileAndRun(&request{Body: t.prog})
+ resp, err := s.compileAndRun(&request{Body: t.prog})
if err != nil {
stdlog.Fatal(err)
}
diff --git a/server.go b/server.go
index 350b384..2f6dab1 100644
--- a/server.go
+++ b/server.go
@@ -15,9 +15,10 @@
)
type server struct {
- mux *http.ServeMux
- db store
- log logger
+ mux *http.ServeMux
+ db store
+ log logger
+ cache *gobCache
// When the executable was last modified. Used for caching headers of compiled assets.
modtime time.Time
@@ -50,10 +51,10 @@
s.mux.HandleFunc("/", s.handleEdit)
s.mux.HandleFunc("/fmt", handleFmt)
s.mux.HandleFunc("/share", s.handleShare)
- s.mux.HandleFunc("/compile", handleCompile)
+ s.mux.HandleFunc("/compile", s.handleCompile)
s.mux.HandleFunc("/playground.js", s.handlePlaygroundJS)
s.mux.HandleFunc("/favicon.ico", handleFavicon)
- s.mux.HandleFunc("/_ah/health", handleHealthCheck)
+ s.mux.HandleFunc("/_ah/health", s.handleHealthCheck)
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
s.mux.Handle("/static/", staticHandler)
@@ -69,8 +70,8 @@
http.ServeFile(w, r, "./static/favicon.ico")
}
-func handleHealthCheck(w http.ResponseWriter, r *http.Request) {
- if err := healthCheck(); err != nil {
+func (s *server) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
+ if err := s.healthCheck(); err != nil {
http.Error(w, "Health check failed: "+err.Error(), http.StatusInternalServerError)
return
}