Skip to content

Commit

Permalink
feat: optimize gRPC error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
DMwangnima committed Nov 15, 2024
1 parent 240f4ab commit 3cc884a
Show file tree
Hide file tree
Showing 22 changed files with 1,822 additions and 361 deletions.
273 changes: 273 additions & 0 deletions internal/remote/trans/grpc/status/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file may have been modified by CloudWeGo authors. All CloudWeGo
* Modifications are Copyright 2021 CloudWeGo Authors.
*/

// Package status implements errors returned by gRPC. These errors are
// serialized and transmitted on the wire between server and client, and allow
// for additional data to be transmitted via the Details field in the status
// proto. gRPC service handlers should return an error created by this
// package, and gRPC clients should expect a corresponding error to be
// returned from the RPC call.
//
// This package upholds the invariants that a non-nil error may not
// contain an OK code, and an OK code must result in a nil error.
package status

import (
"context"
"errors"
"fmt"

spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"

"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/codes"
)

type Iface interface {
GRPCStatus() *Status
}

// Status represents an RPC status code, message, and details. It is immutable
// and should be created with New, Newf, or FromProto.
type Status struct {
s *spb.Status
// kerr is the Kitex custom error that status maps to
kerr error
}

// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
return &Status{s: &spb.Status{Code: int32(c), Message: msg}}
}

// NewWithMappingErr returns as Status representing c and msg with mapping Kitex error
func NewWithMappingErr(c codes.Code, kerr error, msg string) *Status {
st := New(c, msg)
st.kerr = kerr
return st
}

// Newf returns New(c, fmt.Sprintf(format, a...)).
func Newf(c codes.Code, format string, a ...interface{}) *Status {
return New(c, fmt.Sprintf(format, a...))
}

// NewfWithMappingErr return Newf result with mapping Kitex error
func NewfWithMappingErr(c codes.Code, kerr error, format string, a ...interface{}) *Status {
st := Newf(c, format, a...)
st.kerr = kerr
return st
}

// InjectMappingErr injects mapping Kitex error into Status
func InjectMappingErr(st *Status, kerr error) {
st.kerr = kerr
}

// ErrorProto returns an error representing s. If s.Code is OK, returns nil.
func ErrorProto(s *spb.Status) error {
return FromProto(s).Err()
}

// FromProto returns a Status representing s.
func FromProto(s *spb.Status) *Status {
return &Status{s: proto.Clone(s).(*spb.Status)}
}

// Err returns an error representing c and msg. If c is OK, returns nil.
func Err(c codes.Code, msg string) error {
return New(c, msg).Err()
}

// Errorf returns Error(c, fmt.Sprintf(format, a...)).
func Errorf(c codes.Code, format string, a ...interface{}) error {
return Err(c, fmt.Sprintf(format, a...))
}

// Code returns the status code contained in s.
func (s *Status) Code() codes.Code {
if s == nil || s.s == nil {
return codes.OK
}
return codes.Code(s.s.Code)
}

// Message returns the message contained in s.
func (s *Status) Message() string {
if s == nil || s.s == nil {
return ""
}
return s.s.Message
}

// AppendMessage append extra msg for Status
func (s *Status) AppendMessage(extraMsg string) *Status {
if s == nil || s.s == nil || extraMsg == "" {
return s
}
s.s.Message = fmt.Sprintf("%s %s", s.s.Message, extraMsg)
return s
}

// Proto returns s's status as an spb.Status proto message.
func (s *Status) Proto() *spb.Status {
if s == nil {
return nil
}
return proto.Clone(s.s).(*spb.Status)
}

// Err returns an immutable error representing s; returns nil if s.Code() is OK.
func (s *Status) Err() error {
if s.Code() == codes.OK {
return nil
}
return &Error{e: s.Proto(), kerr: s.kerr}
}

// WithDetails returns a new status with the provided details messages appended to the status.
// If any errors are encountered, it returns nil and the first error encountered.
func (s *Status) WithDetails(details ...proto.Message) (*Status, error) {
if s.Code() == codes.OK {
return nil, errors.New("no error details for status with code OK")
}
// s.Code() != OK implies that s.Proto() != nil.
p := s.Proto()
for _, detail := range details {
any, err := anypb.New(detail)
if err != nil {
return nil, err
}
p.Details = append(p.Details, any)
}
return &Status{s: p}, nil
}

// Details returns a slice of details messages attached to the status.
// If a detail cannot be decoded, the error is returned in place of the detail.
func (s *Status) Details() []interface{} {
if s == nil || s.s == nil {
return nil
}
details := make([]interface{}, 0, len(s.s.Details))
for _, any := range s.s.Details {
detail, err := any.UnmarshalNew()
if err != nil {
details = append(details, err)
continue
}
details = append(details, detail)
}
return details
}

// Error wraps a pointer of a status proto. It implements error and Status,
// and a nil *Error should never be returned by this package.
type Error struct {
e *spb.Status
// kerr is the Kitex custom error that status maps to
kerr error
}

