Add "markdown" path.
diff --git a/handlers.go b/handlers.go
index c0d11a9..8c69ec8 100644
--- a/handlers.go
+++ b/handlers.go
@@ -55,6 +55,9 @@
if s.readyPath != "" {
h.mux.HandleFunc(s.readyPath, s.handleReady)
}
+ if s.markdownPath != "" {
+ h.mux.HandleFunc(s.markdownPath, s.handleMarkdown)
+ }
return h
}
@@ -76,6 +79,11 @@
handleHealth only fails if the user's health check function tells us.
*/
func (s *HTTPScaffold) handleHealth(resp http.ResponseWriter, req *http.Request) {
+ if req.Method != "GET" {
+ resp.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
status, healthErr := s.callHealthCheck()
if status == Failed {
@@ -90,6 +98,11 @@
tells us.
*/
func (s *HTTPScaffold) handleReady(resp http.ResponseWriter, req *http.Request) {
+ if req.Method != "GET" {
+ resp.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
status, healthErr := s.callHealthCheck()
if status == OK {
healthErr = s.tracker.markedDown()
@@ -105,6 +118,22 @@
}
}
+/*
+handleMarkdown handles a request to mark down the server.
+*/
+func (s *HTTPScaffold) handleMarkdown(resp http.ResponseWriter, req *http.Request) {
+ if req.Method != s.markdownMethod {
+ resp.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ req.Body.Close()
+ s.tracker.markDown()
+ if s.markdownHandler != nil {
+ s.markdownHandler()
+ }
+}
+
func writeUnavailable(
resp http.ResponseWriter, req *http.Request,
stat HealthStatus, err error) {
diff --git a/scaffold.go b/scaffold.go
index 1a07320..cc64cc1 100644
--- a/scaffold.go
+++ b/scaffold.go
@@ -31,6 +31,11 @@
var ErrManualStop = errors.New("Shutdown called")
/*
+ErrMarkedDown is used after being marked down but before being shut down.
+*/
+var ErrMarkedDown = errors.New("Marked down")
+
+/*
HealthStatus is a type of response from a health check.
*/
type HealthStatus int
@@ -58,6 +63,15 @@
type HealthChecker func() (HealthStatus, error)
/*
+MarkdownHandler is a type of function that an user may implement in order to
+be notified when the server is marked down. The function may do anything
+it needs to do in response to a markdown request. However, markdown will
+proceed even if the function fails. In case the function takes a long time,
+the scaffold will always invoke it inside a new goroutine.
+*/
+type MarkdownHandler func()
+
+/*
An HTTPScaffold provides a set of features on top of a standard HTTP
listener. It includes an HTTP handler that may be plugged in to any
standard Go HTTP server. It is intended to be placed before any other
@@ -73,6 +87,9 @@
healthCheck HealthChecker
healthPath string
readyPath string
+ markdownPath string
+ markdownMethod string
+ markdownHandler MarkdownHandler
}
/*
@@ -155,6 +172,28 @@
}
/*
+SetMarkdown sets up a URI that will cause the server to mark it
+self down. However, this URI will not cause the server to actually shut
+down. Once any HTTP request is received on this path with a matching
+method, the server will be marked down. (the "readyPath" will respond
+with 503, and all other HTTP calls other than the "healthPath" will
+also respond with 503. The "healthPath" will still respond with 200.)
+If "handler" is not nil, the handler will be invoked and the API call
+will not return until the handler has returned. Because of that, the
+handler should return in a timely manner. (For instance, it should return
+in less than 30 seconds if Kubernetes is used unless the "grace period"
+is extended.)
+This makes this function the right thing to use
+as a "preStop" method in Kubernetes, so that the server can take action
+after shutdown to indicate that it has been deleted on purpose.
+*/
+func (s *HTTPScaffold) SetMarkdown(method, path string, handler MarkdownHandler) {
+ s.markdownPath = path
+ s.markdownMethod = method
+ s.markdownHandler = handler
+}
+
+/*
SetHealthChecker specifies a function that the scaffold will call every time
"HealthPath" or "ReadyPath" is invoked.
*/
diff --git a/scaffold_test.go b/scaffold_test.go
index e4fe194..4a82a62 100644
--- a/scaffold_test.go
+++ b/scaffold_test.go
@@ -6,6 +6,7 @@
"fmt"
"io/ioutil"
"net/http"
+ "strings"
"sync/atomic"
"time"
@@ -123,6 +124,60 @@
}, time.Second).Should(BeFalse())
})
+ It("Markdown", func() {
+ var markedDown int32
+
+ s := CreateHTTPScaffold()
+ s.SetHealthPath("/health")
+ s.SetReadyPath("/ready")
+ s.SetMarkdown("POST", "/markdown", func() {
+ atomic.StoreInt32(&markedDown, 1)
+ })
+
+ stopChan := make(chan error)
+ err := s.Open()
+ Expect(err).Should(Succeed())
+
+ go func() {
+ listenErr := s.Listen(&testHandler{})
+ stopChan <- listenErr
+ }()
+
+ // Just make sure server is listening
+ Eventually(func() bool {
+ return testGet(s, "")
+ }, 5*time.Second).Should(BeTrue())
+
+ // Ensure that we are healthy and ready
+ code, _ := getText(fmt.Sprintf("http://%s/health", s.InsecureAddress()))
+ Expect(code).Should(Equal(200))
+ code, _ = getText(fmt.Sprintf("http://%s/ready", s.InsecureAddress()))
+ Expect(code).Should(Equal(200))
+
+ // Mark the server down, but don't stop it
+ resp, err := http.Post(fmt.Sprintf("http://%s/markdown", s.InsecureAddress()),
+ "text/plain", strings.NewReader("Goodbye!"))
+ Expect(err).Should(Succeed())
+ resp.Body.Close()
+ Expect(resp.StatusCode).Should(Equal(200))
+
+ // Server should immediately be marked down, not ready, but healthy
+ Expect(atomic.LoadInt32(&markedDown)).Should(BeEquivalentTo(1))
+ code, _ = getText(fmt.Sprintf("http://%s", s.InsecureAddress()))
+ Expect(code).Should(Equal(503))
+ code, _ = getText(fmt.Sprintf("http://%s/ready", s.InsecureAddress()))
+ Expect(code).Should(Equal(503))
+ code, _ = getText(fmt.Sprintf("http://%s/health", s.InsecureAddress()))
+ Expect(code).Should(Equal(200))
+
+ // Server should not have stopped yet
+ Consistently(stopChan).ShouldNot(Receive())
+
+ stopErr := errors.New("Test stop")
+ s.Shutdown(stopErr)
+ Eventually(stopChan).Should(Receive(Equal(stopErr)))
+ })
+
It("Health Check Functions", func() {
status := int32(OK)
var healthErr = &atomic.Value{}
diff --git a/tracker.go b/tracker.go
index 0f9c9ea..72e1b69 100644
--- a/tracker.go
+++ b/tracker.go
@@ -16,6 +16,15 @@
)
/*
+values for the shutdown state
+*/
+const (
+ running int32 = iota
+ markedDown int32 = iota
+ shutDown int32 = iota
+)
+
+/*
The requestTracker keeps track of HTTP requests. In normal operations it
just counts. Once the server has been marked for shutdown, however, it
counts down to zero and returns a shutdown indication when that
@@ -26,7 +35,7 @@
// If "shutdown" is never called then this will never happen.
C chan error
shutdownWait time.Duration
- shuttingDown int32
+ shutdownState int32
shutdownReason *atomic.Value
commandChan chan int
}
@@ -40,6 +49,7 @@
rt := &requestTracker{
C: make(chan error, 1),
commandChan: make(chan int, 100),
+ shutdownState: running,
shutdownWait: shutdownWait,
shutdownReason: &atomic.Value{},
}
@@ -74,8 +84,8 @@
"Shutdown" method.
*/
func (t *requestTracker) markedDown() error {
- sd := atomic.LoadInt32(&t.shuttingDown)
- if sd != 0 {
+ ss := atomic.LoadInt32(&t.shutdownState)
+ if ss != running {
reason := t.shutdownReason.Load().(*error)
if reason == nil {
return nil
@@ -95,6 +105,11 @@
t.commandChan <- shutdown
}
+func (t *requestTracker) markDown() {
+ t.shutdownReason.Store(&ErrMarkedDown)
+ atomic.StoreInt32(&t.shutdownState, markedDown)
+}
+
func (t *requestTracker) sendStop(sent bool) bool {
if !sent {
reason := t.shutdownReason.Load().(*error)
@@ -128,7 +143,7 @@
}
case shutdown:
stopping = true
- atomic.StoreInt32(&t.shuttingDown, 1)
+ atomic.StoreInt32(&t.shutdownState, shutDown)
if activeRequests <= 0 {
sentStop = t.sendStop(sentStop)
} else {