From 1d593709ec9f23224e06ee4e42406ed38530a2ed Mon Sep 17 00:00:00 2001 From: Andrew Gerrand Date: Thu, 17 Sep 2015 13:15:13 +1000 Subject: [PATCH] cmd/tip: serve talks.golang.org Also make health check test the backup process. Change-Id: I9d2ed2780c07bb08683d231fccad4674c2ac22a1 Reviewed-on: https://go-review.googlesource.com/14668 Reviewed-by: Brad Fitzpatrick --- cmd/tip/README | 4 +++ cmd/tip/godoc.go | 46 ++++++++++------------------- cmd/tip/talks.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/tip/talks.yaml | 15 ++++++++++ cmd/tip/tip.go | 59 +++++++++++++++++++++++++++++++++---- 5 files changed, 160 insertions(+), 37 deletions(-) create mode 100644 cmd/tip/talks.go create mode 100644 cmd/tip/talks.yaml diff --git a/cmd/tip/README b/cmd/tip/README index 098755ce..af01d0ef 100644 --- a/cmd/tip/README +++ b/cmd/tip/README @@ -1,3 +1,7 @@ To deploy tip.golang.org: $ gcloud --project golang-org preview app deploy --set-default godoc.yaml + +To deploy talks.golang.org: + +$ gcloud --project golang-org preview app deploy --set-default talks.yaml diff --git a/cmd/tip/godoc.go b/cmd/tip/godoc.go index 99f770ec..ab3c3d2e 100644 --- a/cmd/tip/godoc.go +++ b/cmd/tip/godoc.go @@ -6,14 +6,11 @@ package main import ( "bytes" + "errors" "fmt" - "io/ioutil" - "log" - "net/http" "os" "os/exec" "path/filepath" - "time" ) type godocBuilder struct { @@ -23,8 +20,6 @@ func (b godocBuilder) Signature(heads map[string]string) string { return heads["go"] + "-" + heads["tools"] } -var indexingMsg = []byte("Indexing in progress: result may be inaccurate") - func (b godocBuilder) Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) { goDir := filepath.Join(dir, "go") toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools") @@ -60,29 +55,18 @@ func (b godocBuilder) Init(dir, hostport string, heads map[string]string) (*exec if err := godoc.Start(); err != nil { return nil, err } - go func() { - // TODO(bradfitz): tell the proxy that this side is dead - if err := godoc.Wait(); err != nil { - log.Printf("process in %v exited: %v", dir, err) - } - }() - - var err error - deadline := time.Now().Add(startTimeout) - for time.Now().Before(deadline) { - time.Sleep(time.Second) - var res *http.Response - res, err = http.Get(fmt.Sprintf("http://%v/search?q=FALLTHROUGH", hostport)) - if err != nil { - continue - } - rbody, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err == nil && res.StatusCode == http.StatusOK && - !bytes.Contains(rbody, indexingMsg) { - return godoc, nil - } - } - godoc.Process.Kill() - return nil, fmt.Errorf("timed out waiting for process in %v at %v (%v)", dir, hostport, err) + return godoc, nil +} + +var indexingMsg = []byte("Indexing in progress: result may be inaccurate") + +func (b godocBuilder) HealthCheck(hostport string) error { + body, err := getOK(fmt.Sprintf("http://%v/search?q=FALLTHROUGH", hostport)) + if err != nil { + return err + } + if bytes.Contains(body, indexingMsg) { + return errors.New("still indexing") + } + return nil } diff --git a/cmd/tip/talks.go b/cmd/tip/talks.go new file mode 100644 index 00000000..15167e1c --- /dev/null +++ b/cmd/tip/talks.go @@ -0,0 +1,73 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +type talksBuilder struct { +} + +func (b talksBuilder) Signature(heads map[string]string) string { + return heads["talks"] +} + +const talksToolsRev = "ac6d9c1d842f9b6482f39f7a172e0251a0f7cbc0" + +func (b talksBuilder) Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) { + toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools") + if err := checkout(repoURL+"tools", talksToolsRev, toolsDir); err != nil { + return nil, err + } + talksDir := filepath.Join(dir, "gopath/src/golang.org/x/talks") + if err := checkout(repoURL+"talks", heads["talks"], talksDir); err != nil { + return nil, err + } + + goDir := os.Getenv("GOROOT_BOOTSTRAP") + if goDir == "" { + goDir = runtime.GOROOT() + } + goBin := filepath.Join(goDir, "bin/go") + goPath := filepath.Join(dir, "gopath") + presentPath := "golang.org/x/tools/cmd/present" + install := exec.Command(goBin, "install", "-tags=appenginevm", presentPath) + install.Env = []string{"GOROOT=" + goDir, "GOPATH=" + goPath} + if err := runErr(install); err != nil { + return nil, err + } + + talksBin := filepath.Join(goPath, "bin/present") + presentSrc := filepath.Join(goPath, "src", presentPath) + present := exec.Command(talksBin, "-http="+hostport, "-base="+presentSrc) + present.Dir = talksDir + // TODO(adg): log this somewhere useful + present.Stdout = os.Stdout + present.Stderr = os.Stderr + if err := present.Start(); err != nil { + return nil, err + } + return present, nil +} + +var talksMsg = []byte("Talks - The Go Programming Language") + +func (b talksBuilder) HealthCheck(hostport string) error { + body, err := getOK(fmt.Sprintf("http://%v/", hostport)) + if err != nil { + return err + } + if !bytes.Contains(body, talksMsg) { + return errors.New("couldn't match string") + } + return nil +} diff --git a/cmd/tip/talks.yaml b/cmd/tip/talks.yaml new file mode 100644 index 00000000..58627593 --- /dev/null +++ b/cmd/tip/talks.yaml @@ -0,0 +1,15 @@ +module: talks +runtime: custom +api_version: go1 +vm: true + +automatic_scaling: + min_num_instances: 1 + max_num_instances: 5 + +handlers: +- url: /.* + script: _go_app + +env_variables: + TIP_BUILDER: 'talks' diff --git a/cmd/tip/tip.go b/cmd/tip/tip.go index 4b897a2d..b6cac88a 100644 --- a/cmd/tip/tip.go +++ b/cmd/tip/tip.go @@ -9,6 +9,7 @@ package main import ( "bufio" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -35,6 +36,8 @@ func main() { switch os.Getenv(k) { case "godoc": b = godocBuilder{} + case "talks": + b = talksBuilder{} default: log.Fatalf("Unknown %v value: %q", k, os.Getenv(k)) } @@ -54,17 +57,19 @@ func main() { type Proxy struct { builder Builder - mu sync.Mutex // protects the followin' - proxy http.Handler - cur string // signature of gorepo+toolsrepo - cmd *exec.Cmd // live godoc instance, or nil for none - side string - err error + mu sync.Mutex // protects the followin' + proxy http.Handler + cur string // signature of gorepo+toolsrepo + cmd *exec.Cmd // live godoc instance, or nil for none + side string + hostport string // host and port of the live instance + err error } type Builder interface { Signature(heads map[string]string) string Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) + HealthCheck(hostport string) error } func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -85,6 +90,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if r.URL.Path == "/_ah/health" { + if err := p.builder.HealthCheck(p.hostport); err != nil { + http.Error(w, "Health check failde: "+err.Error(), http.StatusInternalServerError) + return + } fmt.Fprintln(w, "OK") return } @@ -148,6 +157,15 @@ func (p *Proxy) poll() { hostport = "localhost:8082" } cmd, err := p.builder.Init(dir, hostport, heads) + if err == nil { + go func() { + // TODO(adg,bradfitz): be smarter about dead processes + if err := cmd.Wait(); err != nil { + log.Printf("process in %v exited: %v", dir, err) + } + }() + err = waitReady(p.builder, hostport) + } p.mu.Lock() defer p.mu.Unlock() @@ -165,12 +183,25 @@ func (p *Proxy) poll() { } p.proxy = httputil.NewSingleHostReverseProxy(u) p.side = newSide + p.hostport = hostport if p.cmd != nil { p.cmd.Process.Kill() } p.cmd = cmd } +func waitReady(b Builder, hostport string) error { + var err error + deadline := time.Now().Add(startTimeout) + for time.Now().Before(deadline) { + if err = b.HealthCheck(hostport); err == nil { + return nil + } + time.Sleep(time.Second) + } + return fmt.Errorf("timed out waiting for process at %v: %v", hostport, err) +} + func runErr(cmd *exec.Cmd) error { out, err := cmd.CombinedOutput() if err != nil { @@ -252,3 +283,19 @@ func gerritMetaMap() map[string]string { } return m } + +func getOK(url string) (body []byte, err error) { + res, err := http.Get(url) + if err != nil { + return nil, err + } + body, err = ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusOK { + return nil, errors.New(res.Status) + } + return body, nil +}