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
 	}