github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/grpcutil/grpc_util.go (about)

     1  // Copyright 2014 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package grpcutil
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  	"io"
    17  	"strings"
    18  
    19  	"github.com/cockroachdb/cockroach/pkg/util/netutil"
    20  	"github.com/cockroachdb/errors"
    21  	"google.golang.org/grpc"
    22  	"google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/connectivity"
    24  	"google.golang.org/grpc/status"
    25  )
    26  
    27  // ErrCannotReuseClientConn is returned when a failed connection is
    28  // being reused. We require that new connections be created with
    29  // pkg/rpc.GRPCDial instead.
    30  var ErrCannotReuseClientConn = errors.New("cannot reuse client connection")
    31  
    32  type localRequestKey struct{}
    33  
    34  // NewLocalRequestContext returns a Context that can be used for local (in-process) requests.
    35  func NewLocalRequestContext(ctx context.Context) context.Context {
    36  	return context.WithValue(ctx, localRequestKey{}, struct{}{})
    37  }
    38  
    39  // IsLocalRequestContext returns true if this context is marked for local (in-process) use.
    40  func IsLocalRequestContext(ctx context.Context) bool {
    41  	return ctx.Value(localRequestKey{}) != nil
    42  }
    43  
    44  // IsTimeout returns true if err's Cause is a gRPC timeout, or the request
    45  // was canceled by a context timeout.
    46  func IsTimeout(err error) bool {
    47  	if errors.Is(err, context.DeadlineExceeded) {
    48  		return true
    49  	}
    50  	err = errors.Cause(err)
    51  	if s, ok := status.FromError(err); ok {
    52  		return s.Code() == codes.DeadlineExceeded
    53  	}
    54  	return false
    55  }
    56  
    57  // IsClosedConnection returns true if err's Cause is an error produced by gRPC
    58  // on closed connections.
    59  func IsClosedConnection(err error) bool {
    60  	if errors.Is(err, ErrCannotReuseClientConn) {
    61  		return true
    62  	}
    63  	err = errors.Cause(err)
    64  	if s, ok := status.FromError(err); ok {
    65  		if s.Code() == codes.Canceled ||
    66  			s.Code() == codes.Unavailable {
    67  			return true
    68  		}
    69  	}
    70  	if errors.Is(err, context.Canceled) ||
    71  		strings.Contains(err.Error(), "is closing") ||
    72  		strings.Contains(err.Error(), "tls: use of closed connection") ||
    73  		strings.Contains(err.Error(), "use of closed network connection") ||
    74  		strings.Contains(err.Error(), io.ErrClosedPipe.Error()) ||
    75  		strings.Contains(err.Error(), io.EOF.Error()) ||
    76  		strings.Contains(err.Error(), "node unavailable") {
    77  		return true
    78  	}
    79  	return netutil.IsClosedConnection(err)
    80  }
    81  
    82  // RequestDidNotStart returns true if the given error from gRPC
    83  // means that the request definitely could not have started on the
    84  // remote server.
    85  //
    86  // This method currently depends on implementation details, matching
    87  // on the text of an error message that is known to only be used
    88  // in this case in the version of gRPC that we use today. We will
    89  // need to watch for changes here in future versions of gRPC.
    90  // TODO(bdarnell): Replace this with a cleaner mechanism when/if
    91  // https://github.com/grpc/grpc-go/issues/1443 is resolved.
    92  func RequestDidNotStart(err error) bool {
    93  	if errors.HasType(err, connectionNotReadyError{}) ||
    94  		errors.HasType(err, (*netutil.InitialHeartbeatFailedError)(nil)) {
    95  		return true
    96  	}
    97  	s, ok := status.FromError(errors.Cause(err))
    98  	if !ok {
    99  		// This is a non-gRPC error; assume nothing.
   100  		return false
   101  	}
   102  	// TODO(bdarnell): In gRPC 1.7, we have no good way to distinguish
   103  	// ambiguous from unambiguous failures, so we must assume all gRPC
   104  	// errors are ambiguous.
   105  	// https://github.com/cockroachdb/cockroach/issues/19708#issuecomment-343891640
   106  	if false && s.Code() == codes.Unavailable && s.Message() == "grpc: the connection is unavailable" {
   107  		return true
   108  	}
   109  	return false
   110  }
   111  
   112  // ConnectionReady returns nil if the given connection is ready to
   113  // send a request, or an error (which will pass RequestDidNotStart) if
   114  // not.
   115  //
   116  // This is a workaround for the fact that gRPC 1.7 fails to
   117  // distinguish between ambiguous and unambiguous errors.
   118  //
   119  // This is designed for use with connections prepared by
   120  // pkg/rpc.Connection.Connect (which performs an initial heartbeat and
   121  // thereby ensures that we will never see a connection in the
   122  // first-time Connecting state).
   123  func ConnectionReady(conn *grpc.ClientConn) error {
   124  	if s := conn.GetState(); s == connectivity.TransientFailure {
   125  		return connectionNotReadyError{s}
   126  	}
   127  	return nil
   128  }
   129  
   130  type connectionNotReadyError struct {
   131  	state connectivity.State
   132  }
   133  
   134  func (e connectionNotReadyError) Error() string {
   135  	return fmt.Sprintf("connection not ready: %s", e.state)
   136  }