Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

errdefs: add support for typed errors #1454

Merged
merged 12 commits into from
Apr 27, 2020
166 changes: 166 additions & 0 deletions util/grpcerrors/grpcerrors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package grpcerrors

import (
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/moby/buildkit/util/stack"
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type TypedError interface {
ToProto() TypedErrorProto
}

type TypedErrorProto interface {
proto.Message
WrapError(error) error
}

func ToGRPC(err error) error {
if err == nil {
return nil
}
st, ok := AsGRPCStatus(err)
if !ok || st == nil {
st = status.New(Code(err), err.Error())
}
if st.Code() != Code(err) {
pb := st.Proto()
pb.Code = int32(Code(err))
st = status.FromProto(pb)
}

var details []proto.Message

for _, st := range stack.Traces(err) {
details = append(details, st)
}

each(err, func(err error) {
if te, ok := err.(TypedError); ok {
details = append(details, te.ToProto())
}
})

if len(details) > 0 {
if st2, err := st.WithDetails(details...); err == nil {
st = st2
}
}

return st.Err()
}

func Code(err error) codes.Code {
if se, ok := err.(interface {
Code() codes.Code
}); ok {
return se.Code()
}

wrapped, ok := err.(interface {
Unwrap() error
})
if ok {
return Code(wrapped.Unwrap())
}

return status.FromContextError(err).Code()
}

func WrapCode(err error, code codes.Code) error {
return &withCode{error: err, code: code}
}

func AsGRPCStatus(err error) (*status.Status, bool) {
if err == nil {
return nil, true
}
if se, ok := err.(interface {
GRPCStatus() *status.Status
}); ok {
return se.GRPCStatus(), true
}

wrapped, ok := err.(interface {
Unwrap() error
})
if ok {
return AsGRPCStatus(wrapped.Unwrap())
}

return nil, false
}

func FromGRPC(err error) error {
if err == nil {
return nil
}
st, ok := status.FromError(err)
if !ok {
return err
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would you do FromGRPC(notAGRPCError{})? Maybe do (error, bool) like status.FromError.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't return any extra info/type so I don't think bool is needed and just complicated the caller side

}

pb := st.Proto()

n := &spb.Status{
Code: pb.Code,
Message: pb.Message,
}

details := make([]TypedErrorProto, 0, len(pb.Details))
stacks := make([]*stack.Stack, 0, len(pb.Details))

// details that we don't understand are copied as proto
for _, d := range pb.Details {
detail := &ptypes.DynamicAny{}
if err := ptypes.UnmarshalAny(d, detail); err != nil {
n.Details = append(n.Details, d)
continue
}

switch v := detail.Message.(type) {
case *stack.Stack:
stacks = append(stacks, v)
case TypedErrorProto:
details = append(details, v)
default:
n.Details = append(n.Details, d)
}

}

err = status.FromProto(n).Err()

for _, s := range stacks {
if s != nil {
err = stack.Wrap(err, *s)
}
}

for _, d := range details {
err = d.WrapError(err)
}

return stack.Enable(err)
}

type withCode struct {
code codes.Code
error
}

func (e *withCode) Unwrap() error {
return e.error
}

func each(err error, fn func(error)) {
fn(err)
if wrapped, ok := err.(interface {
Unwrap() error
}); ok {
each(wrapped.Unwrap(), fn)
}
}
28 changes: 28 additions & 0 deletions util/grpcerrors/intercept.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package grpcerrors

import (
"context"

"google.golang.org/grpc"
)

func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
resp, err = handler(ctx, req)
if err != nil {
err = ToGRPC(err)
}
return resp, err
}

func StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return ToGRPC(handler(srv, ss))
}

func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
return FromGRPC(invoker(ctx, method, req, reply, cc, opts...))
}

func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
s, err := streamer(ctx, desc, cc, method, opts...)
return s, ToGRPC(err)
}
3 changes: 3 additions & 0 deletions util/stack/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package stack

//go:generate protoc -I=. -I=../../vendor/ --go_out=. stack.proto
147 changes: 147 additions & 0 deletions util/stack/stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package stack

import (
"fmt"
io "io"
"os"
"strconv"
"strings"

"github.com/pkg/errors"
)

var version string
var revision string

func SetVersionInfo(v, r string) {
version = v
revision = r
}

func Traces(err error) []*Stack {
var st []*Stack

wrapped, ok := err.(interface {
Unwrap() error
})
if ok {
st = Traces(wrapped.Unwrap())
}

if ste, ok := err.(interface {
StackTrace() errors.StackTrace
}); ok {
st = append(st, convertStack(ste.StackTrace()))
}

if ste, ok := err.(interface {
StackTrace() *Stack
}); ok {
st = append(st, ste.StackTrace())
}

return st
}

func Enable(err error) error {
if err == nil {
return nil
}
if !hasLocalStackTrace(err) {
return errors.WithStack(err)
}
return err
}

func Wrap(err error, s Stack) error {
return &withStack{stack: s, error: err}
}

func hasLocalStackTrace(err error) bool {
wrapped, ok := err.(interface {
Unwrap() error
})
if ok && hasLocalStackTrace(wrapped.Unwrap()) {
return true
}

_, ok = err.(interface {
StackTrace() errors.StackTrace
})
return ok
}

func StackFormatter(err error) fmt.Formatter {
return &stackFormatter{err}
}

type stackFormatter struct {
error
}

func (w *stackFormatter) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v\n", w.Error())
Copy link
Collaborator

@tiborvass tiborvass Apr 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

w.Error() always returns a string, so no need for %+v afaict, could be %s which is less confusing (I thought there would be some kind of recursion due to %+v like it is the case in pkg/errors. If there is any recursion it will be in the Error() method itself.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this basically should be io.WriteString(s, w.Error()) like when it woudl fallthrough if not +

for _, stack := range Traces(w.error) {
fmt.Fprintf(s, "%d %s %s\n", stack.Pid, stack.Version, strings.Join(stack.Cmdline, " "))
for _, f := range stack.Frames {
fmt.Fprintf(s, "%s\n\t%s:%d\n", f.Name, f.File, f.Line)
}
fmt.Fprintf(s, "\n")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fprintln

}
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}

func convertStack(s errors.StackTrace) *Stack {
var out Stack
for _, f := range s {
dt, err := f.MarshalText()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤮

if err != nil {
continue
}
p := strings.SplitN(string(dt), " ", 2)
if len(p) != 2 {
continue
}
idx := strings.LastIndexByte(p[1], ':')
if idx == -1 {
continue
}
line, err := strconv.Atoi(p[1][idx+1:])
if err != nil {
continue
}
out.Frames = append(out.Frames, &Frame{
Name: p[0],
File: p[1][:idx],
Line: int32(line),
})
}
out.Cmdline = os.Args
out.Pid = int32(os.Getpid())
out.Version = version
out.Revision = revision
return &out
}

type withStack struct {
stack Stack
error
}

func (e *withStack) Unwrap() error {
return e.error
}

func (e *withStack) StackTrace() *Stack {
return &e.stack
}
Loading