blob: 1a073200cc4d1628d7c92bcceb90d6dd2f7ad9e3 [file] [log] [blame]
package goscaffold
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"time"
)
const (
// DefaultGraceTimeout is the default amount of time to wait for a request
// to complete. Default is 30 seconds, which is also the default grace period
// in Kubernetes.
DefaultGraceTimeout = 30 * time.Second
)
/*
ErrSignalCaught is used in the "Shutdown" mechanism when the shutdown was
caused by a SIGINT or SIGTERM.
*/
var ErrSignalCaught = errors.New("Caught shutdown signal")
/*
ErrManualStop is used when the user doesn't have a reason.
*/
var ErrManualStop = errors.New("Shutdown called")
/*
HealthStatus is a type of response from a health check.
*/
type HealthStatus int
//go:generate stringer -type HealthStatus .
const (
// OK denotes that everything is good
OK HealthStatus = iota
// NotReady denotes that the server is OK, but cannot process requests now
NotReady HealthStatus = iota
// Failed denotes that the server is bad
Failed HealthStatus = iota
)
/*
HealthChecker is a type of function that an implementer may
implement in order to customize what we return from the "health"
and "ready" URLs. It must return either "OK", which means that everything
is fine, "not ready," which means that the "ready" check will fail but
the health check is OK, and "failed," which means that both are bad.
The function may return an optional error, which will be returned as
a reason for the status and will be placed in responses.
*/
type HealthChecker func() (HealthStatus, error)
/*
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
handlers.
*/
type HTTPScaffold struct {
insecurePort int
managementPort int
open bool
tracker *requestTracker
insecureListener net.Listener
managementListener net.Listener
healthCheck HealthChecker
healthPath string
readyPath string
}
/*
CreateHTTPScaffold makes a new scaffold. The default scaffold will
do nothing.
*/
func CreateHTTPScaffold() *HTTPScaffold {
return &HTTPScaffold{
insecurePort: 0,
managementPort: -1,
open: false,
}
}
/*
SetInsecurePort sets the port number to listen on in regular "HTTP" mode.
It may be set to zero, which indicates to listen on an ephemeral port.
It must be called before "listen".
*/
func (s *HTTPScaffold) SetInsecurePort(ip int) {
s.insecurePort = ip
}
/*
InsecureAddress returns the actual address (including the port if an
ephemeral port was used) where we are listening. It must only be
called after "Listen."
*/
func (s *HTTPScaffold) InsecureAddress() string {
if s.insecureListener == nil {
return ""
}
return s.insecureListener.Addr().String()
}
/*
SetManagementPort sets the port number for management operations, including
health checks and diagnostic operations. If not set, then these operations
happen on the other ports. If set, then they only happen on this port.
*/
func (s *HTTPScaffold) SetManagementPort(p int) {
s.managementPort = p
}
/*
ManagementAddress returns the actual address (including the port if an
ephemeral port was used) where we are listening for management
operations. If "SetManagementPort" was not set, then it returns null.
*/
func (s *HTTPScaffold) ManagementAddress() string {
if s.managementListener == nil {
return ""
}
return s.managementListener.Addr().String()
}
/*
SetHealthPath sets up a health check on the management port (if set) or
otherwise the main port. If a health check function has been supplied,
it will return 503 if the function returns "Failed" and 200 otherwise.
This path is intended to be used by systems like Kubernetes as the
"health check." These systems will shut down the server if we return
a non-200 URL.
*/
func (s *HTTPScaffold) SetHealthPath(p string) {
s.healthPath = p
}
/*
SetReadyPath sets up a readines check on the management port (if set) or
otherwise the main port. If a health check function has been supplied,
it will return 503 if the function returns "Failed" or "Not Ready".
It will also return 503 if the "Shutdown" function was called
(or caught by signal handler). This path is intended to be used by
load balancers that will decide whether to route calls, but not by
systems like Kubernetes that will decide to shut down this server.
*/
func (s *HTTPScaffold) SetReadyPath(p string) {
s.readyPath = p
}
/*
SetHealthChecker specifies a function that the scaffold will call every time
"HealthPath" or "ReadyPath" is invoked.
*/
func (s *HTTPScaffold) SetHealthChecker(c HealthChecker) {
s.healthCheck = c
}
/*
Open opens up the ports that were created when the scaffold was set up.
This method is optional. It may be called before Listen so that we can
retrieve the actual address where the server is listening before we actually
start to listen.
*/
func (s *HTTPScaffold) Open() error {
s.tracker = startRequestTracker(DefaultGraceTimeout)
il, err := net.ListenTCP("tcp", &net.TCPAddr{
Port: s.insecurePort,
})
if err != nil {
return err
}
s.insecureListener = il
defer func() {
if !s.open {
il.Close()
}
}()
if s.managementPort >= 0 {
ml, err := net.ListenTCP("tcp", &net.TCPAddr{
Port: s.managementPort,
})
if err != nil {
return err
}
s.managementListener = ml
defer func() {
if !s.open {
ml.Close()
}
}()
}
s.open = true
return nil
}
/*
Listen should be called instead of using the standard "http" and "net"
libraries. It will open a port (or ports) and begin listening for
HTTP traffic. It will block until the server is shut down by
the various methods in this class.
It will use the graceful shutdown logic to ensure that once marked down,
the server will not exit until all the requests have completed,
or until the shutdown timeout has expired.
Like http.Serve, this function will block until we are done serving HTTP.
If "SetInsecurePort" or "SetSecurePort" were not set, then it will listen on
a dynamic port.
Listen will block until the server is shutdown using "Shutdown" or one of
the other shutdown mechanisms. It must not be called until after "Open"
has been called.
If shut down, Listen will return the error that was passed to the "shutdown"
method.
*/
func (s *HTTPScaffold) Listen(baseHandler http.Handler) error {
if !s.open {
err := s.Open()
if err != nil {
return err
}
s.open = true
}
// This is the handler that wraps customer API calls with tracking
trackingHandler := &requestHandler{
s: s,
child: baseHandler,
}
mgmtHandler := s.createManagementHandler()
var mainHandler http.Handler
if s.managementPort >= 0 {
// Management on separate port
mainHandler = trackingHandler
go http.Serve(s.managementListener, mgmtHandler)
} else {
// Management on same port
mgmtHandler.child = trackingHandler
mainHandler = mgmtHandler
}
go http.Serve(s.insecureListener, mainHandler)
err := <-s.tracker.C
s.insecureListener.Close()
if s.managementListener != nil {
s.managementListener.Close()
}
return err
}
/*
Shutdown indicates that the server should stop handling incoming requests
and exit from the "Serve" call. This may be called automatically by
calling "CatchSignals," or automatically using this call. If
"reason" is nil, a default reason will be assigned.
*/
func (s *HTTPScaffold) Shutdown(reason error) {
if reason == nil {
s.tracker.shutdown(ErrManualStop)
} else {
s.tracker.shutdown(reason)
}
}
/*
CatchSignals directs the scaffold to listen for common signals. It catches
three signals. SIGINT (aka control-C) and SIGTERM (what "kill" sends by default)
will cause the program to be marked down, and "SignalCaught" will be returned
by the "Listen" method. SIGHUP ("kill -1" or "kill -HUP") will cause the
stack trace of all the threads to be printed to stderr, just like a Java program.
*/
func (s *HTTPScaffold) CatchSignals() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
signal.Notify(sigChan, syscall.SIGTERM)
signal.Notify(sigChan, syscall.SIGHUP)
go func() {
for {
sig := <-sigChan
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
s.Shutdown(ErrSignalCaught)
signal.Reset()
return
case syscall.SIGHUP:
dumpStack()
}
}
}()
}
func dumpStack() {
stackSize := 32767
stackBuf := make([]byte, stackSize)
var w int
for w < stackSize {
w = runtime.Stack(stackBuf, true)
if w == stackSize {
stackSize *= 2
stackBuf = make([]byte, stackSize)
}
}
fmt.Fprint(os.Stderr, string(stackBuf[:w]))
}