// GetMappingErr returns the Kitex custom error that status Error maps to
func (e *Error) GetMappingErr() error {
return e.kerr
}

func (e *Error) Error() string {
str := fmt.Sprintf("rpc error: code = %d desc = %s", codes.Code(e.e.GetCode()), e.e.GetMessage())
if e.kerr == nil {
return str
}
return fmt.Sprintf("[%s] %s", e.kerr.Error(), str)
}

// GRPCStatus returns the Status represented by se.
func (e *Error) GRPCStatus() *Status {
st := FromProto(e.e)
st.kerr = e.kerr
return st
}

// Is implements future error.Is functionality.
// A Error is equivalent if the code and message are identical
// or if the underlying mapped kitex error conforms to errors.Is.
func (e *Error) Is(target error) bool {
tse, ok := target.(*Error)
if ok {
return proto.Equal(e.e, tse.e)
}
if e.kerr != nil {
return errors.Is(e.kerr, target)
}
return false
}

// FromError returns a Status representing err if it was produced from this
// package or has a method `GRPCStatus() *Status`. Otherwise, ok is false and a
// Status is returned with codes.Unknown and the original error message.
func FromError(err error) (s *Status, ok bool) {
if err == nil {
return nil, true
}
var se Iface
if errors.As(err, &se) {
return se.GRPCStatus(), true
}
return New(codes.Unknown, err.Error()), false
}

// Convert is a convenience function which removes the need to handle the
// boolean return value from FromError.
func Convert(err error) *Status {
s, _ := FromError(err)
return s
}

// Code returns the Code of the error if it is a Status error, codes.OK if err
// is nil, or codes.Unknown otherwise.
func Code(err error) codes.Code {
// Don't use FromError to avoid allocation of OK status.
if err == nil {
return codes.OK
}
var se Iface
if errors.As(err, &se) {
return se.GRPCStatus().Code()
}
return codes.Unknown
}

// FromContextError converts a context error into a Status. It returns a
// Status with codes.OK if err is nil, or a Status with codes.Unknown if err is
// non-nil and not a context error.
func FromContextError(err error) *Status {
switch err {
case nil:
return nil
case context.DeadlineExceeded:
return New(codes.DeadlineExceeded, err.Error())
case context.Canceled:
return New(codes.Canceled, err.Error())
default:
return New(codes.Unknown, err.Error())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ package status

import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"

spb "google.golang.org/genproto/googleapis/rpc/status"
Expand Down Expand Up @@ -63,24 +66,42 @@ func TestStatus(t *testing.T) {
statusNilErr, ok := FromError(nil)
test.Assert(t, ok)
test.Assert(t, statusNilErr == nil)

mappingErr := errors.New("mappingErr")
oriSt := NewWithMappingErr(codes.Internal, mappingErr, "withMappingErr test")
rawStErr := oriSt.Err()
test.Assert(t, strings.Contains(rawStErr.Error(), mappingErr.Error()), rawStErr)
test.Assert(t, errors.Is(rawStErr, mappingErr), rawStErr)
stErr, ok := rawStErr.(*Error)
test.Assert(t, ok)
test.Assert(t, stErr.GetMappingErr() == mappingErr, stErr.GetMappingErr())
st0 := stErr.GRPCStatus()
test.Assert(t, reflect.DeepEqual(st0, oriSt), st0)
st1, ok := FromError(rawStErr)
test.Assert(t, ok)
test.Assert(t, reflect.DeepEqual(st1, oriSt), st1)
st2 := Convert(rawStErr)
test.Assert(t, reflect.DeepEqual(st2, oriSt), st1)
}

func TestError(t *testing.T) {
s := new(spb.Status)
s.Code = 1
s.Message = "test err"

er := &Error{s}
kerr := errors.New("kerr")
er := &Error{e: s, kerr: kerr}
test.Assert(t, len(er.Error()) > 0)
test.Assert(t, strings.Contains(er.Error(), s.Message), er.Error())
test.Assert(t, strings.Contains(er.Error(), kerr.Error()), er.Error())

status := er.GRPCStatus()
test.Assert(t, status.Message() == s.Message)

is := er.Is(context.Canceled)
test.Assert(t, !is)
test.Assert(t, !er.Is(context.Canceled))

is = er.Is(er)
test.Assert(t, is)
test.Assert(t, er.Is(er))
test.Assert(t, er.Is(kerr))
}

func TestFromContextError(t *testing.T) {
Expand All @@ -101,7 +122,7 @@ func TestFromContextError(t *testing.T) {
s := new(spb.Status)
s.Code = 1
s.Message = "test err"
grpcErr := &Error{s}
grpcErr := &Error{e: s}
// grpc err
codeGrpcErr := Code(grpcErr)
test.Assert(t, codeGrpcErr == codes.Canceled)
Expand Down
Loading

0 comments on commit 3cc884a

Please sign in to comment.