Skip to content

Commit 11cd822

Browse files
committed
playground: stop storing timeouts in cache
This changes the Playground to stop storing build and run timeout responses in the cache. These responses could get cached when the Playground was unhealthy, leaving some trivial snippets to be cached incorrectly, confusing users. Adds testing for our caching logic. Updates golang/go#38546 Updates golang/go#38576 Change-Id: Idd2106d673162d9eea8536fe2433f74c23ed6e8a Reviewed-on: https://go-review.googlesource.com/c/playground/+/229307 Run-TryBot: Alexander Rakoczy <alex@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
1 parent 0084f7f commit 11cd822

File tree

6 files changed

+143
-48
lines changed

6 files changed

+143
-48
lines changed

cache.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import (
1111
"github.com/bradfitz/gomemcache/memcache"
1212
)
1313

14+
// responseCache is a common interface for cache implementations.
15+
type responseCache interface {
16+
// Set sets the value for a key.
17+
Set(key string, v interface{}) error
18+
// Get sets v to the value stored for a key.
19+
Get(key string, v interface{}) error
20+
}
21+
1422
// gobCache stores and retrieves values using a memcache client using the gob
1523
// encoding package. It does not currently allow for expiration of items.
1624
// With a nil gobCache, Set is a no-op and Get will always return memcache.ErrCacheMiss.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.12
55
require (
66
cloud.google.com/go v0.38.0
77
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
8+
github.com/google/go-cmp v0.3.0
89
golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0
910
golang.org/x/mod v0.2.0
1011
golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func main() {
3939
s.cache = newGobCache(caddr)
4040
log.Printf("App (project ID: %q) is caching results", pid)
4141
} else {
42+
s.cache = (*gobCache)(nil) // Use a no-op cache implementation.
4243
log.Printf("App (project ID: %q) is NOT caching results", pid)
4344
}
4445
s.log = log

sandbox.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,16 @@ const (
5050
progName = "prog.go"
5151
)
5252

53-
const goBuildTimeoutError = "timeout running go build"
53+
const (
54+
goBuildTimeoutError = "timeout running go build"
55+
runTimeoutError = "timeout running program"
56+
)
5457

55-
// Responses that contain these strings will not be cached due to
56-
// their non-deterministic nature.
57-
var nonCachingErrors = []string{
58+
// internalErrors are strings found in responses that will not be cached
59+
// due to their non-deterministic nature.
60+
var internalErrors = []string{
5861
"out of memory",
5962
"cannot allocate memory",
60-
goBuildTimeoutError,
6163
}
6264

6365
type request struct {
@@ -115,7 +117,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context
115117
resp := &response{}
116118
key := cacheKey(cachePrefix, req.Body)
117119
if err := s.cache.Get(key, resp); err != nil {
118-
if err != memcache.ErrCacheMiss {
120+
if !errors.Is(err, memcache.ErrCacheMiss) {
119121
s.log.Errorf("s.cache.Get(%q, &response): %v", key, err)
120122
}
121123
resp, err = cmdFunc(r.Context(), &req)
@@ -124,12 +126,16 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context
124126
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
125127
return
126128
}
127-
if strings.Contains(resp.Errors, goBuildTimeoutError) {
128-
// TODO(golang.org/issue/38052) - This should be a http.StatusBadRequest, but the UI requires a 200 to parse the response.
129+
if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) {
130+
// TODO(golang.org/issue/38576) - This should be a http.StatusBadRequest,
131+
// but the UI requires a 200 to parse the response. It's difficult to know
132+
// if we've timed out because of an error in the code snippet, or instability
133+
// on the playground itself. Either way, we should try to show the user the
134+
// partial output of their program.
129135
s.writeResponse(w, resp, http.StatusOK)
130136
return
131137
}
132-
for _, e := range nonCachingErrors {
138+
for _, e := range internalErrors {
133139
if strings.Contains(resp.Errors, e) {
134140
s.log.Errorf("cmdFunc compilation error: %q", resp.Errors)
135141
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -140,7 +146,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context
140146
if el.Kind != "stderr" {
141147
continue
142148
}
143-
for _, e := range nonCachingErrors {
149+
for _, e := range internalErrors {
144150
if strings.Contains(el.Message, e) {
145151
s.log.Errorf("cmdFunc runtime error: %q", el.Message)
146152
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -543,8 +549,8 @@ func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxt
543549
sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil }
544550
res, err := sandboxBackendClient().Do(sreq)
545551
if err != nil {
546-
if ctx.Err() == context.DeadlineExceeded {
547-
execRes.Error = "timeout running program"
552+
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
553+
execRes.Error = runTimeoutError
548554
return execRes, nil
549555
}
550556
return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err)

server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type server struct {
1818
mux *http.ServeMux
1919
db store
2020
log logger
21-
cache *gobCache
21+
cache responseCache
2222

2323
// When the executable was last modified. Used for caching headers of compiled assets.
2424
modtime time.Time

server_test.go

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ package main
77
import (
88
"bytes"
99
"context"
10+
"encoding/json"
1011
"fmt"
1112
"io/ioutil"
1213
"net/http"
1314
"net/http/httptest"
1415
"os"
16+
"sync"
1517
"testing"
18+
19+
"github.com/bradfitz/gomemcache/memcache"
20+
"github.com/google/go-cmp/cmp"
1621
)
1722

1823
type testLogger struct {
@@ -166,6 +171,7 @@ func TestCommandHandler(t *testing.T) {
166171
// Should we verify that s.log.Errorf was called
167172
// instead of just printing or failing the test?
168173
s.log = newStdLogger()
174+
s.cache = new(inMemCache)
169175
return nil
170176
})
171177
if err != nil {
@@ -193,63 +199,103 @@ func TestCommandHandler(t *testing.T) {
193199
if r.Body == "allocate-memory-compile-error" {
194200
return &response{Errors: "cannot allocate memory"}, nil
195201
}
202+
if r.Body == "build-timeout-error" {
203+
return &response{Errors: goBuildTimeoutError}, nil
204+
}
205+
if r.Body == "run-timeout-error" {
206+
return &response{Errors: runTimeoutError}, nil
207+
}
196208
resp := &response{Events: []Event{{r.Body, "stdout", 0}}}
197209
return resp, nil
198210
})
199211

200212
testCases := []struct {
201-
desc string
202-
method string
203-
statusCode int
204-
reqBody []byte
205-
respBody []byte
213+
desc string
214+
method string
215+
statusCode int
216+
reqBody []byte
217+
respBody []byte
218+
shouldCache bool
206219
}{
207-
{"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil},
208-
{"GET request", http.MethodGet, http.StatusBadRequest, nil, nil},
209-
{"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil},
210-
{"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil},
220+
{"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil, false},
221+
{"GET request", http.MethodGet, http.StatusBadRequest, nil, nil, false},
222+
{"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil, false},
223+
{"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil, false},
211224
{"Standard flow", http.MethodPost, http.StatusOK,
212225
[]byte(`{"Body":"ok"}`),
213226
[]byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0}
214227
`),
215-
},
216-
{"Errors in response", http.MethodPost, http.StatusOK,
228+
true},
229+
{"Cache-able Errors in response", http.MethodPost, http.StatusOK,
217230
[]byte(`{"Body":"error"}`),
218231
[]byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}
219232
`),
220-
},
233+
true},
221234
{"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError,
222-
[]byte(`{"Body":"oom-error"}`), nil},
235+
[]byte(`{"Body":"oom-error"}`), nil, false},
223236
{"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError,
224-
[]byte(`{"Body":"allocate-memory-error"}`), nil},
237+
[]byte(`{"Body":"allocate-memory-error"}`), nil, false},
225238
{"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError,
226-
[]byte(`{"Body":"oom-compile-error"}`), nil},
239+
[]byte(`{"Body":"oom-compile-error"}`), nil, false},
227240
{"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError,
228-
[]byte(`{"Body":"allocate-memory-compile-error"}`), nil},
241+
[]byte(`{"Body":"allocate-memory-compile-error"}`), nil, false},
242+
{
243+
desc: "Build timeout error",
244+
method: http.MethodPost,
245+
statusCode: http.StatusOK,
246+
reqBody: []byte(`{"Body":"build-timeout-error"}`),
247+
respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running go build","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)),
248+
},
249+
{
250+
desc: "Run timeout error",
251+
method: http.MethodPost,
252+
statusCode: http.StatusOK,
253+
reqBody: []byte(`{"Body":"run-timeout-error"}`),
254+
respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running program","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)),
255+
},
229256
}
230257

231258
for _, tc := range testCases {
232-
req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody))
233-
w := httptest.NewRecorder()
234-
testHandler(w, req)
235-
resp := w.Result()
236-
corsHeader := "Access-Control-Allow-Origin"
237-
if got, want := resp.Header.Get(corsHeader), "*"; got != want {
238-
t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want)
239-
}
240-
if got, want := resp.StatusCode, tc.statusCode; got != want {
241-
t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want)
242-
}
243-
if tc.respBody != nil {
244-
defer resp.Body.Close()
245-
b, err := ioutil.ReadAll(resp.Body)
246-
if err != nil {
247-
t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err)
259+
t.Run(tc.desc, func(t *testing.T) {
260+
req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody))
261+
w := httptest.NewRecorder()
262+
testHandler(w, req)
263+
resp := w.Result()
264+
corsHeader := "Access-Control-Allow-Origin"
265+
if got, want := resp.Header.Get(corsHeader), "*"; got != want {
266+
t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want)
248267
}
249-
if !bytes.Equal(b, tc.respBody) {
250-
t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody)
268+
if got, want := resp.StatusCode, tc.statusCode; got != want {
269+
t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want)
251270
}
252-
}
271+
if tc.respBody != nil {
272+
defer resp.Body.Close()
273+
b, err := ioutil.ReadAll(resp.Body)
274+
if err != nil {
275+
t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err)
276+
}
277+
if !bytes.Equal(b, tc.respBody) {
278+
t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody)
279+
}
280+
}
281+
282+
// Test caching semantics.
283+
sbreq := new(request) // A sandbox request, used in the cache key.
284+
json.Unmarshal(tc.reqBody, sbreq) // Ignore errors, request may be empty.
285+
gotCache := new(response)
286+
if err := s.cache.Get(cacheKey("test", sbreq.Body), gotCache); (err == nil) != tc.shouldCache {
287+
t.Errorf("s.cache.Get(%q, %v) = %v, shouldCache: %v", cacheKey("test", sbreq.Body), gotCache, err, tc.shouldCache)
288+
}
289+
wantCache := new(response)
290+
if tc.shouldCache {
291+
if err := json.Unmarshal(tc.respBody, wantCache); err != nil {
292+
t.Errorf("json.Unmarshal(%q, %v) = %v, wanted no error", tc.respBody, wantCache, err)
293+
}
294+
}
295+
if diff := cmp.Diff(wantCache, gotCache); diff != "" {
296+
t.Errorf("s.Cache.Get(%q) mismatch (-want +got):\n%s", cacheKey("test", sbreq.Body), diff)
297+
}
298+
})
253299
}
254300
}
255301

