internal/health: new health check package

Currently only serving Redis, but could be used for other GCP gRPC
connections.

Mildly surprising behavior: taking down Redis does not make Pool.Get
return an error: it isn't until sending another request on a connection
before the pool starts failing.  This only matters when testing the
health checks with no load.

Change-Id: Ie720b80a398fd9f7d4aa6ab0c6a88adaa85ccdc9
Reviewed-on: https://go-review.googlesource.com/76750
Reviewed-by: Tuo Shan <shantuo@google.com>
diff --git a/database/database.go b/database/database.go
index b41392f..43552e7 100644
--- a/database/database.go
+++ b/database/database.go
@@ -144,12 +144,6 @@
 		IdleTimeout: idleTimeout,
 	}
 
-	c := pool.Get()
-	if c.Err() != nil {
-		return nil, c.Err()
-	}
-	c.Close()
-
 	var rc *remote_api.Client
 	if gaeEndpoint != "" {
 		var err error
@@ -161,6 +155,16 @@
 	return &Database{Pool: pool, RemoteClient: rc}, nil
 }
 
+func (db *Database) CheckHealth() error {
+	// TODO(light): get() can trigger a dial.  Ideally, the pool could
+	// inform whether or not a lack of connections is due to idleness or
+	// errors.
+	c := db.Pool.Get()
+	err := c.Err()
+	c.Close()
+	return err
+}
+
 // Exists returns true if package with import path exists in the database.
 func (db *Database) Exists(path string) (bool, error) {
 	c := db.Pool.Get()
diff --git a/gddo-server/app.yaml b/gddo-server/app.yaml
index 8994fbc..6e75a98 100644
--- a/gddo-server/app.yaml
+++ b/gddo-server/app.yaml
@@ -1,10 +1,9 @@
 # This YAML file is used for local deployment with GAE development environment.
+env: flex
 runtime: go
-vm: true
-api_version: 1
-threadsafe: true
 
-handlers:
-- url: /.*
-  script: IGNORED
-  secure: always
+liveness_check:
+  path: /_ah/health
+
+readiness_check:
+  path: /_ah/ready
diff --git a/gddo-server/main.go b/gddo-server/main.go
index 0edaf8f..223b838 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -36,6 +36,7 @@
 	"github.com/golang/gddo/doc"
 	"github.com/golang/gddo/gosrc"
 	"github.com/golang/gddo/httputil"
+	"github.com/golang/gddo/internal/health"
 )
 
 const (
@@ -586,10 +587,6 @@
 	return s.templates.execute(resp, "bot.html", http.StatusOK, nil, nil)
 }
 
