Skip to content

Commit

Permalink
feat(client): add health check to high-level client interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ecordell committed May 20, 2019
1 parent 44c259f commit e6fb82c
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 0 deletions.
28 changes: 28 additions & 0 deletions pkg/client/client.go
Expand Up @@ -2,8 +2,10 @@ package client

import (
"context"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"

"github.com/operator-framework/operator-registry/pkg/api"
"github.com/operator-framework/operator-registry/pkg/api/grpc_health_v1"
Expand All @@ -15,6 +17,8 @@ type Interface interface {
GetBundleInPackageChannel(ctx context.Context, packageName, channelName string) (*registry.Bundle, error)
GetReplacementBundleInPackageChannel(ctx context.Context, currentName, packageName, channelName string) (*registry.Bundle, error)
GetBundleThatProvides(ctx context.Context, group, version, kind string) (*registry.Bundle, error)
HealthCheck(ctx context.Context, reconnectTimeout time.Duration) (bool, error)
Close() error
}

type Client struct {
Expand Down Expand Up @@ -61,6 +65,30 @@ func (c *Client) GetBundleThatProvides(ctx context.Context, group, version, kind
return parsedBundle, nil
}

func (c *Client) Close() error {
if c.Conn == nil {
return nil
}
return c.Conn.Close()
}

func (c *Client) HealthCheck(ctx context.Context, reconnectTimeout time.Duration) (bool, error) {
res, err := c.Health.Check(ctx, &grpc_health_v1.HealthCheckRequest{Service: "Registry"})
if err != nil {
if c.Conn.GetState() == connectivity.TransientFailure {
ctx, _ := context.WithTimeout(ctx, reconnectTimeout)
if !c.Conn.WaitForStateChange(ctx, connectivity.TransientFailure) {
return false, NewHealthError(c.Conn, HealthErrReasonUnrecoveredTransient, "connection didn't recover from TransientFailure")
}
}
return false, NewHealthError(c.Conn, HealthErrReasonConnection, err.Error())
}
if res.Status != grpc_health_v1.HealthCheckResponse_SERVING {
return false, nil
}
return true, nil
}

func NewClient(address string) (*Client, error) {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
Expand Down
61 changes: 61 additions & 0 deletions pkg/client/errors.go
@@ -0,0 +1,61 @@

package client

import (
"fmt"

"google.golang.org/grpc"
)

const (
HealthErrReasonUnrecoveredTransient = "UnrecoveredTransient"
HealthErrReasonConnection = "ConnectionError"
HealhtErrReasonUnknown = "Unknown"
)

// HealthError is used to represent error types for health checks
type HealthError struct {
ClientState string
Reason string
Message string
}

var _ error = HealthError{}

// Error implements the Error interface.
func (e HealthError) Error() string {
return fmt.Sprintf("%s: %s", e.ClientState, e.Message)
}

// unrecoverableErrors are the set of errors that mean we can't recover an install strategy
var unrecoverableErrors = map[string]struct{}{
HealthErrReasonUnrecoveredTransient: {},
}

func NewHealthError(conn *grpc.ClientConn, reason string, msg string) HealthError {
return HealthError{
ClientState: conn.GetState().String(),
Reason: reason,
Message: msg,
}
}

// IsErrorUnrecoverable reports if a given strategy error is one of the predefined unrecoverable types
func IsErrorUnrecoverable(err error) bool {
if err == nil {
return false
}
_, ok := unrecoverableErrors[reasonForError(err)]
return ok
}

func reasonForError(err error) string {
switch t := err.(type) {
case HealthError:
return t.Reason
case *HealthError:
return t.Reason
}
return HealhtErrReasonUnknown
}

0 comments on commit e6fb82c

Please sign in to comment.