mirror of
https://github.com/golang/go
synced 2024-11-19 06:04:39 -07:00
ce967854d3
500 is considered to be healthy. Use 503 instead, so the instance is considered unhealthy while the side is coming up. Add some extra log statements that help debug instances from application logs. Fixes golang/go#13682. Change-Id: I713c8c2fa75de4e275f632b999edc98cedd257bd Reviewed-on: https://go-review.googlesource.com/18547 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Andrew Gerrand <adg@golang.org>
316 lines
6.8 KiB
Go
316 lines
6.8 KiB
Go
// Copyright 2014 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.
|
|
|
|
// Command tipgodoc is the beginning of the new tip.golang.org server,
|
|
// serving the latest HEAD straight from the Git oven.
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
repoURL = "https://go.googlesource.com/"
|
|
metaURL = "https://go.googlesource.com/?b=master&format=JSON"
|
|
startTimeout = 5 * time.Minute
|
|
)
|
|
|
|
func main() {
|
|
const k = "TIP_BUILDER"
|
|
var b Builder
|
|
switch os.Getenv(k) {
|
|
case "godoc":
|
|
b = godocBuilder{}
|
|
case "talks":
|
|
b = talksBuilder{}
|
|
default:
|
|
log.Fatalf("Unknown %v value: %q", k, os.Getenv(k))
|
|
}
|
|
|
|
p := &Proxy{builder: b}
|
|
go p.run()
|
|
http.Handle("/", p)
|
|
http.HandleFunc("/_ah/health", p.serveHealthCheck)
|
|
|
|
log.Print("Starting up")
|
|
|
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
|
p.stop()
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Proxy implements the tip.golang.org server: a reverse-proxy
|
|
// that builds and runs godoc instances showing the latest docs.
|
|
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
|
|
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) {
|
|
if r.URL.Path == "/_tipstatus" {
|
|
p.serveStatus(w, r)
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
proxy := p.proxy
|
|
err := p.err
|
|
p.mu.Unlock()
|
|
if proxy == nil {
|
|
s := "starting up"
|
|
if err != nil {
|
|
s = err.Error()
|
|
}
|
|
http.Error(w, s, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
proxy.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (p *Proxy) serveStatus(w http.ResponseWriter, r *http.Request) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
fmt.Fprintf(w, "side=%v\ncurrent=%v\nerror=%v\n", p.side, p.cur, p.err)
|
|
}
|
|
|
|
func (p *Proxy) serveHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
// NOTE: Status 502, 503, 504 are the only status codes that signify an unhealthy app.
|
|
// So long as this handler returns one of those codes, this instance will not be sent any requests.
|
|
if p.proxy == nil {
|
|
log.Printf("Health check: not ready")
|
|
http.Error(w, "Not ready", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
if err := p.builder.HealthCheck(p.hostport); err != nil {
|
|
log.Printf("Health check failed: %v", err)
|
|
http.Error(w, "Health check failed", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
io.WriteString(w, "ok")
|
|
}
|
|
|
|
// run runs in its own goroutine.
|
|
func (p *Proxy) run() {
|
|
p.side = "a"
|
|
for {
|
|
p.poll()
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (p *Proxy) stop() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.cmd != nil {
|
|
p.cmd.Process.Kill()
|
|
}
|
|
}
|
|
|
|
// poll runs from the run loop goroutine.
|
|
func (p *Proxy) poll() {
|
|
heads := gerritMetaMap()
|
|
if heads == nil {
|
|
return
|
|
}
|
|
|
|
sig := p.builder.Signature(heads)
|
|
|
|
p.mu.Lock()
|
|
changes := sig != p.cur
|
|
curSide := p.side
|
|
p.cur = sig
|
|
p.mu.Unlock()
|
|
|
|
if !changes {
|
|
return
|
|
}
|
|
|
|
newSide := "b"
|
|
if curSide == "b" {
|
|
newSide = "a"
|
|
}
|
|
|
|
dir := filepath.Join(os.TempDir(), "tip", newSide)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
p.err = err
|
|
return
|
|
}
|
|
hostport := "localhost:8081"
|
|
if newSide == "b" {
|
|
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()
|
|
if err != nil {
|
|
log.Println(err)
|
|
p.err = err
|
|
return
|
|
}
|
|
|
|
u, err := url.Parse(fmt.Sprintf("http://%v/", hostport))
|
|
if err != nil {
|
|
log.Println(err)
|
|
p.err = err
|
|
return
|
|
}
|
|
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 {
|
|
if len(out) == 0 {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%s\n%v", out, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkout(repo, hash, path string) error {
|
|
// Clone git repo if it doesn't exist.
|
|
if _, err := os.Stat(filepath.Join(path, ".git")); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
if err := runErr(exec.Command("git", "clone", repo, path)); err != nil {
|
|
return err
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Pull down changes and update to hash.
|
|
cmd := exec.Command("git", "fetch")
|
|
cmd.Dir = path
|
|
if err := runErr(cmd); err != nil {
|
|
return err
|
|
}
|
|
cmd = exec.Command("git", "reset", "--hard", hash)
|
|
cmd.Dir = path
|
|
if err := runErr(cmd); err != nil {
|
|
return err
|
|
}
|
|
cmd = exec.Command("git", "clean", "-d", "-f", "-x")
|
|
cmd.Dir = path
|
|
return runErr(cmd)
|
|
}
|
|
|
|
// gerritMetaMap returns the map from repo name (e.g. "go") to its
|
|
// latest master hash.
|
|
// The returned map is nil on any transient error.
|
|
func gerritMetaMap() map[string]string {
|
|
res, err := http.Get(metaURL)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer res.Body.Close()
|
|
defer io.Copy(ioutil.Discard, res.Body) // ensure EOF for keep-alive
|
|
if res.StatusCode != 200 {
|
|
return nil
|
|
}
|
|
var meta map[string]struct {
|
|
Branches map[string]string
|
|
}
|
|
br := bufio.NewReader(res.Body)
|
|
// For security reasons or something, this URL starts with ")]}'\n" before
|
|
// the JSON object. So ignore that.
|
|
// Shawn Pearce says it's guaranteed to always be just one line, ending in '\n'.
|
|
for {
|
|
b, err := br.ReadByte()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if b == '\n' {
|
|
break
|
|
}
|
|
}
|
|
if err := json.NewDecoder(br).Decode(&meta); err != nil {
|
|
log.Printf("JSON decoding error from %v: %s", metaURL, err)
|
|
return nil
|
|
}
|
|
m := map[string]string{}
|
|
for repo, v := range meta {
|
|
if master, ok := v.Branches["master"]; ok {
|
|
m[repo] = master
|
|
}
|
|
}
|
|
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
|
|
}
|