@@ -315,3 +361,36 @@ func TestPlaygroundGoproxy(t *testing.T) {
315361
})
316362
}
317363
}
364+
365+
// inMemCache is a responseCache backed by a map. It is only suitable for testing.
366+
type inMemCache struct {
367+
l sync.Mutex
368+
m map[string]*response
369+
}
370+
371+
// Set implements the responseCache interface.
372+
// Set stores a *response in the cache. It panics for other types to ensure test failure.
373+
func (i *inMemCache) Set(key string, v interface{}) error {
374+
i.l.Lock()
375+
defer i.l.Unlock()
376+
if i.m == nil {
377+
i.m = make(map[string]*response)
378+
}
379+
i.m[key] = v.(*response)
380+
return nil
381+
}
382+
383+
// Get implements the responseCache interface.
384+
// Get fetches a *response from the cache, or returns a memcache.ErrcacheMiss.
385+
// It panics for other types to ensure test failure.
386+
func (i *inMemCache) Get(key string, v interface{}) error {
387+
i.l.Lock()
388+
defer i.l.Unlock()
389+
target := v.(*response)
390+
got, ok := i.m[key]
391+
if !ok {
392+
return memcache.ErrCacheMiss
393+
}
394+
*target = *got
395+
return nil
396+
}

0 commit comments

Comments
 (0)