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 <bradfitz@golang.org>
This commit is contained in:
Andrew Gerrand 2015-09-17 13:15:13 +10:00
parent ac6d9c1d84
commit 1d593709ec
5 changed files with 160 additions and 37 deletions

View File

@ -1,3 +1,7 @@
To deploy tip.golang.org: To deploy tip.golang.org:
$ gcloud --project golang-org preview app deploy --set-default godoc.yaml $ 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

View File

@ -6,14 +6,11 @@ package main
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io/ioutil"
"log"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"time"
) )
type godocBuilder struct { type godocBuilder struct {
@ -23,8 +20,6 @@ func (b godocBuilder) Signature(heads map[string]string) string {
return heads["go"] + "-" + heads["tools"] 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) { func (b godocBuilder) Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) {
goDir := filepath.Join(dir, "go") goDir := filepath.Join(dir, "go")
toolsDir := filepath.Join(dir, "gopath/src/golang.org/x/tools") 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 { if err := godoc.Start(); err != nil {
return nil, err return nil, err
} }
go func() { return godoc, nil
// 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 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))
var err error if err != nil {
deadline := time.Now().Add(startTimeout) return err
for time.Now().Before(deadline) { }
time.Sleep(time.Second) if bytes.Contains(body, indexingMsg) {
var res *http.Response return errors.New("still indexing")
res, err = http.Get(fmt.Sprintf("http://%v/search?q=FALLTHROUGH", hostport)) }
if err != nil { return 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)
} }

73
cmd/tip/talks.go Normal file
View File

@ -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
}

15
cmd/tip/talks.yaml Normal file
View File

@ -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'

View File

@ -9,6 +9,7 @@ package main
import ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -35,6 +36,8 @@ func main() {
switch os.Getenv(k) { switch os.Getenv(k) {
case "godoc": case "godoc":
b = godocBuilder{} b = godocBuilder{}
case "talks":
b = talksBuilder{}
default: default:
log.Fatalf("Unknown %v value: %q", k, os.Getenv(k)) log.Fatalf("Unknown %v value: %q", k, os.Getenv(k))
} }
@ -54,17 +57,19 @@ func main() {
type Proxy struct { type Proxy struct {
builder Builder builder Builder
mu sync.Mutex // protects the followin' mu sync.Mutex // protects the followin'
proxy http.Handler proxy http.Handler
cur string // signature of gorepo+toolsrepo cur string // signature of gorepo+toolsrepo
cmd *exec.Cmd // live godoc instance, or nil for none cmd *exec.Cmd // live godoc instance, or nil for none
side string side string
err error hostport string // host and port of the live instance
err error
} }
type Builder interface { type Builder interface {
Signature(heads map[string]string) string Signature(heads map[string]string) string
Init(dir, hostport string, heads map[string]string) (*exec.Cmd, error) 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) { 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 return
} }
if r.URL.Path == "/_ah/health" { 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") fmt.Fprintln(w, "OK")
return return
} }
@ -148,6 +157,15 @@ func (p *Proxy) poll() {
hostport = "localhost:8082" hostport = "localhost:8082"
} }
cmd, err := p.builder.Init(dir, hostport, heads) 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() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
@ -165,12 +183,25 @@ func (p *Proxy) poll() {
} }
p.proxy = httputil.NewSingleHostReverseProxy(u) p.proxy = httputil.NewSingleHostReverseProxy(u)
p.side = newSide p.side = newSide
p.hostport = hostport
if p.cmd != nil { if p.cmd != nil {
p.cmd.Process.Kill() p.cmd.Process.Kill()
} }
p.cmd = cmd 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 { func runErr(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@ -252,3 +283,19 @@ func gerritMetaMap() map[string]string {
} }
return m 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
}