From e6fb82cf21bbfe059a1b23b6cf21fb2af1486b4c Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Mon, 20 May 2019 13:48:58 -0400 Subject: [PATCH] feat(client): add health check to high-level client interface --- pkg/client/client.go | 28 ++++++++++++++++++++ pkg/client/errors.go | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 pkg/client/errors.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 0556001df..8cc33b3ac 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -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" @@ -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 { @@ -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 { diff --git a/pkg/client/errors.go b/pkg/client/errors.go new file mode 100644 index 000000000..0b25fa940 --- /dev/null +++ b/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 +} +