Skip to content

Commit d4ff252

Browse files
committed
feat: persist play.go.dev Go versions
1 parent 3f64432 commit d4ff252

File tree

6 files changed

+301
-178
lines changed

6 files changed

+301
-178
lines changed

cmd/playground/main.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/x1unix/go-playground/internal/builder/storage"
1616
"github.com/x1unix/go-playground/internal/config"
1717
"github.com/x1unix/go-playground/internal/server"
18+
"github.com/x1unix/go-playground/internal/server/backendinfo"
1819
"github.com/x1unix/go-playground/internal/server/webutil"
1920
"github.com/x1unix/go-playground/pkg/goplay"
2021
"github.com/x1unix/go-playground/pkg/util/cmdutil"
@@ -77,11 +78,16 @@ func start(logger *zap.Logger, cfg *config.Config) error {
7778
go cleanupSvc.Start(ctx)
7879
}
7980

81+
backendsInfoSvc := backendinfo.NewBackendVersionService(zap.L(), playgroundClient, backendinfo.ServiceConfig{
82+
CacheFile: filepath.Join(cfg.Build.BuildDir, "go-versions.json"),
83+
TTL: backendinfo.DefaultVersionCacheTTL,
84+
})
85+
8086
// Initialize API endpoints
8187
r := mux.NewRouter()
8288
apiRouter := r.PathPrefix("/api").Subrouter()
8389
svcCfg := server.ServiceConfig{Version: Version}
84-
server.NewAPIv1Handler(svcCfg, playgroundClient, buildSvc).
90+
server.NewAPIv1Handler(svcCfg, playgroundClient, buildSvc, backendsInfoSvc).
8591
Mount(apiRouter)
8692

8793
apiv2Router := apiRouter.PathPrefix("/v2").Subrouter()
@@ -90,7 +96,6 @@ func start(logger *zap.Logger, cfg *config.Config) error {
9096
Builder: buildSvc,
9197
BuildTimeout: cfg.Build.GoBuildTimeout,
9298
}).Mount(apiv2Router)
93-
//server.NewAPIv2Handler(playgroundClient, buildSvc).Mount(apiv2Router)
9499