-func serveHealthCheck(resp http.ResponseWriter, req *http.Request) {
-	resp.Write([]byte("Health check: ok\n"))
-}
-
 func logError(req *http.Request, err error, rv interface{}) {
 	if err != nil {
 		var buf bytes.Buffer
@@ -936,7 +933,9 @@
 	mux.Handle("/", handler(s.serveHome))
 
 	ahMux := http.NewServeMux()
-	ahMux.HandleFunc("/_ah/health", serveHealthCheck)
+	ready := new(health.Handler)
+	ahMux.HandleFunc("/_ah/health", health.HandleLive)
+	ahMux.Handle("/_ah/ready", ready)
 
 	mainMux := http.NewServeMux()
 	mainMux.Handle("/_ah/", ahMux)
@@ -962,6 +961,7 @@
 	if err != nil {
 		return nil, fmt.Errorf("open database: %v", err)
 	}
+	ready.Add(s.db)
 	if gceLogName := v.GetString(ConfigGCELogName); gceLogName != "" {
 		logc, err := logging.NewClient(ctx, v.GetString(ConfigProject))
 		if err != nil {
diff --git a/internal/health/health.go b/internal/health/health.go
new file mode 100644
index 0000000..862e102
--- /dev/null
+++ b/internal/health/health.go
@@ -0,0 +1,66 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+// Package health provides health check handlers.
+package health
+
+import (
+	"io"
+	"net/http"
+)
+
+// Handler is an HTTP handler that reports on the success of an
+// aggregate of Checkers.  The zero value is always healthy.
+type Handler struct {
+	checkers []Checker
+}
+
+// Add adds a new check to the handler.
+func (h *Handler) Add(c Checker) {
+	h.checkers = append(h.checkers, c)
+}
+
+// ServeHTTP returns 200 if it is healthy, 500 otherwise.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
+	for _, c := range h.checkers {
+		if err := c.CheckHealth(); err != nil {
+			writeUnhealthy(w)
+			return
+		}
+	}
+	writeHealthy(w)
+}
+
+func writeUnhealthy(w http.ResponseWriter) {
+	w.Header().Set("Content-Length", "9")
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.WriteHeader(http.StatusInternalServerError)
+	io.WriteString(w, "unhealthy")
+}
+
+// HandleLive is an http.HandleFunc that handles liveness checks by
+// immediately responding with an HTTP 200 status.
+func HandleLive(w http.ResponseWriter, _ *http.Request) {
+	writeHealthy(w)
+}
+
+func writeHealthy(w http.ResponseWriter) {
+	w.Header().Set("Content-Length", "2")
+	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	w.WriteHeader(http.StatusOK)
+	io.WriteString(w, "ok")
+}
+
+// Checker wraps the CheckHealth method.
+//
+// CheckHealth returns nil if the resource is healthy, or a non-nil
+// error if the resource is not healthy.  CheckHealth must be safe to
+// call from multiple goroutines.
+type Checker interface {
+	CheckHealth() error
+}
diff --git a/internal/health/health_test.go b/internal/health/health_test.go
new file mode 100644
index 0000000..8c7893d
--- /dev/null
+++ b/internal/health/health_test.go
@@ -0,0 +1,93 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd.
+
+package health
+
+import (
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"sync"
+	"testing"
+)
+
+func TestNewHandler(t *testing.T) {
+	s := httptest.NewServer(new(Handler))
+	defer s.Close()
+	code, err := check(s)
+	if err != nil {
+		t.Fatalf("GET %s: %v", s.URL, err)
+	}
+	if code != http.StatusOK {
+		t.Errorf("got HTTP status %d; want %d", code, http.StatusOK)
+	}
+}
+
+func TestChecker(t *testing.T) {
+	c1 := &checker{err: errors.New("checker 1 down")}
+	c2 := &checker{err: errors.New("checker 2 down")}
+	h := new(Handler)
+	h.Add(c1)
+	h.Add(c2)
+	s := httptest.NewServer(h)
+	defer s.Close()
+
+	t.Run("AllUnhealthy", func(t *testing.T) {
+		code, err := check(s)
+		if err != nil {
+			t.Fatalf("GET %s: %v", s.URL, err)
+		}
+		if code != http.StatusInternalServerError {
+			t.Errorf("got HTTP status %d; want %d", code, http.StatusInternalServerError)
+		}
+	})
+	c1.set(nil)
+	t.Run("PartialHealthy", func(t *testing.T) {
+		code, err := check(s)
+		if err != nil {
+			t.Fatalf("GET %s: %v", s.URL, err)
+		}
+		if code != http.StatusInternalServerError {
+			t.Errorf("got HTTP status %d; want %d", code, http.StatusInternalServerError)
+		}
+	})
+	c2.set(nil)
+	t.Run("AllHealthy", func(t *testing.T) {
+		code, err := check(s)
+		if err != nil {
+			t.Fatalf("GET %s: %v", s.URL, err)
+		}
+		if code != http.StatusOK {
+			t.Errorf("got HTTP status %d; want %d", code, http.StatusOK)
+		}
+	})
+}
+
+func check(s *httptest.Server) (code int, err error) {
+	resp, err := s.Client().Get(s.URL)
+	if err != nil {
+		return 0, err
+	}
+	resp.Body.Close()
+	return resp.StatusCode, nil
+}
+
+type checker struct {
+	mu  sync.Mutex
+	err error
+}
+
+func (c *checker) CheckHealth() error {
+	defer c.mu.Unlock()
+	c.mu.Lock()
+	return c.err
+}
+
+func (c *checker) set(e error) {
+	defer c.mu.Unlock()
+	c.mu.Lock()
+	c.err = e
+}