Skip to content

Commit

Permalink
secondary: add SummarizeErrors variadic method
Browse files Browse the repository at this point in the history
I was recently debugging a workflow with lots
of async workers able to cancel each other,
so that by the time an error bubbled up it was
ambiguous where the longest stack would be
and whether there were errors in two places
because one wrapped the other or because of two
separate root errors. I felt like what would be
most convenient would be to smoosh everything
together rather than try to be too smart about it.

This PR implements SummarizeErrors, which is mainly
the generalization of CombineErrors over n errors.
If one argument is detectably already wrapping another,
the result will ignore the latter. If two errors are
distinct but both wrapping the same error, the result
will preserve that information.
Two errors are distinct if err1 != err2; there didn't
look to be much benefit in bringing in things like .Is
as we're really interested in identity, not equivalence.
  • Loading branch information
HonoreDB committed Apr 25, 2022
1 parent 676eef5 commit c224793
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 0 deletions.
39 changes: 39 additions & 0 deletions secondary/secondary.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package secondary

import "github.com/cockroachdb/errors/errbase"

// WithSecondaryError enhances the error given as first argument with
// an annotation that carries the error given as second argument. The
// second error does not participate in cause analysis (Is, etc) and
Expand Down Expand Up @@ -44,3 +46,40 @@ func CombineErrors(err error, otherErr error) error {
}
return WithSecondaryError(err, otherErr)
}

// SummarizeErrors reduces a collection of errors to a single
// error with the rest as secondary errors, making an effort
// at deduplication. Use when it's not clear, or not deterministic,
// which of many errors will be the root cause.
func SummarizeErrors(errs ...error) error {
if len(errs) == 0 {
return nil
}
uniqArgsInOrder := make([]error, 0, len(errs))
uniqArgsMap := make(map[error]struct{}, len(errs))
refCount := make(map[error]int)
for _, e := range errs {
if _, dup := uniqArgsMap[e]; !dup {
uniqArgsMap[e] = struct{}{}
uniqArgsInOrder = append(uniqArgsInOrder, e)
walk(e, func(w error) { refCount[w] = refCount[w] + 1 })
}
}
var retVal error
for _, e := range uniqArgsInOrder {
if refCount[e] == 1 {
retVal = CombineErrors(retVal, e)
}
}
return retVal
}

func walk(err error, fn func(error)) {
if err != nil {
fn(err)
walk(errbase.UnwrapOnce(err), fn)
if se, ok := err.(*withSecondaryError); ok {
walk(se.secondaryError, fn)
}
}
}
27 changes: 27 additions & 0 deletions secondary/secondary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ func TestCombineErrors(t *testing.T) {
}
}

// This test asserts SummarizeErrors output in terms of CombineErrors output.
func TestSummarizeErrors(t *testing.T) {
tt := testutils.T{T: t}
err1 := errors.New("err1")
err2 := errors.New("err2")
chainWith2 := &werrFmt{err2, "chainWith2"}
chainWith1And2 := secondary.CombineErrors(chainWith2, err1)

testData := []struct {
args []error
summary error
}{
{[]error{err1, err2}, secondary.CombineErrors(err1, err2)},
{[]error{err2, err1}, secondary.CombineErrors(err2, err1)},
{[]error{err1, err2, err1}, secondary.CombineErrors(err1, err2)},
{[]error{chainWith2, err2}, chainWith2},
{[]error{err2, chainWith2}, chainWith2},
{[]error{chainWith2, err1, err2}, secondary.CombineErrors(chainWith2, err1)},
{[]error{chainWith2, chainWith1And2, err1, err2}, chainWith1And2},
}

for _, test := range testData {
err := secondary.SummarizeErrors(test.args...)
tt.CheckDeepEqual(err, test.summary)
}
}

func TestFormat(t *testing.T) {
tt := testutils.T{t}

Expand Down
8 changes: 8 additions & 0 deletions secondary_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ func WithSecondaryError(err error, additionalErr error) error {
func CombineErrors(err, otherErr error) error {
return secondary.CombineErrors(err, otherErr)
}

// SummarizeErrors reduces a collection of errors to a single
// error with the rest as secondary errors, making an effort
// at deduplication. Use when it's not clear, or not deterministic,
// which of many errors will be the root cause.
func SummarizeErrors(errs ...error) error {
return secondary.SummarizeErrors(errs...)
}

0 comments on commit c224793

Please sign in to comment.