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 {