Skip to content

Commit

Permalink
Health check implementation (feature request improbable-eng#890) (imp…
Browse files Browse the repository at this point in the history
…robable-eng#1056)

* improbable-eng#890 health check implementation (fully working, but lacks tests)

* improbable-eng#890 added tests for client health check implementation

* improbable-eng#890 defer Unlock instead of calling manually

* go.sum reverted to the original state

* improbable-eng#890 switches in main.go reworked to ServeMux, backoff implementation replaced with existing module, health check function simplified

* Update mod files

* improbable-eng#890 client health check simplified and refactored

* improbable-eng#890 test error handling, necessary comments

* improbable-eng#890 health service initialization reqorked; minor fixes in tests

* improbable-eng#890 test error handling fixed

* improbable-eng#890 test timeout handling reworked

* Update go/grpcweb/health_test.go

Co-authored-by: Johan Brandhorst-Satzkorn <[email protected]>

* Update go/grpcweb/health_test.go

Co-authored-by: Johan Brandhorst-Satzkorn <[email protected]>

* improbable-eng#890 docs regenerated

* improbable-eng#890 proper repo name

Co-authored-by: Evgeny Mikerin <[email protected]>
Co-authored-by: Johan Brandhorst-Satzkorn <[email protected]>
  • Loading branch information
3 people authored Oct 20, 2021
1 parent ccbe285 commit 502cb1e
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 44 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/improbable-eng/grpc-web
go 1.16

require (
github.com/cenkalti/backoff/v4 v4.1.1
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f
github.com/golang/protobuf v1.4.3
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
Expand All @@ -17,9 +18,8 @@ require (
github.com/sirupsen/logrus v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506 // indirect
google.golang.org/grpc v1.32.0
Expand Down
18 changes: 10 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down Expand Up @@ -373,8 +376,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -408,15 +411,14 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
9 changes: 9 additions & 0 deletions go/grpcweb/DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ If you'd like to have a standalone binary, please take a look at `grpcwebproxy`.

## Usage

#### func ClientHealthCheck

```go
func ClientHealthCheck(ctx context.Context, backendConn *grpc.ClientConn, service string, setServingStatus func(serving bool)) error
```
Client health check function is also part of the grpc/internal package The
following code is a simplified version of client.go For more details see:
https://pkg.go.dev/google.golang.org/grpc/health

#### func ListGRPCResources

```go
Expand Down
65 changes: 65 additions & 0 deletions go/grpcweb/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package grpcweb

import (
"context"
"time"

backoff "github.com/cenkalti/backoff/v4"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
)

const healthCheckMethod = "/grpc.health.v1.Health/Watch"

// Client health check function is also part of the grpc/internal package
// The following code is a simplified version of client.go
// For more details see: https://pkg.go.dev/google.golang.org/grpc/health
func ClientHealthCheck(ctx context.Context, backendConn *grpc.ClientConn, service string, setServingStatus func(serving bool)) error {
shouldBackoff := false // No need for backoff on the first connection attempt
backoffSrc := backoff.NewExponentialBackOff()
healthClient := healthpb.NewHealthClient(backendConn)

for {
// Backs off if the connection has failed in some way without receiving a message in the previous retry.
if shouldBackoff {
select {
case <-time.After(backoffSrc.NextBackOff()):
case <-ctx.Done():
return nil
}
}
shouldBackoff = true // we should backoff next time, since we attempt connecting below

req := healthpb.HealthCheckRequest{Service: service}
s, err := healthClient.Watch(ctx, &req)
if err != nil {
continue
}

resp := new(healthpb.HealthCheckResponse)
for {
err = s.RecvMsg(resp)
if err != nil {
setServingStatus(false)
// The health check functionality should be disabled if health check service is not implemented on the backend
if status.Code(err) == codes.Unimplemented {
return err
}
// breaking out of the loop, since we got an error from Recv, triggering reconnect
break
}

// As a message has been received, removes the need for backoff for the next retry.
shouldBackoff = false
backoffSrc.Reset()

if resp.Status == healthpb.HealthCheckResponse_SERVING {
setServingStatus(true)
} else {
setServingStatus(false)
}
}
}
}
142 changes: 142 additions & 0 deletions go/grpcweb/health_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package grpcweb_test

import (
"context"
"net"
"testing"
"time"

"github.com/improbable-eng/grpc-web/go/grpcweb"
testproto "github.com/improbable-eng/grpc-web/integration_test/go/_proto/improbable/grpcweb/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

func TestClientWithNoHealthServiceOnServer(t *testing.T) {
// Set up and run a server with no health check handler registered
grpcServer := grpc.NewServer()
testproto.RegisterTestServiceServer(grpcServer, &testServiceImpl{})
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)

go func() {
_ = grpcServer.Serve(listener)
}()
t.Cleanup(grpcServer.Stop)

grpcClientConn, err := grpc.Dial(listener.Addr().String(),
grpc.WithBlock(),
grpc.WithTimeout(100*time.Millisecond),
grpc.WithInsecure(),
)
require.NoError(t, err)

ctx := context.Background()

servingStatus := true
err = grpcweb.ClientHealthCheck(ctx, grpcClientConn, "", func(serving bool) {
servingStatus = serving
})
assert.Error(t, err)
assert.False(t, servingStatus)
}

type clientHealthTestData struct {
listener net.Listener
serving bool
healthServer *health.Server
}

func setupTestData(t *testing.T) clientHealthTestData {
s := clientHealthTestData{}

grpcServer := grpc.NewServer()
s.healthServer = health.NewServer()
healthpb.RegisterHealthServer(grpcServer, s.healthServer)

var err error
s.listener, err = net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)

go func() {
grpcServer.Serve(s.listener)
}()
t.Cleanup(grpcServer.Stop)

return s
}

func (s *clientHealthTestData) dialBackend(t *testing.T) *grpc.ClientConn {
grpcClientConn, err := grpc.Dial(s.listener.Addr().String(),
grpc.WithBlock(),
grpc.WithTimeout(100*time.Millisecond),
grpc.WithInsecure(),
)
require.NoError(t, err)
return grpcClientConn
}

func (s *clientHealthTestData) checkServingStatus(t *testing.T, expStatus bool) {
for start := time.Now(); time.Since(start) < 100*time.Millisecond; {
if s.serving == expStatus {
break
}
}
assert.Equal(t, expStatus, s.serving)
}

func (s *clientHealthTestData) startClientHealthCheck(ctx context.Context, conn *grpc.ClientConn) {
go func() {
_ = grpcweb.ClientHealthCheck(ctx, conn, "", func(status bool) {
s.serving = status
})
}()
}

func TestClientHealthCheck_FailsIfNotServing(t *testing.T) {
s := setupTestData(t)

s.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)

backendConn := s.dialBackend(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

s.startClientHealthCheck(ctx, backendConn)
s.checkServingStatus(t, false)
}

func TestClientHealthCheck_SucceedsIfServing(t *testing.T) {
s := setupTestData(t)

s.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)

backendConn := s.dialBackend(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

s.startClientHealthCheck(ctx, backendConn)
s.checkServingStatus(t, true)
}

func TestClientHealthCheck_ReactsToStatusChange(t *testing.T) {
s := setupTestData(t)

s.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)

backendConn := s.dialBackend(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

s.startClientHealthCheck(ctx, backendConn)
s.checkServingStatus(t, false)

s.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
s.checkServingStatus(t, true)

s.healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
s.checkServingStatus(t, false)
}
Loading

0 comments on commit 502cb1e

Please sign in to comment.