From 6022faf38302b354934727fbe5a34851f9fb95b6 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 19 Jun 2024 00:06:25 -0700 Subject: [PATCH 1/3] Add typeurl to go mod Signed-off-by: Derek McGowan --- go.mod | 6 +++++- go.sum | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d6525c7..51cca1c 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,13 @@ module github.com/containerd/errdefs go 1.20 -require google.golang.org/grpc v1.58.3 +require ( + github.com/containerd/typeurl/v2 v2.1.1 + google.golang.org/grpc v1.58.3 +) require ( + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect golang.org/x/sys v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect diff --git a/go.sum b/go.sum index 4e7237e..66bcada 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,51 @@ +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= From 70fd2d7ff216659f37f286c42cf827f450d9d074 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 18 Jun 2024 18:14:24 -0700 Subject: [PATCH 2/3] Add collapsible error type Signed-off-by: Derek McGowan --- internal/types/collapsible.go | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 internal/types/collapsible.go diff --git a/internal/types/collapsible.go b/internal/types/collapsible.go new file mode 100644 index 0000000..a37e772 --- /dev/null +++ b/internal/types/collapsible.go @@ -0,0 +1,57 @@ +/* + Copyright The containerd 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. +*/ + +package types + +import "fmt" + +// CollapsibleError indicates the error should be collapsed +type CollapsibleError interface { + CollapseError() +} + +// CollapsedError returns a new error with the collapsed +// error returned on unwrapped or when formatted with "%+v" +func CollapsedError(err error, collapsed ...error) error { + return collapsedError{err, collapsed} +} + +type collapsedError struct { + error + collapsed []error +} + +func (c collapsedError) Unwrap() []error { + return append([]error{c.error}, c.collapsed...) +} + +func (c collapsedError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", c.error) + for _, err := range c.collapsed { + fmt.Fprintf(s, "\n%+v", err) + } + return + } + fallthrough + case 's': + fmt.Fprint(s, c.Error()) + case 'q': + fmt.Fprintf(s, "%q", c.Error()) + } +} From f96dfdab01b66580d2451fee46e05f002c9ea157 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 6 Jun 2024 16:04:07 -0700 Subject: [PATCH 3/3] Add stack package for managing error stack traces Signed-off-by: Derek McGowan --- stack/stack.go | 296 ++++++++++++++++++++++++++++++++++++++++++++ stack/stack_test.go | 98 +++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 stack/stack.go create mode 100644 stack/stack_test.go diff --git a/stack/stack.go b/stack/stack.go new file mode 100644 index 0000000..befbf3c --- /dev/null +++ b/stack/stack.go @@ -0,0 +1,296 @@ +/* + Copyright The containerd 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. +*/ + +package stack + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path" + "runtime" + "strings" + "sync/atomic" + "unsafe" + + "github.com/containerd/typeurl/v2" + + "github.com/containerd/errdefs/internal/types" +) + +func init() { + typeurl.Register((*stack)(nil), "github.com/containerd/errdefs", "stack+json") +} + +var ( + // Version is version of running process + Version string = "dev" + + // Revision is the specific revision of the running process + Revision string = "dirty" +) + +type stack struct { + decoded *Trace + + callers []uintptr + helpers []uintptr +} + +// Trace is a stack trace along with process information about the source +type Trace struct { + Version string `json:"version,omitempty"` + Revision string `json:"revision,omitempty"` + Cmdline []string `json:"cmdline,omitempty"` + Frames []Frame `json:"frames,omitempty"` + Pid int32 `json:"pid,omitempty"` +} + +// Frame is a single frame of the trace representing a line of code +type Frame struct { + Name string `json:"Name,omitempty"` + File string `json:"File,omitempty"` + Line int32 `json:"Line,omitempty"` +} + +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + fmt.Fprintf(s, "%s\n\t%s:%d\n", f.Name, f.File, f.Line) + default: + fmt.Fprint(s, f.Name) + } + case 's': + fmt.Fprint(s, path.Base(f.Name)) + case 'q': + fmt.Fprintf(s, "%q", path.Base(f.Name)) + } +} + +// callers returns the current stack, skipping over the number of frames mentioned +// Frames with skip=0: +// +// frame[0] runtime.Callers +// frame[1] github.com/containerd/errdefs/stack.callers +// frame[2] (Use skip=2 to have this be first frame) +func callers(skip int) *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(skip, pcs[:]) + return &stack{ + callers: pcs[0:n], + } +} + +func (s *stack) getDecoded() *Trace { + if s.decoded == nil { + var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) + + var helpers map[string]struct{} + if len(s.helpers) > 0 { + helpers = make(map[string]struct{}) + frames := runtime.CallersFrames(s.helpers) + for { + frame, more := frames.Next() + helpers[frame.Function] = struct{}{} + if !more { + break + } + } + } + + f := make([]Frame, 0, len(s.callers)) + if len(s.callers) > 0 { + frames := runtime.CallersFrames(s.callers) + for { + frame, more := frames.Next() + if _, ok := helpers[frame.Function]; !ok { + f = append(f, Frame{ + Name: frame.Function, + File: frame.File, + Line: int32(frame.Line), + }) + } + if !more { + break + } + } + } + + t := Trace{ + Version: Version, + Revision: Revision, + Cmdline: os.Args, + Frames: f, + Pid: int32(os.Getpid()), + } + + atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) + } + + return s.decoded +} + +func (s *stack) Error() string { + return fmt.Sprintf("%+v", s.getDecoded()) +} + +func (s *stack) MarshalJSON() ([]byte, error) { + return json.Marshal(s.getDecoded()) +} + +func (s *stack) UnmarshalJSON(b []byte) error { + var unsafeDecoded = (*unsafe.Pointer)(unsafe.Pointer(&s.decoded)) + var t Trace + + if err := json.Unmarshal(b, &t); err != nil { + return err + } + + atomic.StorePointer(unsafeDecoded, unsafe.Pointer(&t)) + + return nil +} + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + if st.Flag('+') { + t := s.getDecoded() + fmt.Fprintf(st, "%d %s %s\n", t.Pid, t.Version, strings.Join(t.Cmdline, " ")) + for _, f := range t.Frames { + f.Format(st, verb) + } + fmt.Fprintln(st) + return + } + } +} + +func (s *stack) StackTrace() Trace { + return *s.getDecoded() +} + +func (s *stack) CollapseError() {} + +// ErrStack returns a new error for the callers stack, +// this can be wrapped or joined into an existing error. +// NOTE: When joined with errors.Join, the stack +// will show up in the error string output. +// Use with `stack.Join` to force addition of the +// error stack. +func ErrStack() error { + return callers(3) +} + +// Join adds a stack if there is no stack included to the errors +// and returns a joined error with the stack hidden from the error +// output. The stack error shows up when Unwrapped or formatted +// with `%+v`. +func Join(errs ...error) error { + return joinErrors(nil, errs) +} + +// WithStack will check if the error already has a stack otherwise +// return a new error with the error joined with a stack error +// Any helpers will be skipped. +func WithStack(ctx context.Context, errs ...error) error { + return joinErrors(ctx.Value(helperKey{}), errs) +} + +func joinErrors(helperVal any, errs []error) error { + var filtered []error + var collapsible []error + var hasStack bool + for _, err := range errs { + if err != nil { + if !hasStack && hasLocalStackTrace(err) { + hasStack = true + } + if _, ok := err.(types.CollapsibleError); ok { + collapsible = append(collapsible, err) + } else { + filtered = append(filtered, err) + } + + } + } + if len(filtered) == 0 { + return nil + } + if !hasStack { + s := callers(4) + if helpers, ok := helperVal.([]uintptr); ok { + s.helpers = helpers + } + collapsible = append(collapsible, s) + } + var err error + if len(filtered) > 1 { + err = errors.Join(filtered...) + } else { + err = filtered[0] + } + if len(collapsible) == 0 { + return err + } + + return types.CollapsedError(err, collapsible...) +} + +func hasLocalStackTrace(err error) bool { + switch e := err.(type) { + case *stack: + return true + case interface{ Unwrap() error }: + if hasLocalStackTrace(e.Unwrap()) { + return true + } + case interface{ Unwrap() []error }: + for _, ue := range e.Unwrap() { + if hasLocalStackTrace(ue) { + return true + } + } + } + + // TODO: Consider if pkg/errors compatibility is needed + // NOTE: This was implemented before the standard error package + // so it may unwrap and have this interface. + //if _, ok := err.(interface{ StackTrace() pkgerrors.StackTrace }); ok { + // return true + //} + + return false +} + +type helperKey struct{} + +// WithHelper marks the context as from a helper function +// This will add an additional skip to the error stack trace +func WithHelper(ctx context.Context) context.Context { + helpers, _ := ctx.Value(helperKey{}).([]uintptr) + var pcs [1]uintptr + n := runtime.Callers(2, pcs[:]) + if n == 1 { + ctx = context.WithValue(ctx, helperKey{}, append(helpers, pcs[0])) + } + return ctx +} diff --git a/stack/stack_test.go b/stack/stack_test.go new file mode 100644 index 0000000..00cb81d --- /dev/null +++ b/stack/stack_test.go @@ -0,0 +1,98 @@ +/* + Copyright The containerd 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. +*/ + +package stack + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" +) + +func TestStack(t *testing.T) { + s := callers(2) + if len(s.callers) == 0 { + t.Fatalf("expected callers, got:\n%v", s) + } + tr := s.getDecoded() + if len(tr.Frames) != len(s.callers) { + t.Fatalf("expected 1 frame, got %d", len(tr.Frames)) + } + if name := tr.Frames[0].Name; !strings.HasSuffix(name, "."+t.Name()) { + t.Fatalf("unexpected frame: %s\n%v", name, s) + } +} + +func TestCollapsed(t *testing.T) { + checkError := func(err error, expected string) { + t.Helper() + if err.Error() != expected { + t.Fatalf("unexpected error string %q, expected %q", err.Error(), expected) + } + + if printed := fmt.Sprintf("%v", err); printed != expected { + t.Fatalf("unexpected error string %q, expected %q", printed, expected) + } + + if printed := fmt.Sprintf("%+v", err); !strings.HasPrefix(printed, expected) || !strings.Contains(printed, t.Name()) { + t.Fatalf("unexpected error string %q, expected %q with stack containing %q", printed, expected, t.Name()) + } + } + expected := "some error" + checkError(Join(errors.New(expected)), expected) + checkError(Join(errors.New(expected), ErrStack()), expected) + checkError(WithStack(context.Background(), errors.New(expected)), expected) +} + +func TestHelpers(t *testing.T) { + checkError := func(err error, expected string, withHelper bool) { + t.Helper() + if err.Error() != expected { + t.Fatalf("unexpected error string %q, expected %q", err.Error(), expected) + } + + if printed := fmt.Sprintf("%v", err); printed != expected { + t.Fatalf("unexpected error string %q, expected %q", printed, expected) + } + + printed := fmt.Sprintf("%+v", err) + if !strings.HasPrefix(printed, expected) || !strings.Contains(printed, t.Name()) { + t.Fatalf("unexpected error string %q, expected %q with stack containing %q", printed, expected, t.Name()) + } + if withHelper { + if !strings.Contains(printed, "testHelper") { + t.Fatalf("unexpected error string, expected stack containing testHelper:\n%s", printed) + } + } else if strings.Contains(printed, "testHelper") { + t.Fatalf("unexpected error string, expected stack with no containing testHelper:\n%s", printed) + } + } + expected := "some error" + checkError(Join(errors.New(expected)), expected, false) + checkError(testHelper(expected, false), expected, true) + checkError(testHelper(expected, true), expected, false) +} + +func testHelper(msg string, withHelper bool) error { + if withHelper { + return WithStack(WithHelper(context.Background()), errors.New(msg)) + } else { + return WithStack(context.Background(), errors.New(msg)) + } + +}