Rework checker

This commit is contained in:
Peter 2021-05-03 13:11:24 +02:00
parent bb5d87fdf0
commit 1b8245f2bf
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
9 changed files with 500 additions and 97 deletions

View file

@ -30,16 +30,7 @@ func NewHealthServer(checker health.Checker, watchCheckPeriod time.Duration) v1.
}
func (h healthServer) Check(ctx context.Context, request *v1.HealthCheckRequest) (resp *v1.HealthCheckResponse, err error) {
var result health.Result
if result, err = h.checker.Status(ctx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, status.Error(codes.DeadlineExceeded, err.Error())
}
if errors.Is(err, context.Canceled) {
return nil, status.Error(codes.Aborted, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
}
var result = h.checker.Status(ctx)
if request.Service != "" {
known, result := result.CheckResult(request.Service)

View file

@ -3,53 +3,8 @@ package health
import (
"context"
"errors"
"fmt"
)
type Result map[string]error
func (r Result) IsHealthy() (healthy bool) {
for _, e := range r {
if e != nil {
return false
}
}
return true
}
func (r Result) CheckResult(name string) (knownCheck bool, result error) {
result, knownCheck = r[name]
return
}
type Checker interface {
AddCheck(check Check) error
Status(ctx context.Context) (Result, error)
}
type CheckError struct {
Check string
Message string
Orig error
}
func (c CheckError) Error() string {
return fmt.Sprintf("check %s failed: %s - %v", c.Check, c.Message, c.Orig)
}
func (c CheckError) Is(err error) bool {
return errors.Is(c.Orig, err)
}
func (c CheckError) Unwrap() error {
return c.Orig
}
type Check interface {
Name() string
Status(ctx context.Context) CheckError
}
var (
ErrAmbiguousCheckName = errors.New("a check with the same name is already registered")
)
@ -57,3 +12,13 @@ var (
func New() Checker {
return &checker{}
}
type Checker interface {
AddCheck(check Check) error
Status(ctx context.Context) Result
}
type Check interface {
Name() string
Status(ctx context.Context) error
}

View file

@ -0,0 +1,26 @@
package health
import "context"
func NewCheckFunc(name string, delegate func(ctx context.Context) error) Check {
return &checkDelegate{
name: name,
statusDelegate: delegate,
}
}
type checkDelegate struct {
name string
statusDelegate func(ctx context.Context) error
}
func (c checkDelegate) Name() string {
return c.name
}
func (c checkDelegate) Status(ctx context.Context) error {
if c.statusDelegate == nil {
return nil
}
return c.statusDelegate(ctx)
}

View file

@ -0,0 +1,99 @@
package health_test
import (
"context"
"errors"
"testing"
"time"
"gitlab.com/inetmock/inetmock/internal/test"
"gitlab.com/inetmock/inetmock/pkg/health"
)
func Test_checkDelegate_Name(t *testing.T) {
t.Parallel()
type fields struct {
name string
statusDelegate func(ctx context.Context) error
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Empty name",
want: "",
},
{
name: "Any name",
fields: fields{
name: "My fancy check",
},
want: "My fancy check",
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := health.NewCheckFunc(tt.fields.name, tt.fields.statusDelegate)
if got := c.Name(); got != tt.want {
t.Errorf("Name() = %v, want %v", got, tt.want)
}
})
}
}
func Test_checkDelegate_Status(t *testing.T) {
t.Parallel()
type fields struct {
name string
statusDelegate func(ctx context.Context) error
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "No delegate",
fields: fields{
name: "SampleDelegate",
},
wantErr: false,
},
{
name: "No error from delegate",
fields: fields{
name: "SampleDelegate",
statusDelegate: func(context.Context) error {
return nil
},
},
wantErr: false,
},
{
name: "Error from delegate",
fields: fields{
name: "SampleDelegate",
statusDelegate: func(context.Context) error {
return errors.New("any kind of error")
},
},
wantErr: true,
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(test.Context(t), 50*time.Millisecond)
t.Cleanup(cancel)
c := health.NewCheckFunc(tt.fields.name, tt.fields.statusDelegate)
if err := c.Status(ctx); (err != nil) != tt.wantErr {
t.Errorf("Status() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -2,39 +2,10 @@ package health
import (
"context"
"sync"
"golang.org/x/sync/errgroup"
)
type ResultWriter interface {
WriteResult(checkName string, result error)
GetResult() Result
}
func NewResultWriter() ResultWriter {
return &resultWriter{
lock: new(sync.Mutex),
result: Result{},
}
}
type resultWriter struct {
lock sync.Locker
result Result
}
func (r *resultWriter) WriteResult(checkName string, result error) {
r.lock.Lock()
defer r.lock.Unlock()
r.result[checkName] = result
}
func (r resultWriter) GetResult() Result {
return r.result
}
type checker map[string]Check
func (c *checker) AddCheck(check Check) error {
@ -47,19 +18,20 @@ func (c *checker) AddCheck(check Check) error {
return nil
}
func (c checker) Status(ctx context.Context) (res Result, err error) {
func (c checker) Status(ctx context.Context) Result {
rw := NewResultWriter()
grp, grpCtx := errgroup.WithContext(ctx)
for k, v := range c {
// pin variables
checkName := k
check := v
grp.Go(func() error {
checkErr := v.Status(grpCtx)
rw.WriteResult(k, checkErr)
return checkErr
rw.WriteResult(checkName, check.Status(grpCtx))
return nil
})
}
err = grp.Wait()
res = rw.GetResult()
return
_ = grp.Wait()
return rw.GetResult()
}

157
pkg/health/checker_test.go Normal file
View file

@ -0,0 +1,157 @@
package health_test
import (
"context"
"errors"
"testing"
"time"
"github.com/maxatome/go-testdeep/td"
"gitlab.com/inetmock/inetmock/internal/test"
"gitlab.com/inetmock/inetmock/pkg/health"
)
func Test_checker_AddCheck(t *testing.T) {
t.Parallel()
type args struct {
check health.Check
}
tests := []struct {
name string
checkerSetup func(tb testing.TB, checker health.Checker)
args args
wantErr bool
}{
{
name: "Adding check to empty checker",
args: args{
check: health.NewCheckFunc("Empty", nil),
},
wantErr: false,
},
{
name: "Adding check to non-empty checker",
checkerSetup: func(tb testing.TB, checker health.Checker) {
tb.Helper()
if err := checker.AddCheck(health.NewCheckFunc("Redis", nil)); !td.CmpNoError(tb, err) {
tb.Fail()
}
},
args: args{
check: health.NewCheckFunc("MySQL", nil),
},
wantErr: false,
},
{
name: "Adding conflicting check",
checkerSetup: func(tb testing.TB, checker health.Checker) {
tb.Helper()
if err := checker.AddCheck(health.NewCheckFunc("Redis", nil)); !td.CmpNoError(tb, err) {
tb.Fail()
}
},
args: args{
check: health.NewCheckFunc("Redis", nil),
},
wantErr: true,
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
checker := health.New()
if tt.checkerSetup != nil {
tt.checkerSetup(t, checker)
}
if err := checker.AddCheck(tt.args.check); (err != nil) != tt.wantErr {
t.Errorf("AddCheck() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_checker_Status(t *testing.T) {
t.Parallel()
tests := []struct {
name string
checker health.Checker
wantRes interface{}
}{
{
name: "Get status of empty checker - expect empty result",
checker: health.New(),
wantRes: health.Result{},
},
{
name: "Get status of single check",
checker: func() health.Checker {
checker := health.New()
_ = checker.AddCheck(newCheckOfResult("Redis", nil))
return checker
}(),
wantRes: health.Result{
"Redis": nil,
},
},
{
name: "Get status of multiple checks",
checker: func() health.Checker {
checker := health.New()
_ = checker.AddCheck(newCheckOfResult("Redis", nil))
_ = checker.AddCheck(newCheckOfResult("MySQL", nil))
return checker
}(),
wantRes: td.Map(health.Result{}, map[interface{}]interface{}{
"MySQL": nil,
"Redis": nil,
}),
},
{
name: "Get status of multiple checks with one error",
checker: func() health.Checker {
checker := health.New()
_ = checker.AddCheck(newCheckOfResult("Redis", nil))
_ = checker.AddCheck(newCheckOfResult("MySQL", nil))
_ = checker.AddCheck(newCheckOfResult("HTTP", errors.New("there's something strange in the neighborhood")))
return checker
}(),
wantRes: td.Map(health.Result{}, map[interface{}]interface{}{
"MySQL": nil,
"Redis": nil,
"HTTP": errors.New("there's something strange in the neighborhood"),
}),
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
testCtx, cancel := context.WithTimeout(test.Context(t), 50*time.Millisecond)
t.Cleanup(cancel)
gotRes := tt.checker.Status(testCtx)
td.Cmp(t, gotRes, tt.wantRes)
})
}
}
type checkOfResult struct {
name string
result error
}
func newCheckOfResult(name string, result error) health.Check {
return &checkOfResult{
name: name,
result: result,
}
}
func (c checkOfResult) Name() string {
return c.name
}
func (c checkOfResult) Status(context.Context) error {
return c.result
}

View file

@ -16,14 +16,10 @@ type healthHandler struct {
}
func (h healthHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var err error
var result health.Result
if result, err = h.checker.Status(request.Context()); err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
var result = h.checker.Status(request.Context())
if !result.IsHealthy() {
var err error
var data []byte
if data, err = json.Marshal(result); err != nil {
writer.WriteHeader(http.StatusInternalServerError)

47
pkg/health/result.go Normal file
View file

@ -0,0 +1,47 @@
package health
import "sync"
type Result map[string]error
func (r Result) IsHealthy() (healthy bool) {
for _, e := range r {
if e != nil {
return false
}
}
return true
}
func (r Result) CheckResult(name string) (knownCheck bool, result error) {
result, knownCheck = r[name]
return
}
type ResultWriter interface {
WriteResult(checkName string, result error)
GetResult() Result
}
func NewResultWriter() ResultWriter {
return &resultWriter{
lock: new(sync.Mutex),
result: Result{},
}
}
type resultWriter struct {
lock sync.Locker
result Result
}
func (r *resultWriter) WriteResult(checkName string, result error) {
r.lock.Lock()
defer r.lock.Unlock()
r.result[checkName] = result
}
func (r resultWriter) GetResult() Result {
return r.result
}

150
pkg/health/result_test.go Normal file
View file

@ -0,0 +1,150 @@
package health_test
import (
"errors"
"testing"
"github.com/maxatome/go-testdeep/td"
"gitlab.com/inetmock/inetmock/pkg/health"
)
func TestResult_IsHealthy(t *testing.T) {
t.Parallel()
tests := []struct {
name string
result health.Result
wantHealthy bool
}{
{
name: "Empty expect - expect healthy",
result: health.Result{},
wantHealthy: true,
},
{
name: "Successful test - expect healthy",
result: health.Result{
"Sample check": nil,
},
wantHealthy: true,
},
{
name: "Failed test - expect unhealthy",
result: health.Result{
"Failed check": errors.New("any kind of error"),
},
wantHealthy: false,
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if gotHealthy := tt.result.IsHealthy(); gotHealthy != tt.wantHealthy {
t.Errorf("IsHealthy() = %v, want %v", gotHealthy, tt.wantHealthy)
}
})
}
}
func TestResult_CheckResult(t *testing.T) {
t.Parallel()
type args struct {
name string
}
tests := []struct {
name string
result health.Result
args args
wantKnownCheck bool
wantErr bool
}{
{
name: "Known, successful check",
result: health.Result{
"Redis": nil,
},
args: args{
"Redis",
},
wantKnownCheck: true,
wantErr: false,
},
{
name: "Known, failed check",
result: health.Result{
"Redis": errors.New("abla habla"),
},
args: args{
"Redis",
},
wantKnownCheck: true,
wantErr: true,
},
{
name: "Unknown check",
result: health.Result{},
args: args{
"Redis",
},
wantKnownCheck: false,
wantErr: false,
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotKnownCheck, err := tt.result.CheckResult(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("CheckResult() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotKnownCheck != tt.wantKnownCheck {
t.Errorf("CheckResult() gotKnownCheck = %v, want %v", gotKnownCheck, tt.wantKnownCheck)
}
})
}
}
func Test_resultWriter_WriteResult(t *testing.T) {
t.Parallel()
type args struct {
checkName string
result error
}
tests := []struct {
name string
args args
want interface{}
}{
{
name: "Successful result",
args: args{
checkName: "Sample",
},
want: td.Map(health.Result{}, map[interface{}]interface{}{
"Sample": nil,
}),
},
{
name: "Error result - simple error",
args: args{
checkName: "Sample",
result: errors.New("critical error"),
},
want: td.Map(health.Result{}, map[interface{}]interface{}{
"Sample": errors.New("critical error"),
}),
},
}
for _, tc := range tests {
tt := tc
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := health.NewResultWriter()
r.WriteResult(tt.args.checkName, tt.args.result)
td.Cmp(t, r.GetResult(), tt.want)
})
}
}