95100
// Web UI routes
96101
tplVars := server.TemplateArguments{
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package backendinfo
2+
3+
import "context"
4+
5+
type BackendVersions struct {
6+
// CurrentStable is latest stable Go version.
7+
CurrentStable string
8+
9+
// PreviousStable is previous stable Go version.
10+
PreviousStable string
11+
12+
// Nightly is latest unstable Go version (tip) version.
13+
Nightly string
14+
}
15+
16+
type BackendVersionProvider interface {
17+
// GetRemoteVersions returns Go version used on remote Go backends.
18+
GetRemoteVersions(ctx context.Context) (*BackendVersions, error)
19+
20+
// ServerVersion returns Go version used on server.
21+
ServerVersion() string
22+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package backendinfo
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io/fs"
10+
"os"
11+
"path/filepath"
12+
"runtime"
13+
"strings"
14+
"time"
15+
16+
"github.com/avast/retry-go"
17+
"github.com/x1unix/go-playground/pkg/goplay"
18+
"go.uber.org/zap"
19+
"golang.org/x/sync/errgroup"
20+
)
21+
22+
const (
23+
goVersionRetryAttempts = 3
24+
goVersionRetryDelay = time.Second
25+
26+
DefaultVersionCacheTTL = 48 * time.Hour
27+
)
28+
29+
//go:embed resources/version.go.txt
30+
var versionSnippet []byte
31+
32+
const cacheFileVersion = 1
33+
34+
var _ BackendVersionProvider = (*BackendVersionService)(nil)
35+
36+
type ServiceConfig struct {
37+
// Version is cache file version
38+
Version int
39+
40+
// CacheFile is name of a file which will be used to cache Go playground versions.
41+
CacheFile string
42+
43+
// TTL is expiration interval.
44+
TTL time.Duration
45+
}
46+
47+
type cacheEntry struct {
48+
Version int
49+
CreatedAt time.Time
50+
Data BackendVersions
51+
}
52+
53+
// BackendVersionService provides information about used Go versions
54+
// for all backends.
55+
type BackendVersionService struct {
56+
logger *zap.Logger
57+
client *goplay.Client
58+
cfg ServiceConfig
59+
60+
memCache *cacheEntry
61+
}
62+
63+
func NewBackendVersionService(logger *zap.Logger, client *goplay.Client, cfg ServiceConfig) *BackendVersionService {
64+
return &BackendVersionService{
65+
logger: logger,
66+
client: client,
67+
cfg: cfg,
68+
}
69+
}
70+
71+
func (svc *BackendVersionService) ServerVersion() string {
72+
return normalizeGoVersion(runtime.Version())
73+
}
74+
75+
func (svc *BackendVersionService) visitCache() (*cacheEntry, error) {
76+
if svc.memCache != nil {
77+
return svc.memCache, nil
78+
}
79+
80+
f, err := os.Open(svc.cfg.CacheFile)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
defer f.Close()
86+
dst := &cacheEntry{}
87+
err = json.NewDecoder(f).Decode(dst)
88+
89+
return dst, err
90+
}
91+
92+
// GetVersions provides Go version information for all backends.
93+
func (svc *BackendVersionService) GetRemoteVersions(ctx context.Context) (*BackendVersions, error) {
94+
cached, err := svc.visitCache()
95+
if err != nil {
96+
if !errors.Is(err, fs.ErrNotExist) {
97+
svc.logger.Error("failed to check Go versions cache", zap.Error(err))
98+
}
99+
100+
return svc.populateVersionCache(ctx)
101+
}
102+
103+
if cached.Version != cacheFileVersion {
104+
return nil, fs.ErrNotExist
105+
}
106+
107+
dt := time.Now().UTC().Sub(cached.CreatedAt.UTC())
108+
if dt >= svc.cfg.TTL {
109+
return svc.populateVersionCache(ctx)
110+
}
111+
112+
return &cached.Data, nil
113+
}
114+
115+
func (svc *BackendVersionService) populateVersionCache(ctx context.Context) (*BackendVersions, error) {
116+
versions, err := svc.pullBackendVersions(ctx)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
if err := svc.cacheVersions(versions); err != nil {
122+
svc.logger.Error("failed to cache Go versions", zap.Error(err))
123+
}
124+
125+
return versions, nil
126+
}
127+
128+
func (svc *BackendVersionService) cacheVersions(versions *BackendVersions) error {
129+
svc.memCache = &cacheEntry{
130+
Version: cacheFileVersion,
131+
CreatedAt: time.Now().UTC(),
132+
Data: *versions,
133+
}
134+
135+
err := os.MkdirAll(filepath.Dir(svc.cfg.CacheFile), 0755)
136+
if err != nil {
137+
return fmt.Errorf("MkdirAll failed: %w", err)
138+
}
139+
140+
f, err := os.OpenFile(svc.cfg.CacheFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
141+
if err != nil {
142+
return err
143+
}
144+
145+
defer f.Close()
146+
return json.NewEncoder(f).Encode(svc.memCache)
147+
}
148+
149+
func (svc *BackendVersionService) pullBackendVersions(ctx context.Context) (*BackendVersions, error) {
150+
versionInfo := &BackendVersions{}
151+
g, gCtx := errgroup.WithContext(ctx)
152+
153+
mapping := [3]struct {
154+
backend string
155+
dst *string
156+
}{
157+
{
158+
backend: goplay.BackendGoCurrent,
159+
dst: &versionInfo.CurrentStable,
160+
},
161+
{
162+
backend: goplay.BackendGoPrev,
163+
dst: &versionInfo.PreviousStable,
164+
},
165+
{
166+
backend: goplay.BackendGoTip,
167+
dst: &versionInfo.Nightly,
168+
},
169+
}
170+
171+
for _, e := range mapping {
172+
b := e
173+
g.Go(func() error {
174+
svc.logger.Debug("Fetching go version for backend", zap.String("backend", e.backend))
175+
result, err := svc.fetchGoBackendVersionWithRetry(gCtx, e.backend)
176+
if err != nil {
177+
return fmt.Errorf("failed to get Go version from Go playground server for backend %q: %w",
178+
b.backend, err)
179+
}
180+
181+
// We don't afraid race condition because each backend is written to a separate address
182+
*b.dst = result
183+
return nil
184+
})
185+
}
186+
187+
if err := g.Wait(); err != nil {
188+
return nil, err
189+
}
190+
191+
return versionInfo, nil
192+
}
193+
194+
func (svc *BackendVersionService) fetchGoBackendVersionWithRetry(ctx context.Context, backend goplay.Backend) (string, error) {
195+
var result string
196+
err := retry.Do(
197+
func() error {
198+
version, err := svc.getGoBackendVersion(ctx, backend)
199+
if err != nil {
200+
return err
201+
}
202+
203+
result = version
204+
return nil
205+
},
206+
retry.Attempts(goVersionRetryAttempts),
207+
retry.Delay(goVersionRetryDelay),
208+
retry.RetryIf(func(err error) bool {
209+
httpErr, ok := goplay.IsHTTPError(err)
210+
if !ok {
211+
return false
212+
}
213+
214+
// Retry only on server issues
215+
return httpErr.StatusCode >= 500
216+
}),
217+
retry.OnRetry(func(n uint, err error) {
218+
svc.logger.Error("failed to get Go version from Go playground, retrying...",
219+
zap.Error(err), zap.String("backend", backend), zap.Uint("attempt", n))
220+
}),
221+
)
222+
223+
return result, err
224+
}
225+
226+
func (svc *BackendVersionService) getGoBackendVersion(ctx context.Context, backend goplay.Backend) (string, error) {
227+
// Dirty hack to fetch Go version for playground backend by running a simple program
228+
// which returns Go version to stdout.
229+
result, err := svc.client.Evaluate(ctx, goplay.CompileRequest{
230+
Version: goplay.DefaultVersion,
231+
WithVet: false,
232+
Body: versionSnippet,
233+
}, backend)
234+
235+
if err != nil {
236+
return "", err
237+
}
238+
239+
if result.Errors != "" {
240+
return "", fmt.Errorf("probe program returned an error: %s", result.Errors)
241+
}
242+
243+
if len(result.Events) == 0 {
244+
return "", errors.New("missing output events from probe program")
245+
}
246+
247+
version := normalizeGoVersion(result.Events[0].Message)
248+
return version, nil
249+
}
250+
251+
func normalizeGoVersion(str string) string {
252+
return strings.TrimPrefix(str, "go")
253+
}

internal/server/handler_v1.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/gorilla/mux"
1212
"github.com/x1unix/go-playground/internal/builder"
1313
"github.com/x1unix/go-playground/internal/builder/storage"
14+
"github.com/x1unix/go-playground/internal/server/backendinfo"
1415
"github.com/x1unix/go-playground/pkg/goplay"
1516
"go.uber.org/zap"
1617
"golang.org/x/time/rate"
@@ -25,16 +26,12 @@ const (
2526
artifactParamVal = "artifactId"
2627
)
2728

28-
type BackendVersionProvider interface {
29-
GetVersions(ctx context.Context) (*VersionsInformation, error)
30-
}
31-
3229
// APIv1Handler is API v1 handler
3330
type APIv1Handler struct {
3431
config ServiceConfig
3532
log *zap.SugaredLogger
3633
compiler builder.BuildService
37-
versionProvider BackendVersionProvider
34+
versionProvider backendinfo.BackendVersionProvider
3835

3936
client *goplay.Client
4037
limiter *rate.Limiter
@@ -45,13 +42,13 @@ type ServiceConfig struct {
4542
}
4643

4744
// NewAPIv1Handler is APIv1Handler constructor
48-
func NewAPIv1Handler(cfg ServiceConfig, client *goplay.Client, builder builder.BuildService) *APIv1Handler {
45+
func NewAPIv1Handler(cfg ServiceConfig, client *goplay.Client, builder builder.BuildService, versionProvider backendinfo.BackendVersionProvider) *APIv1Handler {
4946
return &APIv1Handler{
5047
config: cfg,
5148
compiler: builder,
5249
client: client,
5350
log: zap.S().Named("api.v1"),
54-
versionProvider: NewBackendVersionService(zap.L(), client, VersionCacheTTL),
51+
versionProvider: versionProvider,
5552
limiter: rate.NewLimiter(rate.Every(frameTime), compileRequestsPerFrame),
5653
}
5754
}
@@ -73,12 +70,25 @@ func (s *APIv1Handler) HandleGetVersion(w http.ResponseWriter, _ *http.Request)
7370
}
7471

7572
func (s *APIv1Handler) HandleGetVersions(w http.ResponseWriter, r *http.Request) error {
76-
versions, err := s.versionProvider.GetVersions(r.Context())
73+
versions, err := s.versionProvider.GetRemoteVersions(r.Context())
7774
if err != nil {
75+
if errors.Is(err, context.Canceled) {
76+
return nil
77+
}
78+
7879
return err
7980
}
8081

81-
WriteJSON(w, versions)
82+
rsp := VersionsInformation{
83+
WebAssembly: s.versionProvider.ServerVersion(),
84+
Playground: &PlaygroundVersions{
85+
GoCurrent: versions.CurrentStable,
86+
GoPrevious: versions.PreviousStable,
87+
GoTip: versions.Nightly,
88+
},
89+
}
90+
91+
WriteJSON(w, rsp)
8292
return nil
8393
}
8494

0 commit comments

Comments
 (0)