Skip to content

Commit

Permalink
story(health): refactor package to export conjuction functions and ba…
Browse files Browse the repository at this point in the history
…sic toggle implementation (#141)

* refactor(issue-140): remove k8s inspired types

* feat(issue-140): added logical operator helpers for health metrics

* chore(docs): updated coverage badge.

---------

Co-authored-by: GitHub Action <[email protected]>
  • Loading branch information
Zaba505 and actions-user authored Feb 24, 2024
1 parent db7ab21 commit b1f194b
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 209 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/bedrock.svg)](https://pkg.go.dev/github.com/z5labs/bedrock)
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/bedrock)](https://goreportcard.com/report/github.com/z5labs/bedrock)
![Coverage](https://img.shields.io/badge/Coverage-95.0%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-94.8%25-brightgreen)
[![build](https://github.com/z5labs/bedrock/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/bedrock/actions/workflows/build.yaml)

**bedrock provides a minimal, modular and composable foundation for
Expand Down
15 changes: 3 additions & 12 deletions example/custom_framework/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,34 +112,25 @@ func Rest(cfg io.Reader, f func(context.Context, *http.ServeMux) error) error {
return nil, err
}

started := &health.Started{}
started.Started()

liveness := &health.Liveness{}
liveness.Alive()

readiness := &health.Readiness{}
readiness.Ready()

mux := http.NewServeMux()
mux.Handle(
"/health/liveness",
httpvalidate.Request(
httphealth.NewHandler(liveness),
httphealth.NewHandler(&health.Binary{}),
httpvalidate.ForMethods(http.MethodGet),
),
)
mux.Handle(
"/health/readiness",
httpvalidate.Request(
httphealth.NewHandler(readiness),
httphealth.NewHandler(&health.Binary{}),
httpvalidate.ForMethods(http.MethodGet),
),
)
mux.Handle(
"/health/started",
httpvalidate.Request(
httphealth.NewHandler(started),
httphealth.NewHandler(&health.Binary{}),
httpvalidate.ForMethods(http.MethodGet),
),
)
Expand Down
2 changes: 1 addition & 1 deletion example/simple_grpc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func initRuntime(ctx context.Context) (bedrock.Runtime, error) {
brgrpc.Service(
registerSimpleService,
brgrpc.ServiceName("simple"),
brgrpc.Readiness(&health.Readiness{}),
brgrpc.Readiness(&health.Binary{}),
),
// register reflection service so you can test this example
// via Insomnia, Postman and any other API testing tool that
Expand Down
11 changes: 5 additions & 6 deletions grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TransportCredentials(tc credentials.TransportCredentials) RuntimeOption {

type serviceOptions struct {
name string
readiness *health.Readiness
readiness health.Metric
}

// ServiceOption are options for configuring the gRPC health service.
Expand All @@ -79,17 +79,17 @@ func ServiceName(name string) ServiceOption {
}

// Readiness configures the health readiness metric for the gRPC service.
func Readiness(readiness *health.Readiness) ServiceOption {
func Readiness(m health.Metric) ServiceOption {
return func(so *serviceOptions) {
so.readiness = readiness
so.readiness = m
}
}

// Service registers a gRPC service with the underlying gRPC server.
func Service(f func(*grpc.Server), opts ...ServiceOption) RuntimeOption {
return func(ro *runtimeOptions) {
so := serviceOptions{
readiness: &health.Readiness{},
readiness: &health.Binary{},
}
for _, opt := range opts {
opt(&so)
Expand All @@ -103,7 +103,7 @@ func Service(f func(*grpc.Server), opts ...ServiceOption) RuntimeOption {

type serviceHealthMonitor struct {
name string
readiness *health.Readiness
readiness health.Metric
}

type grpcServer interface {
Expand Down Expand Up @@ -177,7 +177,6 @@ func (rt *Runtime) Run(ctx context.Context) error {
monitor := monitor
g.Go(func() error {
healthy := true
monitor.readiness.Ready()
rt.health.SetServingStatus(monitor.name, grpc_health_v1.HealthCheckResponse_SERVING)
for {
select {
Expand Down
14 changes: 7 additions & 7 deletions grpc/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestReadiness(t *testing.T) {
func(s *grpc.Server) {},
// No ServiceName is set so this corresponds to
// overall server health
Readiness(&health.Readiness{}),
Readiness(&health.Binary{}),
),
)
addrCh := make(chan net.Addr)
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestReadiness(t *testing.T) {
})

t.Run("if the health metric is toggled from unhealthy to healthy", func(t *testing.T) {
var readiness health.Readiness
var readiness health.Binary
rt := NewRuntime(
LogHandler(noop.LogHandler{}),
Service(
Expand Down Expand Up @@ -199,8 +199,8 @@ func TestReadiness(t *testing.T) {
if err != nil {
return err
}
readiness.NotReady()
readiness.Ready()
readiness.Toggle()
readiness.Toggle()

client := grpc_health_v1.NewHealthClient(conn)
resp, err := client.Check(gctx, &grpc_health_v1.HealthCheckRequest{
Expand Down Expand Up @@ -230,7 +230,7 @@ func TestReadiness(t *testing.T) {
Service(
func(s *grpc.Server) {},
ServiceName("test"),
Readiness(&health.Readiness{}),
Readiness(&health.Binary{}),
),
)
addrCh := make(chan net.Addr)
Expand Down Expand Up @@ -292,7 +292,7 @@ func TestReadiness(t *testing.T) {

t.Run("will return not serving", func(t *testing.T) {
t.Run("if the health metric returns not healthy", func(t *testing.T) {
var readiness health.Readiness
var readiness health.Binary
rt := NewRuntime(
LogHandler(noop.LogHandler{}),
Service(
Expand Down Expand Up @@ -335,7 +335,7 @@ func TestReadiness(t *testing.T) {
if err != nil {
return err
}
readiness.NotReady()
readiness.Toggle()
<-time.After(200 * time.Millisecond)

client := grpc_health_v1.NewHealthClient(conn)
Expand Down
139 changes: 58 additions & 81 deletions pkg/health/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package health

import (
"context"
"net/http"
"sync"
)

Expand All @@ -16,109 +15,87 @@ type Metric interface {
Healthy(context.Context) bool
}

// Started is used for signifying that the application
// requires a longer amount of time to initialize.
type Started struct {
mu sync.RWMutex
started bool
// Binary represents a health.Metric that is either healthy or not.
// The default value is represents a healthy state.
type Binary struct {
mu sync.Mutex
unhealthy bool
}

// Started marks this metric as "healthy".
func (s *Started) Started() {
s.mu.Lock()
defer s.mu.Unlock()
s.started = true
// Toggle toggles the state of Binary.
func (m *Binary) Toggle() {
m.mu.Lock()
defer m.mu.Unlock()
m.unhealthy = !m.unhealthy
}

// Healthy implements the Metric interface.
func (s *Started) Healthy(ctx context.Context) bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.started
func (m *Binary) Healthy(ctx context.Context) bool {
m.mu.Lock()
defer m.mu.Unlock()
return !m.unhealthy
}

// ServeHTTP implements the http.Handler interface.
func (s *Started) ServeHTTP(w http.ResponseWriter, req *http.Request) {
started := s.Healthy(req.Context())
if started {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusServiceUnavailable)
}

// Readiness is used for signifying that the application is
// temporarily unable to serve traffic.
type Readiness struct {
mu sync.RWMutex
ready bool
// AndMetric represents multiple Metrics all and'd together.
type AndMetric struct {
metrics []Metric
}

// NotReady marks this metric as "unhealthy".
func (r *Readiness) NotReady() {
r.mu.Lock()
defer r.mu.Unlock()
r.ready = false
// And returns a Metric where all the underlying Metrics healthy
// states are joined together via the logical and (&&) operator.
func And(metrics ...Metric) AndMetric {
return AndMetric{
metrics: metrics,
}
}

// Ready marks this metric as "healthy".
func (r *Readiness) Ready() {
r.mu.Lock()
defer r.mu.Unlock()
r.ready = true
// Healthy implements the Metric interface.
func (m AndMetric) Healthy(ctx context.Context) bool {
for _, metric := range m.metrics {
if !metric.Healthy(ctx) {
return false
}
}
return true
}

// Healthy implements the Metric interface.
func (r *Readiness) Healthy(ctx context.Context) bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.ready
// OrMetric represents multiple Metrics all or'd together.
type OrMetric struct {
metrics []Metric
}

// ServeHTTP implements the http.Handler interface.
func (r *Readiness) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ready := r.Healthy(req.Context())
if ready {
w.WriteHeader(http.StatusOK)
return
// Or returns a Metric where all the underlying Metrics healthy
// states are joined together via the logical or (||) operator.
func Or(metrics ...Metric) OrMetric {
return OrMetric{
metrics: metrics,
}
w.WriteHeader(http.StatusServiceUnavailable)
}

// Liveness is used for signifying that the application has transitioned
// to a broken state, and cannot recover execpt by being restarted.
type Liveness struct {
mu sync.RWMutex
alive bool
// Healthy implements the Metric interface.
func (m OrMetric) Healthy(ctx context.Context) bool {
for _, metric := range m.metrics {
if metric.Healthy(ctx) {
return true
}
}
return false
}

// Dead marks this metric as "unhealthy".
func (l *Liveness) Dead() {
l.mu.Lock()
defer l.mu.Unlock()
l.alive = false
// NotMetric represents the not'd value of the unerlying Metric.
type NotMetric struct {
metric Metric
}

// Alive marks this metric as "healthy".
func (l *Liveness) Alive() {
l.mu.Lock()
defer l.mu.Unlock()
l.alive = true
// And returns a Metric where the underlying Metric healthy state
// is negated with the logical not (!) operator.
func Not(metric Metric) NotMetric {
return NotMetric{
metric: metric,
}
}

// Healthy implements the Metric interface.
func (l *Liveness) Healthy(ctx context.Context) bool {
l.mu.RLock()
defer l.mu.RUnlock()
return l.alive
}

// ServeHTTP implements the http.Handler interface.
func (l *Liveness) ServeHTTP(w http.ResponseWriter, req *http.Request) {
alive := l.Healthy(req.Context())
if alive {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusServiceUnavailable)
func (m NotMetric) Healthy(ctx context.Context) bool {
return !m.metric.Healthy(ctx)
}
52 changes: 52 additions & 0 deletions pkg/health/health_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2024 Z5Labs and Contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package health

import (
"context"
"fmt"
)

func ExampleBinary() {
var b Binary
fmt.Println(b.Healthy(context.Background()))

b.Toggle()
fmt.Println(b.Healthy(context.Background()))
// Output: true
// false
}

func ExampleAnd() {
var a Binary
var b Binary
b.Toggle()

ab := And(&a, &b)
fmt.Println(ab.Healthy(context.Background()))
// Output: false
}

func ExampleOr() {
var a Binary
var b Binary
b.Toggle()

ob := Or(&a, &b)
fmt.Println(ob.Healthy(context.Background()))
// Output: true
}

func ExampleNot() {
var b Binary

nb := Not(&b)

fmt.Println(b.Healthy(context.Background()))
fmt.Println(nb.Healthy(context.Background()))
// Output: true
// false
}
Loading

0 comments on commit b1f194b

Please sign in to comment.