Skip to content

Commit

Permalink
Introducing Alive/Ready endpoint (#93)
Browse files Browse the repository at this point in the history
Signed-off-by: Angelos Roussakis <[email protected]>
  • Loading branch information
asenlog authored and Sotirios Mantziaris committed Oct 22, 2019
1 parent ab07780 commit ef20a4b
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 135 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Patron is a framework for creating microservices, originally created by Sotiris

`Patron` is french for `template` or `pattern`, but it means also `boss` which we found out later (no pun intended).

The entry point of the framework is the `Service`. The `Service` uses `Components` to handle the processing of sync and async requests. The `Service` starts by default an `HTTP Component` which hosts the debug, health and metric endpoints. Any other endpoints will be added to the default `HTTP Component` as `Routes`. Alongside `Routes` one can specify middleware functions to be applied ordered to all routes as `MiddlewareFunc`. The service set's up by default logging with `zerolog`, tracing and metrics with `jaeger` and `prometheus`.
The entry point of the framework is the `Service`. The `Service` uses `Components` to handle the processing of sync and async requests. The `Service` starts by default an `HTTP Component` which hosts the debug, alive, ready and metric endpoints. Any other endpoints will be added to the default `HTTP Component` as `Routes`. Alongside `Routes` one can specify middleware functions to be applied ordered to all routes as `MiddlewareFunc`. The service set's up by default logging with `zerolog`, tracing and metrics with `jaeger` and `prometheus`.

`Patron` provides abstractions for the following functionality of the framework:

Expand Down Expand Up @@ -75,7 +75,8 @@ The `Service` has the role of glueing all of the above together, which are:
- setting up logging
- setting up default HTTP component with the following endpoints configured:
- profiling via pprof
- health check
- liveness check
- readiness check
- setting up termination by os signal
- setting up SIGHUP custom hook if provided by an option
- starting and stopping components
Expand Down
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Patron is french for template or pattern, but it means also boss which we found
The entry point of the framework is the Service.
The Service uses Components to handle the processing of sync and async requests.
The Service can setup as many Components it wants, even multiple HTTP components provided the port does not collide.
The Service starts by default a HTTP component which hosts the debug, health and metric endpoints.
The Service starts by default a HTTP component which hosts the debug, alive, ready and metric endpoints.
Any other endpoints will be added to the default HTTP Component as Routes.
The service set's up by default logging with zerolog, tracing and metrics with jaeger and prometheus.
Expand Down
24 changes: 18 additions & 6 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,26 @@ func Middlewares(mm ...http.MiddlewareFunc) OptionFunc {
}
}

// HealthCheck option for overriding the default health check of the default HTTP component.
func HealthCheck(hcf http.HealthCheckFunc) OptionFunc {
// AliveCheck option for overriding the default liveness check of the default HTTP component.
func AliveCheck(acf http.AliveCheckFunc) OptionFunc {
return func(s *Service) error {
if hcf == nil {
return errors.New("health check func is required")
if acf == nil {
return errors.New("alive check func is required")
}
s.hcf = hcf
log.Info("health check func is set")
s.acf = acf
log.Info("alive check func is set")
return nil
}
}

// ReadyCheck option for overriding the default readiness check of the default HTTP component.
func ReadyCheck(rcf http.ReadyCheckFunc) OptionFunc {
return func(s *Service) error {
if rcf == nil {
return errors.New("ready check func is required")
}
s.rcf = rcf
log.Info("ready check func is set")
return nil
}
}
Expand Down
36 changes: 31 additions & 5 deletions option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,49 @@ func TestMiddlewares(t *testing.T) {
}
}

func TestHealthCheck(t *testing.T) {
func TestAliveCheck(t *testing.T) {
type args struct {
hcf phttp.HealthCheckFunc
acf phttp.AliveCheckFunc
}
tests := []struct {
name string
args args
wantErr bool
}{
{"failure due to nil health check", args{hcf: nil}, true},
{"success", args{hcf: phttp.DefaultHealthCheck}, false},
{"failure due to nil liveness check", args{acf: nil}, true},
{"success", args{acf: phttp.DefaultAliveCheck}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := New("test", "1.0.0")
assert.NoError(t, err)
err = HealthCheck(tt.args.hcf)(s)
err = AliveCheck(tt.args.acf)(s)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func TestReadyCheck(t *testing.T) {
type args struct {
rcf phttp.ReadyCheckFunc
}
tests := []struct {
name string
args args
wantErr bool
}{
{"failure due to nil liveness check", args{rcf: nil}, true},
{"success", args{rcf: phttp.DefaultReadyCheck}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := New("test", "1.0.0")
assert.NoError(t, err)
err = ReadyCheck(tt.args.rcf)(s)
if tt.wantErr {
assert.Error(t, err)
} else {
Expand Down
14 changes: 10 additions & 4 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type Service struct {
cps []Component
routes []http.Route
middlewares []http.MiddlewareFunc
hcf http.HealthCheckFunc
acf http.AliveCheckFunc
rcf http.ReadyCheckFunc
termSig chan os.Signal
sighupHandler func()
}
Expand All @@ -46,7 +47,8 @@ func New(name, version string, oo ...OptionFunc) (*Service, error) {

s := Service{
cps: []Component{},
hcf: http.DefaultHealthCheck,
acf: http.DefaultAliveCheck,
rcf: http.DefaultReadyCheck,
termSig: make(chan os.Signal, 1),
sighupHandler: func() { log.Info("SIGHUP received: nothing setup") },
middlewares: []http.MiddlewareFunc{},
Expand Down Expand Up @@ -189,8 +191,12 @@ func (s *Service) createHTTPComponent() (Component, error) {
http.Port(int(portVal)),
}

if s.hcf != nil {
options = append(options, http.HealthCheck(s.hcf))
if s.acf != nil {
options = append(options, http.AliveCheck(s.acf))
}

if s.rcf != nil {
options = append(options, http.ReadyCheck(s.rcf))
}

if s.routes != nil {
Expand Down
33 changes: 33 additions & 0 deletions sync/http/alive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package http

import (
"net/http"
)

// AliveStatus type representing the liveness of the service via HTTP component.
type AliveStatus int

const (
// Alive represents a state defining a Alive state.
Alive AliveStatus = 1
// Unresponsive represents a state defining a Unresponsive state.
Unresponsive AliveStatus = 2
)

// AliveCheckFunc defines a function type for implementing a liveness check.
type AliveCheckFunc func() AliveStatus

func aliveCheckRoute(acf AliveCheckFunc) Route {

f := func(w http.ResponseWriter, r *http.Request) {
switch acf() {
case Alive:
w.WriteHeader(http.StatusOK)
case Unresponsive:
w.WriteHeader(http.StatusServiceUnavailable)
default:
w.WriteHeader(http.StatusOK)
}
}
return NewRouteRaw("/alive", http.MethodGet, f, false)
}
31 changes: 31 additions & 0 deletions sync/http/alive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package http

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_aliveCheckRoute(t *testing.T) {
tests := []struct {
name string
acf AliveCheckFunc
want int
}{
{"alive", func() AliveStatus { return Alive }, http.StatusOK},
{"unresponsive", func() AliveStatus { return Unresponsive }, http.StatusServiceUnavailable},
{"default", func() AliveStatus { return 10 }, http.StatusOK},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := aliveCheckRoute(tt.acf)
resp := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/alive", nil)
assert.NoError(t, err)
r.Handler(resp, req)
assert.Equal(t, tt.want, resp.Code)
})
}
}
14 changes: 9 additions & 5 deletions sync/http/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ const (
)

var (
// DefaultHealthCheck returns always healthy.
DefaultHealthCheck = func() HealthStatus { return Healthy }
// DefaultAliveCheck return always live.
DefaultAliveCheck = func() AliveStatus { return Alive }
DefaultReadyCheck = func() ReadyStatus { return Ready }
)

// Component implementation of HTTP.
type Component struct {
hc HealthCheckFunc
ac AliveCheckFunc
rc ReadyCheckFunc
httpPort int
httpReadTimeout time.Duration
httpWriteTimeout time.Duration
Expand All @@ -40,7 +42,8 @@ type Component struct {
// New returns a new component.
func New(oo ...OptionFunc) (*Component, error) {
c := Component{
hc: DefaultHealthCheck,
ac: DefaultAliveCheck,
rc: DefaultReadyCheck,
httpPort: httpPort,
httpReadTimeout: httpReadTimeout,
httpWriteTimeout: httpWriteTimeout,
Expand All @@ -56,7 +59,8 @@ func New(oo ...OptionFunc) (*Component, error) {
}
}

c.routes = append(c.routes, healthCheckRoute(c.hc))
c.routes = append(c.routes, aliveCheckRoute(c.ac))
c.routes = append(c.routes, readyCheckRoute(c.rc))
c.routes = append(c.routes, profilingRoutes()...)
c.routes = append(c.routes, metricRoute())

Expand Down
4 changes: 2 additions & 2 deletions sync/http/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestComponent_ListenAndServe_DefaultRoutes_Shutdown(t *testing.T) {
done <- true
}()
time.Sleep(100 * time.Millisecond)
assert.Len(t, s.routes, 14)
assert.Len(t, s.routes, 15)
cnl()
assert.True(t, <-done)
}
Expand All @@ -66,7 +66,7 @@ func TestComponent_ListenAndServeTLS_DefaultRoutes_Shutdown(t *testing.T) {
done <- true
}()
time.Sleep(100 * time.Millisecond)
assert.Len(t, s.routes, 14)
assert.Len(t, s.routes, 15)
cnl()
assert.True(t, <-done)
}
Expand Down
37 changes: 0 additions & 37 deletions sync/http/health.go

This file was deleted.

32 changes: 0 additions & 32 deletions sync/http/health_test.go

This file was deleted.

21 changes: 16 additions & 5 deletions sync/http/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,24 @@ func Middlewares(mm ...MiddlewareFunc) OptionFunc {
}
}

// HealthCheck option for setting the health check function of the HTTP component.
func HealthCheck(hcf HealthCheckFunc) OptionFunc {
// AliveCheck option for setting the liveness check function of the HTTP component.
func AliveCheck(acf AliveCheckFunc) OptionFunc {
return func(s *Component) error {
if hcf == nil {
return errors.New("health check function is not defined")
if acf == nil {
return errors.New("alive check function is not defined")
}
s.hc = hcf
s.ac = acf
return nil
}
}

// ReadyCheck option for setting the readiness check function of the HTTP component.
func ReadyCheck(rcf ReadyCheckFunc) OptionFunc {
return func(s *Component) error {
if rcf == nil {
return errors.New("alive check function is not defined")
}
s.rc = rcf
return nil
}
}
Expand Down
Loading

0 comments on commit ef20a4b

Please sign in to comment.